From 55bc7aac88ff96852c259dc576a70385ba8c81f6 Mon Sep 17 00:00:00 2001 From: Xarvex Date: Thu, 6 Mar 2025 20:34:38 -0600 Subject: [PATCH] refactor!: change layout; import and build change Fixes: #200 Fixes: #365 Fixes: #512 Fixes: #800 fix(pyproject): resolve mix-up of mypy and pytest chore(ci): remove legacy scripts chore: format with new mypy rules; fix translation test wip(ci/mypy): remove config flag fix(pyinstaller): use correct dict access fix(resources): usage in ts_qt.py feat(nix/package): validate tests with pytest hook fix(nix/package): remove old dependency patch feat(nix): support Darwin fix(nix/package): move check deps to checkInputs fix(nix/shell): typo fix(nix/shell): correctly wrap Python with Qt args fix(pyproject): specify mypy-extensions feat(nix/package): provide pillow-jxl-plugin nix(nix/package): split into multiple files, allow overriding of JXL and vtf2img fix(nix/shell): provide FFmpeg on runtime feat(flake): output pillow-jxl-plugin and vtf2img fix(nix/package): load pipewire feat(nix/package): run tests on pillow-jxl-plugin fix: remove extra noqa comment docs: update installation docs docs: shrink table size on docs site nit(nix/package): pipewire not needed in buildInputs docs: update commands, environment, setup fix: use consistent possessives chore: format with prettier, add ignore flags fix(pyinstaller): consume from pyproject Revert "fix(pyinstaller): consume from pyproject" This reverts commit 398cd4e5630a3e83d22d15286d7ac59b4c07c5d6. refactor: use icon from resource manager Also fixes incorrect path currently used in ts_qt.py. nix(pyinstaller): replace use of sys.platform with platform.system docs: add build section Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> --- .envrc.recommended | 22 +- .github/FUNDING.yml | 1 + .github/ISSUE_TEMPLATE/bug_report.yml | 3 +- .github/ISSUE_TEMPLATE/feature_request.yml | 1 + .github/workflows/mypy.yaml | 28 +- .github/workflows/publish_docs.yaml | 37 +- .github/workflows/pytest.yaml | 49 +- .github/workflows/release.yml | 99 +++- .github/workflows/ruff.yaml | 29 +- .gitignore | 3 + Build_MacOS_app.sh | 69 --- Build_win.bat | 31 -- CONTRIBUTING.md | 98 +--- README.md | 40 +- TagStudio.sh | 7 - docs/help/ffmpeg.md | 2 +- docs/index.md | 14 +- docs/install.md | 370 ++++++++++++- docs/library/entry.md | 4 +- docs/library/entry_groups.md | 2 +- docs/library/field.md | 2 +- docs/library/index.md | 4 +- docs/library/library_search.md | 6 +- docs/library/tag.md | 2 +- docs/library/tag_overrides.md | 2 +- docs/stylesheets/extra.css | 3 + docs/updates/roadmap.md | 4 +- docs/updates/schema_changes.md | 2 +- docs/usage.md | 10 +- flake.lock | 502 +----------------- flake.nix | 224 ++------ mkdocs.yml | 4 + nix/package/default.nix | 114 ++++ nix/package/pillow-jxl-plugin.nix | 67 +++ nix/package/pyexiv2.nix | 32 ++ nix/package/vtf2img.nix | 29 + nix/shell.nix | 93 ++++ pyproject.toml | 116 ++-- requirements-dev.txt | 8 - requirements.txt | 20 - src/tagstudio/core/driver.py | 7 +- src/tagstudio/core/enums.py | 6 +- src/tagstudio/core/library/__init__.py | 1 - .../core/library/alchemy/__init__.py | 5 - src/tagstudio/core/library/alchemy/db.py | 4 +- .../library/alchemy/default_color_groups.py | 2 +- src/tagstudio/core/library/alchemy/enums.py | 7 +- src/tagstudio/core/library/alchemy/fields.py | 7 +- src/tagstudio/core/library/alchemy/joins.py | 3 +- src/tagstudio/core/library/alchemy/library.py | 54 +- src/tagstudio/core/library/alchemy/models.py | 12 +- .../core/library/alchemy/visitors.py | 21 +- src/tagstudio/core/library/json/__init__.py | 0 src/tagstudio/core/library/json/library.py | 27 +- src/tagstudio/core/media_types.py | 120 ++--- src/tagstudio/core/palette.py | 4 +- src/tagstudio/core/query_lang/__init__.py | 11 - src/tagstudio/core/query_lang/parser.py | 14 +- src/tagstudio/core/query_lang/tokenizer.py | 4 +- src/tagstudio/core/ts_core.py | 9 +- src/tagstudio/core/utils/dupe_files.py | 6 +- src/tagstudio/core/utils/missing_files.py | 6 +- src/tagstudio/core/utils/refresh_dir.py | 6 +- src/tagstudio/main.py | 7 +- src/tagstudio/qt/cache_manager.py | 11 +- src/tagstudio/qt/flowlayout.py | 1 + .../qt/helpers/blender_thumbnailer.py | 5 +- src/tagstudio/qt/helpers/color_overlay.py | 3 +- src/tagstudio/qt/helpers/file_deleter.py | 1 + src/tagstudio/qt/helpers/file_opener.py | 3 +- src/tagstudio/qt/helpers/file_tester.py | 3 +- src/tagstudio/qt/helpers/function_iterator.py | 2 + src/tagstudio/qt/helpers/gradient.py | 1 + src/tagstudio/qt/helpers/image_effects.py | 1 + src/tagstudio/qt/helpers/qbutton_wrapper.py | 1 + .../qt/helpers/rounded_pixmap_style.py | 5 +- src/tagstudio/qt/helpers/silent_popen.py | 2 + src/tagstudio/qt/helpers/text_wrapper.py | 1 + src/tagstudio/qt/helpers/vendored/ffmpeg.py | 3 +- .../helpers/vendored/pydub/audio_segment.py | 15 +- .../qt/helpers/vendored/pydub/utils.py | 5 +- src/tagstudio/qt/main_window.py | 37 +- src/tagstudio/qt/modals/about.py | 13 +- src/tagstudio/qt/modals/add_field.py | 10 +- src/tagstudio/qt/modals/build_color.py | 24 +- src/tagstudio/qt/modals/build_namespace.py | 23 +- src/tagstudio/qt/modals/build_tag.py | 38 +- src/tagstudio/qt/modals/delete_unlinked.py | 21 +- src/tagstudio/qt/modals/drop_import.py | 17 +- src/tagstudio/qt/modals/ffmpeg_checker.py | 3 +- src/tagstudio/qt/modals/file_extension.py | 9 +- src/tagstudio/qt/modals/fix_dupes.py | 22 +- src/tagstudio/qt/modals/fix_unlinked.py | 21 +- src/tagstudio/qt/modals/folders_to_tags.py | 18 +- src/tagstudio/qt/modals/merge_dupe_entries.py | 12 +- src/tagstudio/qt/modals/mirror_entities.py | 9 +- src/tagstudio/qt/modals/relink_unlinked.py | 7 +- src/tagstudio/qt/modals/settings_panel.py | 7 +- src/tagstudio/qt/modals/tag_color_manager.py | 22 +- .../qt/modals/tag_color_selection.py | 17 +- src/tagstudio/qt/modals/tag_database.py | 15 +- src/tagstudio/qt/modals/tag_search.py | 27 +- src/tagstudio/qt/pagination.py | 16 +- src/tagstudio/qt/platform_strings.py | 3 +- src/tagstudio/qt/resource_manager.py | 8 +- src/tagstudio/qt/resources.json | 2 +- src/tagstudio/qt/splash.py | 24 +- src/tagstudio/qt/translations.py | 2 +- src/tagstudio/qt/ts_qt.py | 122 ++--- src/tagstudio/qt/widgets/clickable_label.py | 1 + src/tagstudio/qt/widgets/collage_icon.py | 15 +- src/tagstudio/qt/widgets/color_box.py | 25 +- src/tagstudio/qt/widgets/fields.py | 11 +- src/tagstudio/qt/widgets/item_thumb.py | 33 +- src/tagstudio/qt/widgets/landing.py | 11 +- src/tagstudio/qt/widgets/media_player.py | 2 +- src/tagstudio/qt/widgets/migration_modal.py | 51 +- .../widgets/paged_panel/paged_body_wrapper.py | 6 +- .../qt/widgets/paged_panel/paged_panel.py | 11 +- .../widgets/paged_panel/paged_panel_state.py | 3 +- src/tagstudio/qt/widgets/panel.py | 10 +- .../qt/widgets/preview/field_containers.py | 42 +- .../qt/widgets/preview/file_attributes.py | 37 +- .../qt/widgets/preview/preview_thumb.py | 33 +- src/tagstudio/qt/widgets/preview_panel.py | 34 +- src/tagstudio/qt/widgets/progress.py | 12 +- src/tagstudio/qt/widgets/tag.py | 21 +- src/tagstudio/qt/widgets/tag_box.py | 17 +- src/tagstudio/qt/widgets/tag_color_label.py | 18 +- src/tagstudio/qt/widgets/tag_color_preview.py | 19 +- src/tagstudio/qt/widgets/text.py | 3 +- src/tagstudio/qt/widgets/text_box_edit.py | 3 +- src/tagstudio/qt/widgets/text_line_edit.py | 5 +- src/tagstudio/qt/widgets/thumb_button.py | 3 +- src/tagstudio/qt/widgets/thumb_renderer.py | 37 +- src/tagstudio/qt/widgets/video_player.py | 23 +- start_win.bat | 2 - tagstudio.spec | 78 +-- tests/conftest.py | 10 +- tests/macros/test_dupe_entries.py | 4 +- tests/macros/test_folders_tags.py | 2 +- tests/macros/test_missing_files.py | 7 +- tests/macros/test_refresh_dir.py | 5 +- tests/macros/test_sidecar.py | 4 +- tests/qt/test_build_tag_panel.py | 6 +- tests/qt/test_field_containers.py | 2 +- tests/qt/test_flow_widget.py | 3 +- tests/qt/test_folders_to_tags.py | 2 +- tests/qt/test_item_thumb.py | 5 +- tests/qt/test_preview_panel.py | 2 +- tests/qt/test_qt_driver.py | 6 +- tests/qt/test_tag_panel.py | 4 +- tests/qt/test_tag_search_panel.py | 2 +- tests/qt/test_thumb_renderer.py | 3 +- tests/test_db_migrations.py | 6 +- tests/test_driver.py | 9 +- tests/test_json_migration.py | 5 +- tests/test_library.py | 11 +- tests/test_search.py | 7 +- tests/test_translations.py | 26 +- 160 files changed, 1944 insertions(+), 1861 deletions(-) delete mode 100755 Build_MacOS_app.sh delete mode 100644 Build_win.bat delete mode 100755 TagStudio.sh create mode 100644 docs/stylesheets/extra.css create mode 100644 nix/package/default.nix create mode 100644 nix/package/pillow-jxl-plugin.nix create mode 100644 nix/package/pyexiv2.nix create mode 100644 nix/package/vtf2img.nix create mode 100644 nix/shell.nix delete mode 100644 requirements-dev.txt delete mode 100644 requirements.txt delete mode 100644 src/tagstudio/core/library/__init__.py delete mode 100644 src/tagstudio/core/library/alchemy/__init__.py delete mode 100644 src/tagstudio/core/library/json/__init__.py delete mode 100644 src/tagstudio/core/query_lang/__init__.py delete mode 100644 start_win.bat diff --git a/.envrc.recommended b/.envrc.recommended index 94dd6bc2..da9b036e 100644 --- a/.envrc.recommended +++ b/.envrc.recommended @@ -1,16 +1,18 @@ -# vi: ft=bash -# # If you wish to use this file, copy or symlink it to `.envrc` for direnv to read it. # This will use the flake development shell. -if ! has nix_direnv_version || ! nix_direnv_version 3.0.5; then - source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.5/direnvrc" "sha256-RuwIS+QKFj/T9M2TFXScjBsLR6V3A17YVoEW/Q6AZ1w=" +if ! has nix_direnv_version || ! nix_direnv_version 3.0.6; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.6/direnvrc" "sha256-RYcUJaRMf8oF5LznDrlCXbkOQrywm0HDv1VjYGaJGdM=" fi -devenv_root_file="$(mktemp -t devenv-root-XXXXXXXX)" -printf %s "${PWD}" >"${devenv_root_file}" -if ! use flake . --override-input devenv-root "file+file://${devenv_root_file}"; then - printf '%s\n' "devenv could not be built. The devenv environment was not loaded. Make the necessary changes to devenv.nix and hit enter to try again." >&2 +if [ -f .venv/bin/activate ]; then + # If the file is watched when it does not exist, + # direnv will execute again when it gets created. + watch_file .venv/bin/activate fi -rm "${devenv_root_file}" -unset devenv_root_file +watch_file nix/shell.nix +watch_file pyproject.toml + +use flake + +# vi: ft=bash diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 57b3052b..c2cdaaef 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ +--- patreon: cyanvoxel diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 4bfe6d49..a719991f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,3 +1,4 @@ +--- name: Bug Report description: File a bug or issue report. title: '[Bug]: ' @@ -51,7 +52,7 @@ body: - type: textarea attributes: label: Steps to Reproduce - description: Minimal steps neded for the problem to occur. + description: Minimal steps needed for the problem to occur. placeholder: | 1. 2. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 18abecb4..32259fb6 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,3 +1,4 @@ +--- name: Feature Request description: Suggest a new feature. title: '[Feature Request]: ' diff --git a/.github/workflows/mypy.yaml b/.github/workflows/mypy.yaml index 344375f5..df695692 100644 --- a/.github/workflows/mypy.yaml +++ b/.github/workflows/mypy.yaml @@ -1,38 +1,34 @@ +--- name: MyPy -on: [ push, pull_request ] - +on: [push, pull_request] jobs: mypy: name: Run MyPy runs-on: ubuntu-latest - steps: - - name: Checkout code + - name: Checkout repo uses: actions/checkout@v4 - - uses: reviewdog/action-setup@v1 + - name: Setup reviewdog + uses: reviewdog/action-setup@v1 with: reviewdog_version: latest - - uses: actions/setup-python@v5 + - name: Setup Python + uses: actions/setup-python@v5 with: python-version: '3.12' - cache: 'pip' + cache: pip - - name: Install dependencies + - name: Install Python dependencies run: | python -m pip install --upgrade uv - uv pip install --system -r requirements.txt - uv pip install --system mypy==1.11.2 - mkdir tagstudio/.mypy_cache + uv pip install --system .[mypy] - - uses: tsuyoshicho/action-mypy@v4 + - name: Execute MyPy + uses: tsuyoshicho/action-mypy@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} - reporter: github-check fail_on_error: true - workdir: tagstudio - level: error - mypy_flags: --config-file ../pyproject.toml diff --git a/.github/workflows/publish_docs.yaml b/.github/workflows/publish_docs.yaml index b5a67547..4bda2b75 100644 --- a/.github/workflows/publish_docs.yaml +++ b/.github/workflows/publish_docs.yaml @@ -1,19 +1,20 @@ -name: Publish Docs +--- +name: Publish Docs on: push: branches: - main paths: - - 'docs/**' - - 'mkdocs.yml' - - 'CHANGELOG.md' - - '.github/workflows/publish_docs.yaml' + - .github/workflows/publish_docs.yaml + - docs/** + - mkdocs.yml + - CHANGELOG.md permissions: contents: write -concurrency: +concurrency: group: publish-docs cancel-in-progress: true @@ -21,18 +22,28 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 with: - python-version: 3.x - - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV + python-version: '3.12' + cache: pip + + - name: Install Python dependencies + run: | + python -m pip install --upgrade uv + uv pip install --system .[mkdocs] + + - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV + - uses: actions/cache@v4 with: key: mkdocs-material-${{ env.cache_id }} path: .cache restore-keys: | mkdocs-material- - - run: pip install mkdocs-material mkdocs-material[imaging] - - run: mkdocs gh-deploy --force - + - name: Execute mkdocs + run: mkdocs gh-deploy --force diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 77b12a13..2a8bf7e8 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -1,10 +1,11 @@ +--- name: pytest -on: [ push, pull_request ] +on: [push, pull_request] jobs: pytest: - name: Run tests + name: Run pytest runs-on: ubuntu-24.04 steps: @@ -15,56 +16,54 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.12' - cache: 'pip' + cache: pip + + - name: Install Python dependencies + run: | + python -m pip install --upgrade uv + uv pip install --system .[pytest] - name: Install system dependencies run: | - # dont run update, it is slow sudo apt-get update sudo apt-get install -y --no-install-recommends \ - libxkbcommon-x11-0 \ - x11-utils \ - libyaml-dev \ - libgl1 \ libegl1 \ + libgl1 \ + libopengl0 \ + libpulse0 \ + libxcb-cursor0 \ libxcb-icccm4 \ libxcb-image0 \ libxcb-keysyms1 \ libxcb-randr0 \ libxcb-render-util0 \ libxcb-xinerama0 \ - libopengl0 \ - libxcb-cursor0 \ - libpulse0 + libxkbcommon-x11-0 \ + libyaml-dev \ + x11-utils - - name: Install dependencies - run: | - python -m pip install --upgrade uv - uv pip install --system -r requirements.txt - uv pip install --system -r requirements-dev.txt - - - name: Run pytest + - name: Execute pytest run: | xvfb-run pytest --cov-report xml --cov=tagstudio - - name: Store coverage + - name: Upload coverage uses: actions/upload-artifact@v4 with: - name: 'coverage' - path: 'coverage.xml' + name: coverage + path: coverage.xml coverage: - name: Check Code Coverage + name: Check coverage runs-on: ubuntu-latest needs: pytest steps: - - name: Load coverage + - name: Fetch coverage uses: actions/download-artifact@v4 with: - name: 'coverage' + name: coverage - - name: Check Code Coverage + - name: Check coverage uses: yedpodtrzitko/coverage@main with: thresholdAll: 0.4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9d4e6ba4..03c158f8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,3 +1,4 @@ +--- name: Release on: @@ -19,15 +20,27 @@ jobs: suffix: _portable runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 with: python-version: '3.12' - cache: 'pip' - - run: pip install -Ur requirements.txt pyinstaller - - run: pyinstaller tagstudio.spec -- ${{ matrix.build-flag }} - - run: tar czfC dist/tagstudio_linux_x86_64${{ matrix.suffix }}.tar.gz dist tagstudio - - uses: actions/upload-artifact@v4 + cache: pip + + - name: Install Python dependencies + run: | + python -m pip install --upgrade uv + uv pip install --system .[pyinstaller] + + - name: Execute PyInstaller + run: | + pyinstaller tagstudio.spec -- ${{ matrix.build-flag }} + tar czfC dist/tagstudio_linux_x86_64${{ matrix.suffix }}.tar.gz dist tagstudio + + - name: Upload artifact + uses: actions/upload-artifact@v4 with: name: tagstudio_linux_x86_64${{ matrix.suffix }} path: dist/tagstudio_linux_x86_64${{ matrix.suffix }}.tar.gz @@ -41,20 +54,35 @@ jobs: arch: x86_64 - os-version: '14' arch: aarch64 + runs-on: macos-${{ matrix.os-version }} + env: - # even though we run on 12, target towards compatibility + # INFO: Even though we run on 13, target towards compatibility MACOSX_DEPLOYMENT_TARGET: '11.0' + steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 with: python-version: '3.12' - cache: 'pip' - - run: pip install -Ur requirements.txt pyinstaller - - run: pyinstaller tagstudio.spec - - run: tar czfC dist/tagstudio_macos_${{ matrix.arch }}.tar.gz dist TagStudio.app - - uses: actions/upload-artifact@v4 + cache: pip + + - name: Install Python dependencies + run: | + python -m pip install --upgrade uv + uv pip install --system .[pyinstaller] + + + - name: Execute PyInstaller + run: | + pyinstaller tagstudio.spec + tar czfC dist/tagstudio_macos_${{ matrix.arch }}.tar.gz dist TagStudio.app + - name: Upload artifact + uses: actions/upload-artifact@v4 with: name: tagstudio_macos_${{ matrix.arch }} path: dist/tagstudio_macos_${{ matrix.arch }}.tar.gz @@ -72,30 +100,51 @@ jobs: build-flag: --portable suffix: _portable file-end: .exe + runs-on: windows-2019 + steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 with: python-version: '3.12' - cache: 'pip' - - run: pip install -Ur requirements.txt pyinstaller - - run: PyInstaller tagstudio.spec -- ${{ matrix.build-flag }} - - run: Compress-Archive -Path dist/TagStudio${{ matrix.file-end }} -DestinationPath dist/tagstudio_windows_x86_64${{ matrix.suffix }}.zip - - uses: actions/upload-artifact@v4 + cache: pip + + - name: Install Python dependencies + run: | + python -m pip install --upgrade uv + uv pip install --system .[pyinstaller] + + - name: Execute PyInstaller + run: | + PyInstaller tagstudio.spec -- ${{ matrix.build-flag }} + Compress-Archive -Path dist/TagStudio${{ matrix.file-end }} -DestinationPath dist/tagstudio_windows_x86_64${{ matrix.suffix }}.zip + - name: Upload artifact + uses: actions/upload-artifact@v4 with: name: tagstudio_windows_x86_64${{ matrix.suffix }} path: dist/tagstudio_windows_x86_64${{ matrix.suffix }}.zip publish: needs: [linux, macos, windows] + runs-on: ubuntu-latest + permissions: contents: write + steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - - uses: softprops/action-gh-release@v2 + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Fetch artifacts + uses: actions/download-artifact@v4 + + - name: Publish release + uses: softprops/action-gh-release@v2 with: files: | tagstudio_linux_x86_64/* diff --git a/.github/workflows/ruff.yaml b/.github/workflows/ruff.yaml index 897dbdc5..e3dfbf2d 100644 --- a/.github/workflows/ruff.yaml +++ b/.github/workflows/ruff.yaml @@ -1,20 +1,31 @@ +--- name: Ruff -on: [ push, pull_request ] + +on: [push, pull_request] + jobs: ruff-format: + name: Run Ruff format runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: chartboost/ruff-action@v1 + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Execute Ruff format + uses: chartboost/ruff-action@v1 with: - version: 0.6.4 - args: 'format --check' + version: 0.8.1 + args: format --check ruff-check: + name: Run Ruff check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: chartboost/ruff-action@v1 + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Execute Ruff check + uses: chartboost/ruff-action@v1 with: - version: 0.6.4 - args: 'check' + version: 0.8.1 + args: check diff --git a/.gitignore b/.gitignore index 4dd46079..12aece25 100644 --- a/.gitignore +++ b/.gitignore @@ -265,3 +265,6 @@ TagStudio.ini .envrc .direnv .devenv + +result +result-* diff --git a/Build_MacOS_app.sh b/Build_MacOS_app.sh deleted file mode 100755 index eeed6b7c..00000000 --- a/Build_MacOS_app.sh +++ /dev/null @@ -1,69 +0,0 @@ -#! /usr/bin/env bash -# GETTING BASE DIR -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) - -# SETTING UP CONSTANTS -TAGSTUDIO_NAME="TagStudio" -TAGSTUDIO_DIR="$SCRIPT_DIR/tagstudio" -TAGSTUDIO_DIR_RESOURCES="$TAGSTUDIO_DIR/resources" -TAGSTUDIO_ICON="$TAGSTUDIO_DIR/resources/icon.ico" -TAGSTUDIO_SRC="$TAGSTUDIO_DIR/src" -TAGSTUDIO_MAIN="$TAGSTUDIO_DIR/tag_studio.py" -DIST_PATH="$SCRIPT_DIR/dist" -BUILD_PATH="$SCRIPT_DIR/build" -LOGS_PATH="$BUILD_PATH/logs" - -printf -- "šŸ Starting Script \n" - -# CREATE VENV AND INSTALL REQUIREMENTS -printf -- "šŸ Creating Python virtual env\n" -python3 -m venv .venv -source .venv/bin/activate - -if [ ! -d $LOGS_PATH ]; then - printf -- "šŸ“ Creating Logs folder\n" - mkdir -p $LOGS_PATH; -fi - -printf -- "šŸ’» Installing Requirements \n" -pip install -r requirements.txt > "$LOGS_PATH/pip.log" 2>&1 -pip install PyInstaller > "$LOGS_PATH/pip.log" 2>&1 - - -if [[ "$OSTYPE" == "darwin"* ]]; then - printf -- "šŸ MacOS Detected \n" - SYS_CMD="--windowed" - OS=0 -fi - -SECONDS=0 - -# CREATE COMMAND -printf -- "ā³ Building App \n" - -COMMAND=$( python -m PyInstaller \ - --name "$TAGSTUDIO_NAME" \ - --icon "$TAGSTUDIO_ICON" \ - --add-data "$TAGSTUDIO_DIR_RESOURCES:./resources" \ - --add-data "$TAGSTUDIO_SRC:./src" \ - --distpath "$DIST_PATH" \ - -p "$TAGSTUDIO_DIR" \ - --noconsole \ - --workpath "$BUILD_PATH" \ - -y "$SYS_CMD" "$TAGSTUDIO_MAIN" \ - > "$LOGS_PATH/pyinstaller.log" 2>&1 ) - -duration=$SECONDS - -if $COMMAND; then - printf -- "āœ… Build Successfull \n" - printf -- "āŒ› $((duration)) seconds of build\n" - if [[ "$OS" == 0 ]]; then - printf -- "šŸ“ Opening App folder \n" - open $DIST_PATH - fi -else - printf -- "āŒ Error Building the app\nPlease read the logs\navailable at build/logs\n" -fi - -printf -- "šŸ END OF TRANSMISSION" diff --git a/Build_win.bat b/Build_win.bat deleted file mode 100644 index 3e99fdcf..00000000 --- a/Build_win.bat +++ /dev/null @@ -1,31 +0,0 @@ -@echo off -set TAGSTUDIO_NAME=TagStudio -set TAGSTUDIO_DIR=tagstudio -set TAGSTUDIO_DIR_RESOURCES=%TAGSTUDIO_DIR%/resources -set TAGSTUDIO_ICON=%TAGSTUDIO_DIR%/resources/icon.ico -set TAGSTUDIO_SRC=%TAGSTUDIO_DIR%/src -set TAGSTUDIO_MAIN=%TAGSTUDIO_DIR%/tag_studio.py -set BUILD_MODE=--onedir - - -if "%1" == "--help" ( - echo run "%~nx0" for normal Build - echo run "%~nx0 --portable" for Build packaged into one file - goto end -) -if "%1" == "--portable" ( - echo Building portable executable... - set BUILD_MODE=--onefile - goto run -) -if not "%1" == "" ( - echo Invalid argument run "%~nx0 --help" for help - goto end -) -:run -echo Building executable... -set COMMAND=PyInstaller --name "%TAGSTUDIO_NAME%" --icon "%TAGSTUDIO_ICON%" --add-data "%TAGSTUDIO_DIR_RESOURCES%:./resources" --add-data "%TAGSTUDIO_SRC%:./src" -p "%TAGSTUDIO_DIR%" --console %BUILD_MODE% "%TAGSTUDIO_MAIN%" -y -call .venv\Scripts\activate.bat -%COMMAND% -deactivate -:end diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 93ced2e3..30964e85 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,100 +1,56 @@ # Contributing to TagStudio -_Last Updated: January 30th, 2025_ +_Last Updated: March 8th, 2025_ Thank you so much for showing interest in contributing to TagStudio! Here are a set of instructions and guidelines for contributing code or documentation to the project. This document will change over time, so make sure that your contributions still line up with the requirements here before submitting a pull request. ## Getting Started -- Check the [Feature Roadmap](/docs/updates/roadmap.md) page to see what priority features there are, the [FAQ](/README.md/#faq), as well as the open [Issues](https://github.com/TagStudioDev/TagStudio/issues) and [Pull Requests](https://github.com/TagStudioDev/TagStudio/pulls). +- Check the [Feature Roadmap](/docs/updates/roadmap.md) page to see what priority features there are, the [FAQ](/README.md/#faq), as well as the project's [Issues](https://github.com/TagStudioDev/TagStudio/issues) and [Pull Requests](https://github.com/TagStudioDev/TagStudio/pulls). - If you'd like to add a feature that isn't on the feature roadmap or doesn't have an open issue, **PLEASE create a feature request** issue for it discussing your intentions so any feedback or important information can be given by the team first. - We don't want you wasting time developing a feature or making a change that can't/won't be added for any reason ranging from pre-existing refactors to design philosophy differences. - **Please don't** create pull requests that consist of large refactors, _especially_ without discussing them with us first. These end up doing more harm than good for the project by continuously delaying progress and disrupting everyone else's work. -- If you wish to discuss TagStudio further, feel free to join the [Discord Server](https://discord.com/invite/hRNnVKhF2G) +- If you wish to discuss TagStudio further, feel free to join the [Discord Server](https://discord.com/invite/hRNnVKhF2G)! ### Contribution Checklist - I've read the [Feature Roadmap](/docs/updates/roadmap.md) page - I've read the [FAQ](/README.md/#faq), including the "[Features I Likely Won't Add/Pull](/README.md/#features-i-likely-wont-addpull)" section -- I've checked the open [Issues](https://github.com/TagStudioDev/TagStudio/issues) and [Pull Requests](https://github.com/TagStudioDev/TagStudio/pulls) +- I've checked the project's [Issues](https://github.com/TagStudioDev/TagStudio/issues) and [Pull Requests](https://github.com/TagStudioDev/TagStudio/pulls) - **I've created a new issue for my feature/fix _before_ starting work on it**, or have at least notified others in the relevant existing issue(s) of my intention to work on it - I've set up my development environment including Ruff, Mypy, and PyTest - I've read the [Code Guidelines](#code-guidelines) and/or [Documentation Guidelines](#documentation-guidelines) - **_I mean it, I've found or created an issue for my feature/fix!_** > [!NOTE] -> If the fix is small and self-explanatory (i.e. a typo), then it doesn't require an issue to be opened first. Issue tracking is supposed to make our lives easier, not harder. Please use your best judgement to minimize the amount of work involved for everyone involved. +> If the fix is small and self-explanatory (i.e. a typo), then it doesn't require an issue to be opened first. Issue tracking is supposed to make our lives easier, not harder. Please use your best judgement to minimize the amount of work for everyone involved. ## Creating a Development Environment -### Prerequisites +If you wish to develop for TagStudio, you'll need to create a development environment by installing the required dependencies. You have a number of options depending on your level of experience and familiarly with existing Python toolchains. -- [Python](https://www.python.org/downloads/) 3.12 -- [Ruff](https://github.com/astral-sh/ruff) (Included in `requirements-dev.txt`) -- [Mypy](https://github.com/python/mypy) (Included in `requirements-dev.txt`) -- [PyTest](https://docs.pytest.org) (Included in `requirements-dev.txt`) +If you know what you're doing and have developed for Python projects in the past, you can get started quickly with the "Brief Instructions" below. Otherwise, please see the full instructions on the documentation website for "[Creating a Development Environment](https://docs.tagstud.io/install/#creating-a-development-environment)". -### Creating a Python Virtual Environment +### Brief Instructions -If you wish to launch the source version of TagStudio outside of your IDE: +1. Have [Python 3.12](https://www.python.org/downloads/) and PIP installed. Also have [FFmpeg](https://ffmpeg.org/download.html) installed if you wish to have audio/video playback and thumbnails. +2. Clone the repository to the folder of your choosing: + ``` + git clone https://github.com/TagStudioDev/TagStudio.git + ``` +3. Use a dependency manager such as [uv](https://docs.astral.sh/uv/) or [Poetry 2.0](https://python-poetry.org/blog/category/releases/) to install the required dependencies, or alternatively create and activate a [virtual environment](https://docs.tagstud.io/install/#manual-installation) with `venv`. -> [!IMPORTANT] -> Depending on your system, Python may be called `python`, `py`, `python3`, or `py3`. These instructions use the alias `python3` for consistency. You can check to see which alias your system uses and if it's for the correct Python version by typing `python3 --version` (or whichever alias) into your terminal. +4. If using a virtual environment instead of a dependency manager, install an editable version of the program and development dependencies with the following PIP command: -> [!TIP] -> On Linux and macOS, you can launch the `tagstudio.sh` script to skip the following process, minus the `requirements-dev.txt` installation step. _Using the script is fine if you just want to launch the program from source._ + ``` + pip install -e .[dev] + ``` -1. Make sure you're using the correct Python version: - - If the output matches `Python 3.12.x` (where the x is any number) then you're using the correct Python version and can skip to step 2. Otherwise, you can install the correct Python version from the [Python](https://www.python.org/downloads/) website, or you can use a tool like [pyenv](https://github.com/pyenv/pyenv/) to install the correct version without changes to your system: - 1. Follow pyenv's [install instructions](https://github.com/pyenv/pyenv/?tab=readme-ov-file#installation) for your system. - 2. Install the appropriate Python version with pyenv by running `pyenv install 3.12` (This will **not** mess with your existing Python installation). - 3. Navigate to the repository root folder in your terminal and run `pyenv local 3.12`. - - You could alternatively use `pyenv shell 3.12` or `pyenv global 3.12` instead to set the Python version for the current terminal session or the entire system respectively, however using `local` is recommended. + Otherwise, modify the command above for use with your dependency manager of choice. For example if using uv, you may use this: -2. In the root repository directory, create a python virtual environment: - `python3 -m venv .venv` -3. Activate your environment: - -- Windows w/Powershell: `.venv\Scripts\Activate.ps1` -- Windows w/Command Prompt: `.venv\Scripts\activate.bat` -- Linux/macOS: `source .venv/bin/activate` - Depending on your system, the regular activation script *might* not work on alternative shells. In this case, refer to the table below for supported shells: - |Shell |Script | - |-------:|:------------------------| - |Bash/ZSH|`.venv/bin/activate` | - |Fish |`.venv/bin/activate.fish`| - |CSH/TCSH|`.venv/bin/activate.csh` | - |PWSH |`.venv/bin/activate.ps1` | - - -4. Install the required packages: - -- `pip install -r requirements.txt` -- If developing (includes Ruff and Mypy): `pip install -r requirements-dev.txt` - -_Learn more about setting up a virtual environment [here](https://docs.python.org/3/tutorial/venv.html)._ - -### Manually Launching (Outside of an IDE) - -If you encounter errors about the Python version, or seemingly vague script errors, [pyenv](https://github.com/pyenv/pyenv/) may solve your issue. See step 1 of [Creating a Python Virtual Environment](#creating-a-python-virtual-environment). - -- **Windows** (start_win.bat) - - - To launch TagStudio, launch the `start_win.bat` file. You can modify this .bat file or create a shortcut and add one or more additional arguments if desired. - -- **Linux/macOS** (TagStudio.sh) - - - Run the "TagStudio.sh" script and the program should launch! (Make sure that the script is marked as executable if on Linux). Note that launching from the script from outside of a terminal will not launch a terminal window with any debug or crash information. If you wish to see this information, just launch the shell script directly from your terminal with `./TagStudio.sh`. - - - **NixOS** (Nix Flake) - - Use the provided [Flake](https://nixos.wiki/wiki/Flakes) to create and enter a working environment by running `nix develop`. Then, run the program via `python3 tagstudio/tag_studio.py` from the root directory. - -> [!WARNING] -> Support for NixOS is still a work in progress. - -- **Any** (No Scripts) - - - Alternatively, with the virtual environment loaded, run the python file at `tagstudio\tag_studio.py` from your terminal. If you're in the project's root directory, simply run `python3 tagstudio/tag_studio.py`. + ``` + uv pip install -e .[dev] + ``` ## Workflow Checks @@ -125,16 +81,16 @@ Mypy is a static type checker for Python. It sure has a lot to say sometimes, bu #### Running Locally -- **First time only:** Move into the `/tagstudio` directory with `cd tagstudio` and run the following: +- **(First time only)** Run the following: - `mkdir -p .mypy_cache` - `mypy --install-types --non-interactive` -- Check code by moving into the `/tagstudio` directory with `cd tagstudio` _(if you aren't already inside)_ and running `mypy --config-file ../pyproject.toml .`. _(Don't forget the `.` at the end!)_ +- You can now check code by running `mypy --config-file pyproject.toml .` in the repository root. _(Don't forget the "." at the end!)_ Mypy is also available as a VS Code [extension](https://marketplace.visualstudio.com/items?itemName=matangover.mypy), PyCharm [plugin](https://plugins.jetbrains.com/plugin/11086-mypy), and [more](https://plugins.jetbrains.com/plugin/11086-mypy). ### PyTest -- Run all tests by moving into the `/tagstudio` directory with `cd tagstudio` and running `pytest tests/`. +- Run all tests by running `pytest tests/` in the repository root. ## Code Style @@ -173,7 +129,7 @@ Most of the style guidelines can be checked, fixed, and enforced via Ruff. Older > [!IMPORTANT] > Please do not force push if your PR is open for review! > -> Force pushing makes it impossible to discern which changes have already been reviewed and which haven't. This means a reviewer will then have to rereview all the already reviewed code, which is a lot of unnecessary work for reviewers. +> Force pushing makes it impossible to discern which changes have already been reviewed and which haven't. This means a reviewer will then have to re-review all the already reviewed code, which is a lot of unnecessary work for reviewers. > [!TIP] > If you're unsure where to stop the scope of your PR, ask yourself: _"If I broke this up, could any parts of it still be used by the project in the meantime?"_ @@ -182,7 +138,7 @@ Most of the style guidelines can be checked, fixed, and enforced via Ruff. Older - Final code must function on supported versions of Windows, macOS, and Linux: - Windows: 10, 11 - - macOS: 12.0+ + - macOS: 13.0+ - Linux: _Varies_ - Final code must **_NOT:_** - Contain superfluous or unnecessary logging statements diff --git a/README.md b/README.md index bbd4ae6f..7a52f92b 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ TagStudio is a photo & file organization application with an underlying tag-base ## Goals - To achieve a portable, private, extensible, open-format, and feature-rich system of organizing and rediscovering files. -- To provide powerful methods for organization, notably the concept of tag inheritance, or ā€œtaggable tagsā€ _(and in the near future, the combination of composition-based tags)._ +- To provide powerful methods for organization, notably the concept of tag inheritance, or "taggable tags" _(and in the near future, the combination of composition-based tags)._ - To create an implementation of such a system that is resilient against a user’s actions outside the program (modifying, moving, or renaming files) while also not burdening the user with mandatory sidecar files or requiring them to change their existing file structures and workflows. - To support a wide range of users spanning across different platforms, multi-user setups, and those with large (several terabyte) libraries. - To make the dang thing look nice, too. It’s 2025, not 1995. @@ -75,7 +75,7 @@ Translation hosting generously provided by [Weblate](https://weblate.org/en/). C - Add metadata to your library entries, including: - Name, Author, Artist (Single-Line Text Fields) - Description, Notes (Multiline Text Fields) -- Create rich tags composed of a name, color, a list of aliases, and a list of ā€œparent tagsā€ - these being tags in which these tags inherit values from. +- Create rich tags composed of a name, color, a list of aliases, and a list of "parent tags" - these being tags in which these tags inherit values from. - Copy and paste tags and fields across file entries - Automatically organize tags into groups based on parent tags marked as "categories" - Generate tags from your existing folder structure with the "Folders to Tags" macro (NOTE: these tags do NOT sync with folders after they are created) @@ -98,29 +98,21 @@ Translation hosting generously provided by [Weblate](https://weblate.org/en/). C ## Installation -To download TagStudio, visit the [Releases](https://github.com/TagStudioDev/TagStudio/releases) section of the GitHub repository and download the latest release for your system under the "Assets" section. TagStudio is available for **Windows**, **macOS** _(Apple Silicon & Intel)_, and **Linux**. Windows and Linux builds are also available in portable versions if you want a more self-contained executable to move around. +To download executable builds of TagStudio, visit the [Releases](https://github.com/TagStudioDev/TagStudio/releases) page of the GitHub repository and download the latest release for your system under the "Assets" section at the bottom of the release. -**We do not currently publish TagStudio to any package managers. Any TagStudio distributions outside of the GitHub releases page are _unofficial_ and not maintained by us.** Installation support will not be given to users installing from unofficial sources. Use these versions at your own risk. +TagStudio has builds for **Windows**, **macOS** _(Apple Silicon & Intel)_, and **Linux**. We also offer portable releases for Windows and Linux which are self-contained and easier to move around. -> [!IMPORTANT] -> On macOS, you may be met with a message saying _""TagStudio" can't be opened because Apple cannot check it for malicious software."_ If you encounter this, then you'll need to go to the "Settings" app, navigate to "Privacy & Security", and scroll down to a section that says _""TagStudio" was blocked from use because it is not from an identified developer."_ Click the "Open Anyway" button to allow TagStudio to run. You should only have to do this once after downloading the application. +For detailed instructions, installation help, and instructions for developing for TagStudio, please see the "[Installation](https://docs.tagstud.io/install/)" page on our documentation website. -> [!IMPORTANT] -> On Linux with non-Qt based Desktop Environments you may be unable to open TagStudio. You need to make sure that "xcb-cursor0" or "libxcb-cursor0" packages are installed. For more info check [Missing linux dependencies](https://github.com/TagStudioDev/TagStudio/discussions/182#discussioncomment-9452896) + +> [!CAUTION] +> **We do not currently publish TagStudio to any package managers. Any TagStudio distributions outside of the GitHub [Releases](https://github.com/TagStudioDev/TagStudio/releases) page are _unofficial_ and not maintained by us.** +> +> Installation support will not be given to users installing from unofficial sources. Use these versions at your own risk! ### Third-Party Dependencies -- For video thumbnails and playback, you'll also need [FFmpeg](https://ffmpeg.org/download.html) installed on your system. If you encounter any issues with this, please reference our [FFmpeg Help](/docs/help/ffmpeg.md) guide. - -### Optional Arguments - -Arguments available to pass to the program, either via the command line or a shortcut. - -> `--open ` / `-o ` -> Path to a TagStudio Library folder to open on start. -> -> `--config-file ` / `-c ` -> Path to the TagStudio config file to load. +For video thumbnails and playback, you'll also need [FFmpeg](https://ffmpeg.org/download.html) installed on your system. If you encounter any issues with this, please reference our [FFmpeg Help](/docs/help/ffmpeg.md) guide. ## Usage @@ -136,13 +128,13 @@ Libraries under 10,000 files automatically scan for new or modified files when o Access the "Add Tag" search box by either clicking on the "Add Tag" button at the bottom of the right sidebar, accessing the "Add Tags to Selected" option from the File menu, or by pressing Ctrl+Shift+T. -From here you can search for existing tags or create a new one if the one you're looking for doesn't exist. Click the ā€œ+ā€ button next to any tags you want to to the currently selected file entries. To quickly add the top result, press the Enter/Return key to add the the topmost tag and reset the tag search. Press Enter/Return once more to close the dialog box. By using this method, you can quickly add various tags in quick succession just by using the keyboard! +From here you can search for existing tags or create a new one if the one you're looking for doesn't exist. Click the "+" button next to any tags you want to to the currently selected file entries. To quickly add the top result, press the Enter/Return key to add the the topmost tag and reset the tag search. Press Enter/Return once more to close the dialog box. By using this method, you can quickly add various tags in quick succession just by using the keyboard! To remove a tag from a file entry, hover over the tag in the preview panel and click on the "-" icon that appears. ### Adding Metadata to File Entries -To add a metadata field to a file entry, start by clicking the ā€œAdd Fieldā€ button at the bottom of the preview panel. From the dropdown menu, select the type of metadata field you’d like to add to the entry +To add a metadata field to a file entry, start by clicking the "Add Field" button at the bottom of the preview panel. From the dropdown menu, select the type of metadata field you’d like to add to the entry ### Editing Metadata Fields @@ -172,11 +164,11 @@ You can manage your library of tags from opening the "Tag Manager" panel from Ed ### Editing Tags -To edit a tag, click on it inside the preview panel or right-click the tag and select ā€œEdit Tagā€ from the context menu. +To edit a tag, click on it inside the preview panel or right-click the tag and select "Edit Tag" from the context menu. ### Relinking Moved Files -Inevitably some of the files inside your library will be renamed, moved, or deleted. If a file has been renamed or moved, TagStudio will display the thumbnail as a red broken chain link. To relink moved files or delete these entries, select the "Manage Unlinked Entries" option under the Tools menu. Click the "Refresh" button to scan your library for unlinked entries. Once complete, you can attempt to ā€œSearch & Relinkā€ any unlinked file entries to their respective files, or ā€œDelete Unlinked Entriesā€ in the event the original files have been deleted and you no longer wish to keep their entries inside your library. +Inevitably some of the files inside your library will be renamed, moved, or deleted. If a file has been renamed or moved, TagStudio will display the thumbnail as a red broken chain link. To relink moved files or delete these entries, select the "Manage Unlinked Entries" option under the Tools menu. Click the "Refresh" button to scan your library for unlinked entries. Once complete, you can attempt to "Search & Relink" any unlinked file entries to their respective files, or "Delete Unlinked Entries" in the event the original files have been deleted and you no longer wish to keep their entries inside your library. > [!WARNING] > There is currently no method to relink entries to files that have been renamed - only moved or deleted. This is a high priority for future releases. @@ -194,7 +186,7 @@ These features were present in pre-public versions of TagStudio (9.0 and below) #### Fix Duplicate Files -Load in a .dupeguru file generated by [dupeGuru](https://github.com/arsenetar/dupeguru/) and mirror metadata across entries marked as duplicates. After mirroring, return to dupeGuru to manage deletion of the duplicate files. After deletion, use the ā€œFix Unlinked Entriesā€ feature in TagStudio to delete the duplicate set of entries for the now-deleted files +Load in a .dupeguru file generated by [dupeGuru](https://github.com/arsenetar/dupeguru/) and mirror metadata across entries marked as duplicates. After mirroring, return to dupeGuru to manage deletion of the duplicate files. After deletion, use the "Fix Unlinked Entries" feature in TagStudio to delete the duplicate set of entries for the now-deleted files > [!CAUTION] > While this feature is functional, it’s a pretty roundabout process and can be streamlined in the future. diff --git a/TagStudio.sh b/TagStudio.sh deleted file mode 100755 index 3c515614..00000000 --- a/TagStudio.sh +++ /dev/null @@ -1,7 +0,0 @@ -#! /usr/bin/env bash -set -e -cd "$(dirname "$0")" -! [ -d .venv ] && python3 -m venv .venv -source .venv/bin/activate -pip install -r requirements.txt -python tagstudio/tag_studio.py diff --git a/docs/help/ffmpeg.md b/docs/help/ffmpeg.md index f99c8c14..45f4ff45 100644 --- a/docs/help/ffmpeg.md +++ b/docs/help/ffmpeg.md @@ -43,7 +43,7 @@ FFmpeg is available under the macOS section of the [FFmpeg website](https://www. ### Package Managers -FFmpeg may be installed by default on some Linux distributions, but if not, it is available via your distro's package manager of choice: +FFmpeg may be installed by default on some Linux distributions, but if not, it is available via your distribution package manager of choice: 1. Debian/Ubuntu (`sudo apt install ffmpeg`) 2. Fedora (`sudo dnf install ffmpeg-free`) diff --git a/docs/index.md b/docs/index.md index 4d46950a..b3df7805 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,21 +4,21 @@ title: Home # Welcome to the TagStudio Documentation! -![TagStudio Alpha](assets/github_header.png) +![TagStudio Alpha](./assets/github_header.png) TagStudio is a photo & file organization application with an underlying tag-based system that focuses on giving freedom and flexibility to the user. No proprietary programs or formats, no sea of sidecar files, and no complete upheaval of your filesystem structure.
- ![TagStudio screenshot](assets/screenshot.png) - + ![TagStudio screenshot](./assets/screenshot.png) +
TagStudio Alpha v9.5.0 running on macOS Sequoia.
## Feature Roadmap -The [Feature Roadmap](updates/roadmap.md) lists all of the planned core features for TagStudio to be considered "feature complete" along with estimated release milestones. The development and testing of these features takes priority over all other requested or submitted features unless they are later added to this roadmap. This helps ensure that TagStudio eventually sees a full release and becomes more usable by more people more quickly. +The [Feature Roadmap](./updates/roadmap.md) lists all of the planned core features for TagStudio to be considered "feature complete" along with estimated release milestones. The development and testing of these features takes priority over all other requested or submitted features unless they are later added to this roadmap. This helps ensure that TagStudio eventually sees a full release and becomes more usable by more people more quickly. ## Current Features @@ -32,8 +32,8 @@ The [Feature Roadmap](updates/roadmap.md) lists all of the planned core features - Add custom powerful [tags](./library/tag.md) to your library entries - Add [metadata fields](./library/field.md) to your library entries, including: - Name, Author, Artist (Single-Line Text Fields) - - Description, Notes (Multiline Text Fields) -- Create rich tags composed of a name, color, a list of aliases, and a list of ā€œparent tagsā€ - these being tags in which these tags inherit values from. + - Description, Notes (Multi-Line Text Fields) +- Create rich tags composed of a name, color, a list of aliases, and a list of "parent tags" - these being tags in which these tags inherit values from. - Copy and paste tags and fields across file entries - Automatically organize tags into groups based on parent tags marked as "categories" - Generate tags from your existing folder structure with the "Folders to Tags" macro (NOTE: these tags do NOT sync with folders after they are created) @@ -41,7 +41,7 @@ The [Feature Roadmap](updates/roadmap.md) lists all of the planned core features ### Search - [Search](./library/library_search.md) for file entries based on tags, file path (`path:`), file types (`filetype:`), and even media types! (`mediatype:`) -- Use and combine boolean operators (`AND`, `OR`, `NOT`) along with parentheses groups, quotation escaping, and underscore substitution to create detailed search queries +- Use and combine Boolean operators (`AND`, `OR`, `NOT`) along with parentheses groups, quotation escaping, and underscore substitution to create detailed search queries - Use special search conditions (`special:untagged`) to find file entries without tags or fields, respectively ### File Entries diff --git a/docs/install.md b/docs/install.md index ec110014..0ba721f4 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,27 +1,373 @@ # Installation -To download TagStudio, visit the [Releases](https://github.com/TagStudioDev/TagStudio/releases) section of the GitHub repository and download the latest release for your system under the "Assets" section. TagStudio is available for **Windows**, **macOS** _(Apple Silicon & Intel)_, and **Linux**. Windows and Linux builds are also available in portable versions if you want a more self-contained executable to move around. +## Releases -**We do not currently publish TagStudio to any package managers. Any TagStudio distributions outside of the GitHub releases page are _unofficial_ and not maintained by us.** Installation support will not be given to users installing from unofficial sources. Use these versions at your own risk. +TagStudio provides executable [releases](https://github.com/TagStudioDev/TagStudio/releases) as well as full access to its [source code](https://github.com/TagStudioDev/TagStudio) under the [GPLv3](https://github.com/TagStudioDev/TagStudio/blob/main/LICENSE) license. + +To download executable builds of TagStudio, visit the [Releases](https://github.com/TagStudioDev/TagStudio/releases) page of the GitHub repository and download the latest release for your system under the "Assets" section at the bottom of the release. + +TagStudio has builds for **Windows**, **macOS** _(Apple Silicon & Intel)_, and **Linux**. We also offer portable releases for Windows and Linux which are self-contained and easier to move around. !!! info "For macOS Users" - On macOS, you may be met with a message saying _""TagStudio" can't be opened because Apple cannot check it for malicious software."_ If you encounter this, then you'll need to go to the "Settings" app, navigate to "Privacy & Security", and scroll down to a section that says _""TagStudio" was blocked from use because it is not from an identified developer."_ Click the "Open Anyway" button to allow TagStudio to run. You should only have to do this once after downloading the application. + On macOS, you may be met with a message saying "**"TagStudio" can't be opened because Apple cannot check it for malicious software.**" If you encounter this, then you'll need to go to the "Settings" app, navigate to "Privacy & Security", and scroll down to a section that says "**"TagStudio" was blocked from use because it is not from an identified developer.**" Click the "Open Anyway" button to allow TagStudio to run. You should only have to do this once after downloading the application. + +### Package Managers -!!! info "For Linux Users" - On Linux with non-Qt based Desktop Environments you may be unable to open TagStudio. You need to make sure that "xcb-cursor0" or "libxcb-cursor0" packages are installed. For more info check [Missing linux dependencies](https://github.com/TagStudioDev/TagStudio/discussions/182#discussioncomment-9452896) +!!! danger "Unofficial Releases" + **We do not currently publish TagStudio to _remote_ package repositories. Any TagStudio distributions outside of the [GitHub repository](https://github.com/TagStudioDev/TagStudio) are _unofficial_ and not maintained by us!** + + Installation support will not be given to users installing from unofficial sources. Use these versions at your own risk! + +### Installing with PIP + +TagStudio is installable via [PIP](https://pip.pypa.io/). Note that since we don't currently distribute on PyPI, the repository needs to be cloned and installed locally. Make sure you have Python 3.12 and PIP installed if you choose to install using this method. + +The repository can be cloned/downloaded via `git` in your terminal, or by downloading the zip file from the "Code" button on the [repository page](https://github.com/TagStudioDev/TagStudio). + +```sh +git clone https://github.com/TagStudioDev/TagStudio.git +``` + +Once cloned or downloaded, you can install TagStudio with the following PIP command: + +```sh +pip install . +``` + + +!!! note "Developer Dependencies" + If you wish to create an editable install with the additional dependencies required for developing TagStudio, use this modified PIP command instead: + ```sh + pip install -e .[dev] + ``` + _See more under "[Creating a Development Environment](#creating-a-development-environment)"_ + +TagStudio can now be launched via the `tagstudio` command in your terminal. + +### Linux + +Some external dependencies are required for TagStudio to execute. Below is a table of known packages that will be necessary. + + +| Package | Reason | +|--------------- | --------------- | +| [dbus](https://repology.org/project/dbus) | required for Qt; opening desktop applications | +| [ffmpeg](https://repology.org/project/ffmpeg) | audio/video playback | +| libstdc++ | required for Qt | +| [libva](https://repology.org/project/libva) | hardware rendering with [VAAPI](https://www.freedesktop.org/wiki/Software/vaapi) | +| [libvdpau](https://repology.org/project/libvdpau) | hardware rendering with [VDPAU](https://www.freedesktop.org/wiki/Software/VDPAU) | +| [libx11](https://repology.org/project/libx11) | required for Qt | +| libxcb-cursor OR [xcb-util-cursor](https://repology.org/project/xcb-util-cursor) | required for Qt | +| [libxkbcommon](https://repology.org/project/libxkbcommon) | required for Qt | +| [libxrandr](https://repology.org/project/libxrandr) | hardware rendering | +| [pipewire](https://repology.org/project/pipewire) | PipeWire audio support | +| [qt](https://repology.org/project/qt) | required | +| [qt-multimedia](https://repology.org/project/qt) | required | +| [qt-wayland](https://repology.org/project/qt) | Wayland support | + +### Nix(OS) + +For [Nix(OS)](https://nixos.org/), the TagStudio repository includes a [flake](https://wiki.nixos.org/wiki/Flakes) that provides some outputs such as a development shell and package. + +Two packages are provided: `tagstudio` and `tagstudio-jxl`. The distinction was made because `tagstudio-jxl` has an extra compilation step for [JPEG-XL](https://jpeg.org/jpegxl) image support. To give either of them a test run, you can execute `nix run github:TagStudioDev/TagStudio#tagstudio`. If you are in a cloned repository and wish to run a package with the context of the repository, you can simply use `nix run` with no arguments. + +`nix build` can be used in place of `nix run` if you only want to build. **The packages will only build if tests pass.** + + +!!! info "Nix Support" + Support for Nix is handled on a best-effort basis by one of our maintainers. Issues related to Nix may be slower to resolve, and could require further details. + +Want to add TagStudio into your configuration? + +This can be done by first adding the flake input into your `flake.nix`: + +```nix title="flake.nix" +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + tagstudio = { + url = "github:TagStudioDev/TagStudio"; + inputs.nixpkgs.follows = "nixpkgs"; # Use the same package set as your flake. + }; + }; +} +``` + +Then, make sure you add the `inputs` context to your configuration: + + +=== "NixOS with Home Manager" + ```nix title="flake.nix" + { + outputs = + inputs@{ home-manager, nixpkgs, ... }: + { + nixosConfigurations.HOSTNAME = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + + specialArgs = { inherit inputs; }; + modules = [ + ./configuration.nix + + home-manager.nixosModules.home-manager + { + home-manager = { + useGlobalPkgs = true; + useUserPackages = true; + + extraSpecialArgs = { inherit inputs; }; + users.USER.imports = [ + ./home.nix + ]; + }; + } + ]; + }; + }; + } + ``` + + + +=== "NixOS" + ```nix title="flake.nix" + { + outputs = + inputs@{ nixpkgs, ... }: + { + nixosConfigurations.HOSTNAME = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + + specialArgs = { inherit inputs; }; + modules = [ + ./configuration.nix + ]; + }; + }; + } + ``` +=== "Home Manager (standalone)" + ```nix title="flake.nix" + { + outputs = + inputs@{ home-manager, nixpkgs, ... }: + let + pkgs = import nixpkgs { + system = "x86_64-linux"; + }; + in + { + homeConfigurations.USER = home-manager.lib.homeManagerConfiguration { + inherit pkgs; + + extraSpecialArgs = { inherit inputs; }; + modules = [ + ./home.nix + ]; + }; + }; + } + ``` + + +Finally, `inputs` can be used in a module to add the package to your packages list: + + +=== "Home Manager module" + ```nix title="home.nix" + { inputs, pkgs, ... }: + + { + home.packages = [ + inputs.tagstudio.packages.${pkgs.stdenv.hostPlatform.system}.tagstudio + ]; + } + ``` + + + +=== "NixOS module" + ```nix title="configuration.nix" + { inputs, pkgs, ... }: + + { + environment.systemPackages = [ + inputs.tagstudio.packages.${pkgs.stdenv.hostPlatform.system}.tagstudio + ]; + } + ``` + + +Don't forget to rebuild! + +## Creating a Development Environment + +If you wish to develop for TagStudio, you'll need to create a development environment by installing the required dependencies. You have a number of options depending on your level of experience and familiarly with existing Python toolchains. + + +!!! tip "Contributing" + If you wish to contribute to TagStudio's development, please read our [CONTRIBUTING.md](https://github.com/TagStudioDev/TagStudio/blob/main/CONTRIBUTING.md)! + +### Install Python + +Python [3.12](https://www.python.org/downloads/) is required to develop for TagStudio. Any version matching "Python 3.12.x" should work, with "x" being any number. Alternatively you can use a tool such as [pyenv](https://github.com/pyenv/pyenv/) to install this version of Python without affecting any existing Python installations on your system. + + +!!! info "Python Aliases" + Depending on your system, Python may be called `python`, `py`, `python3`, or `py3`. These instructions use the alias `python` for consistency. + +If you already have Python installed on your system, you can check the version by running the following command: + +```sh +python --version +``` + +#### Installing with pyenv + +If you choose to install Python using pyenv, please refer to the following instructions: + +1. Follow pyenv's [install instructions](https://github.com/pyenv/pyenv/?tab=readme-ov-file#installation) for your system. +2. Install the appropriate Python version with pyenv by running `pyenv install 3.12` (This will **not** mess with your existing Python installation). +3. Navigate to the repository root folder in your terminal and run `pyenv local 3.12`. You could alternatively use `pyenv shell 3.12` or `pyenv global 3.12` instead to set the Python version for the current terminal session or the entire system respectively, however using `local` is recommended. + +### Installing Dependencies + +To install the required dependencies, you can use a dependency manager such as [uv](https://docs.astral.sh/uv) or [Poetry 2.0](https://python-poetry.org). Alternatively you can create a virtual environment and manually install the dependencies yourself. + +#### Installing with uv + +If using [uv](https://docs.astral.sh/uv), you can install the dependencies for TagStudio with the following command: + +```sh +uv pip install -e .[dev] +``` + +#### Installing with Poetry + +If using [Poetry](https://python-poetry.org), you can install the dependencies for TagStudio with the following command: + +```sh +poetry install --with dev +``` + +#### Installing with Nix + +If using [Nix](https://nixos.org/), there is a development environment already provided in the [flake](https://wiki.nixos.org/wiki/Flakes) that is accessible with the following command: + +```sh +nix develop +``` + +You can automatically enter this development shell, and keep your user shell, with a tool like [direnv](https://direnv.net/). A reference `.envrc` is provided in the repository; to use it: + +```sh +ln -s .envrc.recommended .envrc +``` + +You will have to allow usage of it. + + +!!! warning "`.envrc` Security" + These files are generally a good idea to check, as they execute commands on directory load. direnv has a security framework to only run `.envrc` files you have allowed, and does keep track on if it has changed. So, with that being said, the file may need to be allowed again if modifications are made. + +```sh +cat .envrc # You are checking them, right? +direnv allow +``` + +#### Manual Installation + +If you choose to manually set up a virtual environment and install dependencies instead of using a dependency manager, please refer to the following instructions: + + +!!! tip "Virtual Environments" + Learn more about setting up a virtual environment with Python's [official tutorial](https://docs.python.org/3/tutorial/venv.html). + +1. In the root repository directory, create a python virtual environment: + + ```sh + python -m venv .venv + ``` + +2. Activate your environment: + + - Windows w/Powershell: `.venv\Scripts\Activate.ps1` + - Windows w/Command Prompt: `.venv\Scripts\activate.bat` + - Linux/macOS: `source .venv/bin/activate` + + + !!! info "Supported Shells" + Depending on your system, the regular activation script _might_ not work on alternative shells. In this case, refer to the table below for supported shells: + + | Shell | Script | + | ---------: | :------------------------ | + | Bash/ZSH | `.venv/bin/activate` | + | Fish | `.venv/bin/activate.fish` | + | CSH/TCSH | `.venv/bin/activate.csh` | + | PowerShell | `.venv/bin/activate.ps1` | + +3. Use the following PIP command to create an editable installation and install the required development dependencies: + + ```sh + pip install -e .[dev] + ``` + +### Launching + +The entry point for TagStudio is `src/tagstudio/main.py`. You can target this file from your IDE to run or connect a debug session. The example(s) below show off example launch scripts for different IDEs. Here you can also take advantage of [launch arguments](#launch-arguments) to pass your own test [libraries](./library/index.md) to use while developing. + + +=== "VS Code" + ```json title=".vscode/launch.json" + { + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "TagStudio", + "type": "python", + "request": "launch", + "program": "${workspaceRoot}/src/tagstudio/main.py", + "console": "integratedTerminal", + "justMyCode": true, + "args": ["-o", "~/Documents/Example"] + } + ] + } + ``` + +## Building + +To build your own executables of TagStudio, first follow the steps in "[Installing with PIP](#installing-with-pip)" including the developer dependencies step. Once that's complete, run the following PyInstaller command: + +``` +pyinstaller tagstudio.spec +``` + +If you're on Windows or Linux and wish to build a portable executable, then pass the following flag: + +``` +pyinstaller tagstudio.spec -- --portable +``` + +The resulting executable file(s) will be located in a new folder named "dist". ## Third-Party Dependencies -- For video thumbnails and playback, you'll also need [FFmpeg](https://ffmpeg.org/download.html) installed on your system. If you encounter any issues with this, please reference our [FFmpeg Help](./help/ffmpeg.md) guide. +For audio/video thumbnails and playback you'll also need [FFmpeg](https://ffmpeg.org/download.html) installed on your system. If you encounter any issues with this, please reference our [FFmpeg Help](./help/ffmpeg.md) guide. -## Optional Arguments +You can check to see if FFmpeg and FFprobe are correctly located by launching TagStudio and going to "About TagStudio" in the menu bar. -Optional arguments to pass to the program: +## Launch Arguments -`--open ` / `-o ` -: Path to a TagStudio Library folder to open on start. +There are a handful of launch arguments you can pass to TagStudio via the command line or a desktop shortcut. -`--config-file ` / `-c ` -: Path to the TagStudio config file to load. +| Argument | Short | Description | +| ---------------------- | ----- | ---------------------------------------------------- | +| `--open ` | `-o` | Path to a TagStudio Library folder to open on start. | +| `--config-file ` | `-c` | Path to the TagStudio config file to load. | diff --git a/docs/library/entry.md b/docs/library/entry.md index d1501872..2a73c006 100644 --- a/docs/library/entry.md +++ b/docs/library/entry.md @@ -1,6 +1,6 @@ # File Entries -File entries are the individual representations of your files inside a TagStudio [library](index.md). Each one corresponds one-to-one to a file on disk, and tracks all of the additional [tags](tag.md) and metadata that you attach to it inside TagStudio. +File entries are the individual representations of your files inside a TagStudio [library](./index.md). Each one corresponds one-to-one to a file on disk, and tracks all of the additional [tags](tag.md) and metadata that you attach to it inside TagStudio. ## Storage @@ -14,7 +14,7 @@ File entries appear as file previews both inside the thumbnail grid. The preview If the file that an entry is referencing has been moved, renamed, or deleted on disk, then TagStudio will display a red chain-link icon for the thumbnail image. Certain uncached stats such as the file size and image dimensions will also be unavailable to see in the preview panel when a file becomes unlinked. -To fix file entries that have become unlinked, select the "Fix Unlinked Entries" option from the Tools menu. From there, refresh the unlinked entry count and choose whether to search and relink you files, and/or delete the file entires from your library. This will NOT delete or modify any files on disk. +To fix file entries that have become unlinked, select the "Fix Unlinked Entries" option from the Tools menu. From there, refresh the unlinked entry count and choose whether to search and relink you files, and/or delete the file entries from your library. This will NOT delete or modify any files on disk. ## Internal Structure diff --git a/docs/library/entry_groups.md b/docs/library/entry_groups.md index a9dcb459..a25ae99f 100644 --- a/docs/library/entry_groups.md +++ b/docs/library/entry_groups.md @@ -5,4 +5,4 @@ tags: # Entry Groups -Entries can be grouped via tags marked as ā€œgroupsā€ which when applied to different entries will signal TagStudio to treat those entries as a single group inside of searches and browsing. +Entries can be grouped via tags marked as "groups" which when applied to different entries will signal TagStudio to treat those entries as a single group inside of searches and browsing. diff --git a/docs/library/field.md b/docs/library/field.md index 615bdb94..73e3cafd 100644 --- a/docs/library/field.md +++ b/docs/library/field.md @@ -1,6 +1,6 @@ # Fields -Fields are additional types of metadata that you can attach to [file entries](entry.md). Like [tags](tag.md), fields are not stored inside files themselves nor in sidecar files, but rather inside the respective TagStudio [library](index.md) save file. +Fields are additional types of metadata that you can attach to [file entries](./entry.md). Like [tags](./tag.md), fields are not stored inside files themselves nor in sidecar files, but rather inside the respective TagStudio [library](index.md) save file. ## Field Types diff --git a/docs/library/index.md b/docs/library/index.md index 7157aa2a..cbc55030 100644 --- a/docs/library/index.md +++ b/docs/library/index.md @@ -1,5 +1,5 @@ # Library -The library is how TagStudio represents your chosen directory, with every file inside being represented by a [file entry](entry.md). You can have as many or few libraries as you wish, since each libraries' data is stored within a `.TagStudio` folder at its root. From there the library save file itself is stored as `ts_library.sqlite`, with TagStudio versions 9.4 and below using a the legacy `ts_library.json` format. +The library is how TagStudio represents your chosen directory, with every file inside being represented by a [file entry](./entry.md). You can have as many or few libraries as you wish, since each libraries' data is stored within a `.TagStudio` folder at its root. From there the library save file itself is stored as `ts_library.sqlite`, with TagStudio versions 9.4 and below using a the legacy `ts_library.json` format. -Note that this means [tags](tag.md) you create only exist _per-library_. +Note that this means [tags](./tag.md) you create only exist _per-library_. diff --git a/docs/library/library_search.md b/docs/library/library_search.md index 4c6774cb..d71f7e2d 100644 --- a/docs/library/library_search.md +++ b/docs/library/library_search.md @@ -4,11 +4,11 @@ TagStudio provides various methods to search your library, ranging from TagStudi ## Boolean Operators -TagStudio allows you to use common [boolean search](https://en.wikipedia.org/wiki/Full-text_search#Boolean_queries) operators when searching your library, along with [grouping](#grouping-and-nesting), [nesting](#grouping-and-nesting), and [character escaping](#escaping-characters). Note that you may need to use grouping in order to get the desired results you're looking for. +TagStudio allows you to use common [Boolean search](https://en.wikipedia.org/wiki/Full-text_search#Boolean_queries) operators when searching your library, along with [grouping](#grouping-and-nesting), [nesting](#grouping-and-nesting), and [character escaping](#escaping-characters). Note that you may need to use grouping in order to get the desired results you're looking for. ### AND -The `AND` operator will only return results that match **both** sides of the operator. `AND` is used implicitly when no boolean operators are given. To use the `AND` operator explicitly, simply type "and" (case insensitive) in-between items of your search. +The `AND` operator will only return results that match **both** sides of the operator. `AND` is used implicitly when no Boolean operators are given. To use the `AND` operator explicitly, simply type "and" (case insensitive) in-between items of your search. !!! example @@ -74,7 +74,7 @@ TagStudio uses a "[smartcase](https://neovim.io/doc/user/options.html#'smartcase #### Glob Syntax -Optionally, you may use [glob]() syntax to search filepaths. +Optionally, you may use [glob](https://en.wikipedia.org/wiki/Glob_(programming)) syntax to search filepaths. #### Examples diff --git a/docs/library/tag.md b/docs/library/tag.md index f1a9d831..fa2a19ad 100644 --- a/docs/library/tag.md +++ b/docs/library/tag.md @@ -52,7 +52,7 @@ In a system where tags have no relationships, you're required to add as many tag #### Intuition via Substitution -Now when searching for for images that have `Dreamworks` and `Character`, any images or files originally just tagged with `Shrek` will appear as you would expect. A little bit of tag setup goes a long way not only saving so much time during tagging, but also to ensure an intuitive way to search your files! +Now when searching for images that have `Dreamworks` and `Character`, any images or files originally just tagged with `Shrek` will appear as you would expect. A little bit of tag setup goes a long way not only saving so much time during tagging, but also to ensure an intuitive way to search your files! #### Rediscovery via Linking diff --git a/docs/library/tag_overrides.md b/docs/library/tag_overrides.md index d6225f75..7206593c 100644 --- a/docs/library/tag_overrides.md +++ b/docs/library/tag_overrides.md @@ -5,7 +5,7 @@ tags: # Tag Overrides -Tag overrides are the ability to add or remove [parent tags](tag.md#parent-tags) from a [tag](tag.md) on a per- [entry](entry.md) basis. +Tag overrides are the ability to add or remove [parent tags](./tag.md#parent-tags) from a [tag](./tag.md) on a per-[entry](./entry.md) basis. ## Examples diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 00000000..a0526aa9 --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,3 @@ +th, td { + padding: 0.5em 1em 0.5em 1em !important; +} diff --git a/docs/updates/roadmap.md b/docs/updates/roadmap.md index d34e936d..17037374 100644 --- a/docs/updates/roadmap.md +++ b/docs/updates/roadmap.md @@ -1,6 +1,6 @@ # Feature Roadmap -This checklist details the current and remaining features required at a minimum for TagStudio to be considered ā€œFeature Completeā€. This list is _not_ a definitive list for additional feature requests and PRs as they come in, but rather an outline of my personal core feature set intended for TagStudio. +This checklist details the current and remaining features required at a minimum for TagStudio to be considered "Feature Complete". This list is _not_ a definitive list for additional feature requests and PRs as they come in, but rather an outline of my personal core feature set intended for TagStudio. ## Priorities @@ -39,7 +39,7 @@ These version milestones are rough estimations for when the previous core featur - [x] Boolean operators [HIGH] - [x] Filename search [HIGH] -- [x] Filetype search [HIGH] +- [x] File type search [HIGH] - [x] Search by extension (e.g. ".jpg", ".png") [HIGH] - [x] Optional consolidation of extension synonyms (i.e. ".jpg" can equal ".jpeg") [LOW] - [x] Search by media type (e.g. "image", "video", "document") [MEDIUM] diff --git a/docs/updates/schema_changes.md b/docs/updates/schema_changes.md index aeed0983..97eee3dd 100644 --- a/docs/updates/schema_changes.md +++ b/docs/updates/schema_changes.md @@ -43,4 +43,4 @@ Migration from the legacy JSON format is provided via a walkthrough when opening - Adds the `color_border` column to `tag_colors` table. Used for instructing the [secondary color](../library/tag_color.md#secondary-color) to apply to a tag's border as a new optional behavior. - Adds three new default colors: "Burgundy (TagStudio Shades)", "Dark Teal (TagStudio Shades)", and "Dark Lavender (TagStudio Shades)". -- Updates Neon colors to use the the new `color_border` property. +- Updates Neon colors to use the new `color_border` property. diff --git a/docs/usage.md b/docs/usage.md index e83f942c..04a2bb1d 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -12,13 +12,13 @@ Libraries under 10,000 files automatically scan for new or modified files when o Access the "Add Tag" search box by either clicking on the "Add Tag" button at the bottom of the right sidebar, accessing the "Add Tags to Selected" option from the File menu, or by pressing Ctrl+Shift+T. -From here you can search for existing tags or create a new one if the one you're looking for doesn't exist. Click the ā€œ+ā€ button next to any tags you want to to the currently selected file entries. To quickly add the top result, press the Enter/Return key to add the the topmost tag and reset the tag search. Press Enter/Return once more to close the dialog box. By using this method, you can quickly add various tags in quick succession just by using the keyboard! +From here you can search for existing tags or create a new one if the one you're looking for doesn't exist. Click the "+" button next to any tags you want to the currently selected file entries. To quickly add the top result, press the Enter/Return key to add the the topmost tag and reset the tag search. Press Enter/Return once more to close the dialog box. By using this method, you can quickly add various tags in quick succession just by using the keyboard! To remove a tag from a file entry, hover over the tag in the preview panel and click on the "-" icon that appears. ## Adding Metadata to File Entries -To add a metadata field to a file entry, start by clicking the ā€œAdd Fieldā€ button at the bottom of the preview panel. From the dropdown menu, select the type of metadata field you’d like to add to the entry +To add a metadata field to a file entry, start by clicking the "Add Field" button at the bottom of the preview panel. From the dropdown menu, select the type of metadata field you’d like to add to the entry ## Editing Metadata Fields @@ -33,7 +33,7 @@ Create a new tag by accessing the "New Tag" option from the Edit menu or by pres - The tag **name** is the base name of the tag. **_This does NOT have to be unique!_** - The tag **shorthand** is a special type of alias that displays in situations where screen space is more valuable, notably with name disambiguation. - **Aliases** are alternate names for a tag. These let you search for terms other than the exact tag name in order to find the tag again. -- **Parent Tags** are tags in which this this tag can substitute for in searches. In other words, tags under this section are parents of this tag. +- **Parent Tags** are tags in which this tag can substitute for in searches. In other words, tags under this section are parents of this tag. - Parent tags with the disambiguation check next to them will be used to help disambiguate tag names that may not be unique. - For example: If you had a tag for "Freddy Fazbear", you might add "Five Nights at Freddy's" as one of the parent tags. If the disambiguation box is checked next to "Five Nights at Freddy's" parent tag, then the tag "Freddy Fazbear" will display as "Freddy Fazbear (Five Nights at Freddy's)". Furthermore, if the "Five Nights at Freddy's" tag has a shorthand like "FNAF", then the "Freddy Fazbear" tag will display as "Freddy Fazbear (FNAF)". - The **color** option lets you select an optional color palette to use for your tag. @@ -45,11 +45,11 @@ You can manage your library of tags from opening the "Tag Manager" panel from Ed ## Editing Tags -To edit a tag, click on it inside the preview panel or right-click the tag and select ā€œEdit Tagā€ from the context menu. +To edit a tag, click on it inside the preview panel or right-click the tag and select "Edit Tag" from the context menu. ## Relinking Moved Files -Inevitably some of the files inside your library will be renamed, moved, or deleted. If a file has been renamed or moved, TagStudio will display the thumbnail as a red broken chain link. To relink moved files or delete these entries, select the "Manage Unlinked Entries" option under the Tools menu. Click the "Refresh" button to scan your library for unlinked entries. Once complete, you can attempt to ā€œSearch & Relinkā€ any unlinked file entries to their respective files, or ā€œDelete Unlinked Entriesā€ in the event the original files have been deleted and you no longer wish to keep their entries inside your library. +Inevitably some of the files inside your library will be renamed, moved, or deleted. If a file has been renamed or moved, TagStudio will display the thumbnail as a red broken chain link. To relink moved files or delete these entries, select the "Manage Unlinked Entries" option under the Tools menu. Click the "Refresh" button to scan your library for unlinked entries. Once complete, you can attempt to "Search & Relink" any unlinked file entries to their respective files, or "Delete Unlinked Entries" in the event the original files have been deleted and you no longer wish to keep their entries inside your library. !!! warning diff --git a/flake.lock b/flake.lock index c26dd0ec..1750fba8 100644 --- a/flake.lock +++ b/flake.lock @@ -1,132 +1,5 @@ { "nodes": { - "cachix": { - "inputs": { - "devenv": "devenv_2", - "flake-compat": [ - "devenv", - "flake-compat" - ], - "nixpkgs": [ - "devenv", - "nixpkgs" - ], - "pre-commit-hooks": [ - "devenv", - "pre-commit-hooks" - ] - }, - "locked": { - "lastModified": 1712055811, - "narHash": "sha256-7FcfMm5A/f02yyzuavJe06zLa9hcMHsagE28ADcmQvk=", - "owner": "cachix", - "repo": "cachix", - "rev": "02e38da89851ec7fec3356a5c04bc8349cae0e30", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "cachix", - "type": "github" - } - }, - "devenv": { - "inputs": { - "cachix": "cachix", - "flake-compat": "flake-compat_2", - "nix": "nix_2", - "nixpkgs": "nixpkgs_2", - "pre-commit-hooks": "pre-commit-hooks" - }, - "locked": { - "lastModified": 1724763216, - "narHash": "sha256-oW2bwCrJpIzibCNK6zfIDaIQw765yMAuMSG2gyZfGv0=", - "owner": "cachix", - "repo": "devenv", - "rev": "1e4ef61205b9aa20fe04bf1c468b6a316281c4f1", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "devenv", - "type": "github" - } - }, - "devenv-root": { - "flake": false, - "locked": { - "narHash": "sha256-d6xi4mKdjkX2JFicDIv5niSzpyI0m/Hnm8GGAIU04kY=", - "type": "file", - "url": "file:///dev/null" - }, - "original": { - "type": "file", - "url": "file:///dev/null" - } - }, - "devenv_2": { - "inputs": { - "flake-compat": [ - "devenv", - "cachix", - "flake-compat" - ], - "nix": "nix", - "nixpkgs": "nixpkgs", - "poetry2nix": "poetry2nix", - "pre-commit-hooks": [ - "devenv", - "cachix", - "pre-commit-hooks" - ] - }, - "locked": { - "lastModified": 1708704632, - "narHash": "sha256-w+dOIW60FKMaHI1q5714CSibk99JfYxm0CzTinYWr+Q=", - "owner": "cachix", - "repo": "devenv", - "rev": "2ee4450b0f4b95a1b90f2eb5ffea98b90e48c196", - "type": "github" - }, - "original": { - "owner": "cachix", - "ref": "python-rewrite", - "repo": "devenv", - "type": "github" - } - }, - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1673956053, - "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "flake-compat_2": { - "flake": false, - "locked": { - "lastModified": 1696426674, - "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, "flake-parts": { "inputs": { "nixpkgs-lib": [ @@ -147,354 +20,44 @@ "type": "github" } }, - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1689068808, - "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_2": { - "inputs": { - "systems": "systems_2" - }, - "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_3": { - "inputs": { - "systems": "systems_3" - }, - "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "devenv", - "pre-commit-hooks", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1709087332, - "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", - "type": "github" - } - }, - "nix": { - "inputs": { - "flake-compat": "flake-compat", - "nixpkgs": [ - "devenv", - "cachix", - "devenv", - "nixpkgs" - ], - "nixpkgs-regression": "nixpkgs-regression" - }, - "locked": { - "lastModified": 1712911606, - "narHash": "sha256-BGvBhepCufsjcUkXnEEXhEVjwdJAwPglCC2+bInc794=", - "owner": "domenkozar", - "repo": "nix", - "rev": "b24a9318ea3f3600c1e24b4a00691ee912d4de12", - "type": "github" - }, - "original": { - "owner": "domenkozar", - "ref": "devenv-2.21", - "repo": "nix", - "type": "github" - } - }, - "nix-github-actions": { - "inputs": { - "nixpkgs": [ - "devenv", - "cachix", - "devenv", - "poetry2nix", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1688870561, - "narHash": "sha256-4UYkifnPEw1nAzqqPOTL2MvWtm3sNGw1UTYTalkTcGY=", - "owner": "nix-community", - "repo": "nix-github-actions", - "rev": "165b1650b753316aa7f1787f3005a8d2da0f5301", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nix-github-actions", - "type": "github" - } - }, - "nix2container": { - "inputs": { - "flake-utils": "flake-utils_3", - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1724996935, - "narHash": "sha256-njRK9vvZ1JJsP8oV2OgkBrpJhgQezI03S7gzskCcHos=", - "owner": "nlewo", - "repo": "nix2container", - "rev": "fa6bb0a1159f55d071ba99331355955ae30b3401", - "type": "github" - }, - "original": { - "owner": "nlewo", - "repo": "nix2container", - "type": "github" - } - }, - "nix_2": { - "inputs": { - "flake-compat": [ - "devenv", - "flake-compat" - ], - "nixpkgs": [ - "devenv", - "nixpkgs" - ], - "nixpkgs-regression": "nixpkgs-regression_2" - }, - "locked": { - "lastModified": 1712911606, - "narHash": "sha256-BGvBhepCufsjcUkXnEEXhEVjwdJAwPglCC2+bInc794=", - "owner": "domenkozar", - "repo": "nix", - "rev": "b24a9318ea3f3600c1e24b4a00691ee912d4de12", - "type": "github" - }, - "original": { - "owner": "domenkozar", - "ref": "devenv-2.21", - "repo": "nix", - "type": "github" - } - }, "nixpkgs": { "locked": { - "lastModified": 1692808169, - "narHash": "sha256-x9Opq06rIiwdwGeK2Ykj69dNc2IvUH1fY55Wm7atwrE=", + "lastModified": 1741173522, + "narHash": "sha256-k7VSqvv0r1r53nUI/IfPHCppkUAddeXn843YlAC5DR0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9201b5ff357e781bf014d0330d18555695df7ba8", + "rev": "d69ab0d71b22fa1ce3dbeff666e6deb4917db049", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixpkgs-unstable", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } }, "nixpkgs-qt6": { "locked": { - "lastModified": 1718428119, - "narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=", + "lastModified": 1734856068, + "narHash": "sha256-Q+CB1ajsJg4Z9HGHTBAGY1q18KpnnkmF/eCTLUY6FQ0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5", + "rev": "93ff48c9be84a76319dac293733df09bbbe3f25c", "type": "github" }, "original": { "owner": "NixOS", "repo": "nixpkgs", - "rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5", - "type": "github" - } - }, - "nixpkgs-regression": { - "locked": { - "lastModified": 1643052045, - "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", - "type": "github" - } - }, - "nixpkgs-regression_2": { - "locked": { - "lastModified": 1643052045, - "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", - "type": "github" - } - }, - "nixpkgs-stable": { - "locked": { - "lastModified": 1710695816, - "narHash": "sha256-3Eh7fhEID17pv9ZxrPwCLfqXnYP006RKzSs0JptsN84=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "614b4613980a522ba49f0d194531beddbb7220d3", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-23.11", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_2": { - "locked": { - "lastModified": 1713361204, - "narHash": "sha256-TA6EDunWTkc5FvDCqU3W2T3SFn0gRZqh6D/hJnM02MM=", - "owner": "cachix", - "repo": "devenv-nixpkgs", - "rev": "285676e87ad9f0ca23d8714a6ab61e7e027020c6", - "type": "github" - }, - "original": { - "owner": "cachix", - "ref": "rolling", - "repo": "devenv-nixpkgs", - "type": "github" - } - }, - "nixpkgs_3": { - "locked": { - "lastModified": 1724819573, - "narHash": "sha256-GnR7/ibgIH1vhoy8cYdmXE6iyZqKqFxQSVkFgosBh6w=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "71e91c409d1e654808b2621f28a327acfdad8dc2", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "poetry2nix": { - "inputs": { - "flake-utils": "flake-utils", - "nix-github-actions": "nix-github-actions", - "nixpkgs": [ - "devenv", - "cachix", - "devenv", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1692876271, - "narHash": "sha256-IXfZEkI0Mal5y1jr6IRWMqK8GW2/f28xJenZIPQqkY0=", - "owner": "nix-community", - "repo": "poetry2nix", - "rev": "d5006be9c2c2417dafb2e2e5034d83fabd207ee3", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "poetry2nix", - "type": "github" - } - }, - "pre-commit-hooks": { - "inputs": { - "flake-compat": [ - "devenv", - "flake-compat" - ], - "flake-utils": "flake-utils_2", - "gitignore": "gitignore", - "nixpkgs": [ - "devenv", - "nixpkgs" - ], - "nixpkgs-stable": "nixpkgs-stable" - }, - "locked": { - "lastModified": 1713775815, - "narHash": "sha256-Wu9cdYTnGQQwtT20QQMg7jzkANKQjwBD9iccfGKkfls=", - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "rev": "2ac4dcbf55ed43f3be0bae15e181f08a57af24a4", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "pre-commit-hooks.nix", + "rev": "93ff48c9be84a76319dac293733df09bbbe3f25c", "type": "github" } }, "root": { "inputs": { - "devenv": "devenv", - "devenv-root": "devenv-root", "flake-parts": "flake-parts", - "nix2container": "nix2container", - "nixpkgs": "nixpkgs_3", + "nixpkgs": "nixpkgs", "nixpkgs-qt6": "nixpkgs-qt6", - "systems": "systems_4" + "systems": "systems" } }, "systems": { @@ -511,51 +74,6 @@ "repo": "default", "type": "github" } - }, - "systems_2": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_3": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_4": { - "locked": { - "lastModified": 1689347949, - "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", - "owner": "nix-systems", - "repo": "default-linux", - "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default-linux", - "type": "github" - } } }, "root": "root", diff --git a/flake.nix b/flake.nix index a8c08a1e..6973dd28 100644 --- a/flake.nix +++ b/flake.nix @@ -2,209 +2,69 @@ description = "TagStudio"; inputs = { - devenv.url = "github:cachix/devenv"; - - devenv-root = { - url = "file+file:///dev/null"; - flake = false; - }; - flake-parts = { url = "github:hercules-ci/flake-parts"; inputs.nixpkgs-lib.follows = "nixpkgs"; }; - nix2container = { - url = "github:nlewo/nix2container"; - inputs.nixpkgs.follows = "nixpkgs"; - }; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + nixpkgs-qt6.url = "github:NixOS/nixpkgs/93ff48c9be84a76319dac293733df09bbbe3f25c"; - # Pinned to Qt version 6.7.1 - nixpkgs-qt6.url = "github:NixOS/nixpkgs/e6cea36f83499eb4e9cd184c8a8e823296b50ad5"; - - systems.url = "github:nix-systems/default-linux"; + systems.url = "github:nix-systems/default"; }; outputs = - { + inputs@{ flake-parts, nixpkgs, - nixpkgs-qt6, self, - systems, ... - }@inputs: + }: + let + inherit (nixpkgs) lib; + in flake-parts.lib.mkFlake { inherit inputs; } { - imports = [ inputs.devenv.flakeModule ]; - - systems = import systems; + systems = import inputs.systems; perSystem = + { pkgs, ... }: { - config, - pkgs, - system, - ... - }: - let - inherit (nixpkgs) lib; + packages = + let + python = pkgs.python312Packages; - qt6Pkgs = import nixpkgs-qt6 { inherit system; }; - in - { - formatter = pkgs.nixfmt-rfc-style; - - devenv.shells = rec { - default = tagstudio; - - tagstudio = - let - cfg = config.devenv.shells.tagstudio; - in - { - # NOTE: many things were simply transferred over from previous, - # there must be additional work in ensuring all relevant dependencies - # are in place (and no extraneous). I have already spent much - # work making this in the first place and just need to get it out - # there, especially after my promises. Would appreciate any help - # (possibly PRs!) on taking care of this. Otherwise, just expect - # this to get ironed out over time. - # - # Thank you! -Xarvex - - devenv.root = - let - devenvRoot = builtins.readFile inputs.devenv-root.outPath; - in - # If not overriden (/dev/null), --impure is necessary. - pkgs.lib.mkIf (devenvRoot != "") devenvRoot; - - name = "TagStudio"; - - # Derived from previous flake iteration. - packages = - (with pkgs; [ - cmake - binutils - coreutils - dbus - fontconfig - freetype - gdb - glib - libGL - libGLU - libgcc - libxkbcommon - mypy - ruff - xorg.libxcb - xorg.libX11 - zstd - ]) - ++ (with qt6Pkgs; [ - qt6.full - qt6.qtbase - qt6.qtwayland - qtcreator - ]); - - enterShell = - let - setQtEnv = - pkgs.runCommand "set-qt-env" - { - buildInputs = with qt6Pkgs.qt6; [ - qtbase - ]; - - nativeBuildInputs = - (with pkgs; [ - makeShellWrapper - ]) - ++ (with qt6Pkgs.qt6; [ - wrapQtAppsHook - ]); - } - '' - makeShellWrapper "$(type -p sh)" "$out" "''${qtWrapperArgs[@]}" - sed "/^exec/d" -i "$out" - ''; - in - '' - source ${setQtEnv} - ''; - - scripts.tagstudio.exec = '' - python ${cfg.devenv.root}/tagstudio/tag_studio.py - ''; - - env = { - QT_QPA_PLATFORM = "wayland;xcb"; - - # Derived from previous flake iteration. - # Not desired given LD_LIBRARY_PATH pollution. - # See supposed alternative below, further research required. - LD_LIBRARY_PATH = lib.makeLibraryPath ( - (with pkgs; [ - dbus - fontconfig - freetype - gcc-unwrapped - glib - libglvnd - libkrb5 - libpulseaudio - libva - libxkbcommon - openssl - stdenv.cc.cc.lib - wayland - xorg.libxcb - xorg.libX11 - xorg.libXrandr - zlib - zstd - ]) - ++ (with qt6Pkgs.qt6; [ - qtbase - qtwayland - full - ]) - ); - }; - - languages.python = { - enable = true; - venv = { - enable = true; - quiet = true; - requirements = - let - excludeDeps = - req: deps: - builtins.concatStringsSep "\n" ( - builtins.filter (line: !(lib.any (elem: lib.hasPrefix elem line) deps)) (lib.splitString "\n" req) - ); - in - '' - ${builtins.readFile ./requirements.txt} - ${excludeDeps (builtins.readFile ./requirements-dev.txt) [ - "mypy" - "ruff" - ]} - ''; - }; - - # Should be able to replace LD_LIBRARY_PATH? - # Was not quite able to get working, - # will be consulting cachix community. -Xarvex - # libraries = with pkgs; [ ]; - }; + pillow-jxl-plugin = python.callPackage ./nix/package/pillow-jxl-plugin.nix { + inherit (pkgs) cmake; + inherit pyexiv2; + inherit (pkgs) rustPlatform; }; + pyexiv2 = python.callPackage ./nix/package/pyexiv2.nix { inherit (pkgs) exiv2; }; + vtf2img = python.callPackage ./nix/package/vtf2img.nix { }; + in + rec { + default = tagstudio; + tagstudio = pkgs.python312Packages.callPackage ./nix/package { + inherit pillow-jxl-plugin vtf2img; + }; + tagstudio-jxl = tagstudio.override { withJXLSupport = true; }; + + inherit pillow-jxl-plugin pyexiv2 vtf2img; + }; + + devShells = rec { + default = tagstudio; + tagstudio = import ./nix/shell.nix { + inherit + inputs + lib + pkgs + self + ; + }; }; + + formatter = pkgs.nixfmt-rfc-style; }; }; } diff --git a/mkdocs.yml b/mkdocs.yml index b3c3c8ad..f18c8f81 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -89,6 +89,7 @@ theme: #- navigation.top - search.suggest - content.code.annotate + - content.code.copy - content.action.edit icon: repo: fontawesome/brands/github @@ -136,3 +137,6 @@ plugins: - tags - social: # social embed cards enabled: !ENV [CI, false] # enabled only when running in CI (eg GitHub Actions) + +extra_css: + - stylesheets/extra.css diff --git a/nix/package/default.nix b/nix/package/default.nix new file mode 100644 index 00000000..faba3072 --- /dev/null +++ b/nix/package/default.nix @@ -0,0 +1,114 @@ +{ + buildPythonApplication, + chardet, + ffmpeg-headless, + ffmpeg-python, + hatchling, + humanfriendly, + lib, + mutagen, + numpy, + opencv-python, + pillow, + pillow-heif, + pillow-jxl-plugin, + pipewire, + pydub, + pyside6, + pytest-qt, + pytest-xdist, + pytestCheckHook, + pythonRelaxDepsHook, + qt6, + rawpy, + send2trash, + sqlalchemy, + stdenv, + structlog, + syrupy, + ujson, + vtf2img, + + withJXLSupport ? false, +}: + +let + pyproject = (lib.importTOML ../../pyproject.toml).project; +in +buildPythonApplication { + pname = pyproject.name; + inherit (pyproject) version; + pyproject = true; + + src = ../../.; + + nativeBuildInputs = [ + pythonRelaxDepsHook + qt6.wrapQtAppsHook + ]; + buildInputs = [ + qt6.qtbase + qt6.qtmultimedia + ]; + + nativeCheckInputs = [ + pytest-qt + pytest-xdist + pytestCheckHook + syrupy + ]; + + makeWrapperArgs = + [ "--prefix PATH : ${lib.makeBinPath [ ffmpeg-headless ]}" ] + ++ lib.optional stdenv.hostPlatform.isLinux "--prefix LD_LIBRARY_PATH : ${ + lib.makeLibraryPath [ pipewire ] + }"; + + pythonRemoveDeps = true; + pythonImportsCheck = [ "tagstudio" ]; + + build-system = [ hatchling ]; + dependencies = [ + chardet + ffmpeg-python + humanfriendly + mutagen + numpy + opencv-python + pillow + pillow-heif + pydub + pyside6 + rawpy + send2trash + sqlalchemy + structlog + ujson + vtf2img + ] ++ lib.optional withJXLSupport pillow-jxl-plugin; + + # INFO: These tests require modifications to a library, which does not work + # in a read-only environment. + disabledTests = [ + "test_build_tag_panel_add_alias_callback" + "test_build_tag_panel_add_aliases" + "test_build_tag_panel_add_sub_tag_callback" + "test_build_tag_panel_build_tag" + "test_build_tag_panel_remove_alias_callback" + "test_build_tag_panel_remove_subtag_callback" + "test_build_tag_panel_set_aliases" + "test_build_tag_panel_set_parent_tags" + "test_build_tag_panel_set_tag" + "test_json_migration" + "test_library_migrations" + ]; + + meta = { + inherit (pyproject) description; + homepage = "https://docs.tagstud.io/"; + license = lib.licenses.gpl3Only; + maintainers = with lib.maintainers; [ xarvex ]; + mainProgram = "tagstudio"; + platforms = lib.platforms.unix; + }; +} diff --git a/nix/package/pillow-jxl-plugin.nix b/nix/package/pillow-jxl-plugin.nix new file mode 100644 index 00000000..9c8c2fa2 --- /dev/null +++ b/nix/package/pillow-jxl-plugin.nix @@ -0,0 +1,67 @@ +{ + buildPythonPackage, + cmake, + fetchPypi, + lib, + numpy, + packaging, + pillow, + pyexiv2, + pytestCheckHook, + rustPlatform, +}: + +buildPythonPackage rec { + pname = "pillow-jxl-plugin"; + version = "1.3.2"; + pyproject = true; + + src = fetchPypi { + pname = builtins.replaceStrings [ "-" ] [ "_" ] pname; + inherit version; + hash = "sha256-efBoek8yUFR+ArhS55lm9F2XhkZ7/I3GsScQEe8U/2I="; + }; + + cargoDeps = rustPlatform.fetchCargoVendor { + inherit src; + hash = "sha256-vZHrwGfgo3fIIOY7p0vy4XIKiHoddPDdJggkBen+w/A="; + }; + + nativeBuildInputs = [ + cmake + rustPlatform.cargoSetupHook + rustPlatform.maturinBuildHook + ]; + + nativeCheckInputs = [ + numpy + pyexiv2 + pytestCheckHook + ]; + + # INFO: Working directory takes precedence in the Python path. Remove + # `pillow_jxl` to prevent it from being loaded during pytest, rather than the + # built module, as it includes a `pillow_jxl.pillow_jxl` .so that is imported. + # See: https://github.com/NixOS/nixpkgs/issues/255262 + # See: https://github.com/NixOS/nixpkgs/pull/255471 + preCheck = '' + rm -r pillow_jxl + ''; + + dontUseCmakeConfigure = true; + + pythonImportsCheck = [ "pillow_jxl" ]; + + dependencies = [ + packaging + pillow + ]; + + meta = { + description = "Pillow plugin for JPEG-XL, using Rust for bindings."; + homepage = "https://github.com/Isotr0py/pillow-jpegxl-plugin"; + license = lib.licenses.gpl3; + maintainers = with lib.maintainers; [ xarvex ]; + platforms = lib.platforms.unix; + }; +} diff --git a/nix/package/pyexiv2.nix b/nix/package/pyexiv2.nix new file mode 100644 index 00000000..b946e0ea --- /dev/null +++ b/nix/package/pyexiv2.nix @@ -0,0 +1,32 @@ +{ + autoPatchelfHook, + buildPythonPackage, + exiv2, + fetchFromGitHub, + lib, +}: + +buildPythonPackage rec { + pname = "pyexiv2"; + version = "2.15.3"; + + src = fetchFromGitHub { + owner = "LeoHsiao1"; + repo = pname; + rev = "v${version}"; + hash = "sha256-83bFMaoXncvhRJNcCgkkC7B29wR5pjuLO/EdkQdqxxo="; + }; + + nativeBuildInputs = [ autoPatchelfHook ]; + buildInputs = [ exiv2.lib ]; + + pythonImportsCheck = [ "pyexiv2" ]; + + meta = { + description = "Read and write image metadata, including EXIF, IPTC, XMP, ICC Profile."; + homepage = "https://github.com/LeoHsiao1/pyexiv2"; + license = lib.licenses.gpl3; + maintainers = with lib.maintainers; [ xarvex ]; + platforms = with lib.platforms; darwin ++ linux ++ windows; + }; +} diff --git a/nix/package/vtf2img.nix b/nix/package/vtf2img.nix new file mode 100644 index 00000000..edfe42da --- /dev/null +++ b/nix/package/vtf2img.nix @@ -0,0 +1,29 @@ +{ + buildPythonPackage, + fetchPypi, + lib, + pillow, +}: + +buildPythonPackage rec { + pname = "vtf2img"; + version = "0.1.0"; + + src = fetchPypi { + inherit pname version; + hash = "sha256-YmWs8673d72wH4nTOXP4AFGs2grIETln4s1MD5PfE0A="; + }; + + pythonImportsCheck = [ "vtf2img" ]; + + dependencies = [ pillow ]; + + meta = { + description = "A Python library to convert Valve Texture Format (VTF) files to images."; + homepage = "https://github.com/julienc91/vtf2img"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ xarvex ]; + mainProgram = "vtf2img"; + platforms = lib.platforms.unix; + }; +} diff --git a/nix/shell.nix b/nix/shell.nix new file mode 100644 index 00000000..ffafa793 --- /dev/null +++ b/nix/shell.nix @@ -0,0 +1,93 @@ +{ + inputs, + lib, + pkgs, + ... +}: + +let + qt6Pkgs = import inputs.nixpkgs-qt6 { inherit (pkgs) system; }; + + pythonLibraryPath = lib.makeLibraryPath ( + (with pkgs; [ + fontconfig.lib + freetype + glib + stdenv.cc.cc.lib + zstd + ]) + ++ (with qt6Pkgs.qt6; [ qtbase ]) + ++ lib.optionals (!pkgs.stdenv.isDarwin) ( + (with pkgs; [ + dbus.lib + libGL + libdrm + libpulseaudio + libva + libxkbcommon + pipewire + xorg.libX11 + xorg.libXrandr + ]) + ++ (with qt6Pkgs.qt6; [ qtwayland ]) + ) + ); + libraryPath = "${lib.optionalString pkgs.stdenv.isDarwin "DY"}LD_LIBRARY_PATH"; + + python = pkgs.python312; + pythonWrapped = pkgs.symlinkJoin { + inherit (python) + name + pname + version + meta + ; + + paths = [ python ]; + + nativeBuildInputs = (with pkgs; [ makeWrapper ]) ++ (with qt6Pkgs.qt6; [ wrapQtAppsHook ]); + buildInputs = with qt6Pkgs.qt6; [ qtbase ]; + + postBuild = '' + wrapProgram $out/bin/python3.12 \ + --prefix ${libraryPath} : ${pythonLibraryPath} \ + "''${qtWrapperArgs[@]}" + ''; + }; +in +pkgs.mkShellNoCC { + nativeBuildInputs = with pkgs; [ + coreutils + + ruff + ]; + buildInputs = [ pythonWrapped ] ++ (with pkgs; [ ffmpeg-headless ]); + + env.QT_QPA_PLATFORM = "wayland;xcb"; + + shellHook = + let + python = lib.getExe pythonWrapped; + in + # bash + '' + if [ ! -f .venv/bin/activate ] || [ "$(readlink -f .venv/bin/python)" != "$(readlink -f ${python})" ]; then + printf '%s\n' 'Regenerating virtual environment, Python interpreter changed...' >&2 + rm -rf .venv + ${python} -m venv .venv + fi + + source .venv/bin/activate + + if [ ! -f .venv/pyproject.toml ] || [ "$(cat .venv/pyproject.toml)" != "$(cat pyproject.toml)" ]; then + printf '%s\n' 'Installing dependencies, pyproject.toml changed...' >&2 + pip install --quiet --editable '.[mkdocs,mypy,pytest]' + cp pyproject.toml .venv/pyproject.toml + fi + ''; + + meta = { + maintainers = with lib.maintainers; [ xarvex ]; + platforms = lib.platforms.unix; + }; +} diff --git a/pyproject.toml b/pyproject.toml index 14a5cc8d..3b7f3c55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,28 +1,85 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + [project] name = "TagStudio" description = "A User-Focused Photo & File Management System." version = "9.5.1" license = "GPL-3.0-only" readme = "README.md" +dependencies = [ + "chardet==5.2.0", + "ffmpeg-python==0.2.0", + "humanfriendly==10.0", + "mutagen==1.47.0", + "numpy==2.1.0", + "opencv_python==4.10.0.84", + "Pillow==10.3.0", + "pillow-heif==0.16.0", + "pillow-jxl-plugin==1.3.0", + "pydub==0.25.1", + "PySide6==6.8.0.1", + "rawpy==0.22.0", + "Send2Trash==1.8.3", + "SQLAlchemy==2.0.34", + "structlog==24.4.0", + "typing_extensions>=3.10.0.0,<4.11.0", + "ujson>=5.8.0,<5.9.0", + "vtf2img==0.1.0", +] -[tool.ruff] -exclude = ["main_window.py", "home_ui.py", "resources.py", "resources_rc.py"] -line-length = 100 +[project.optional-dependencies] +dev = ["pre-commit==3.7.0", "tagstudio[mkdocs,mypy,pyinstaller,pytest,ruff]"] +mkdocs = ["mkdocs-material[imaging]==9.*"] +mypy = ["mypy==1.11.2", "mypy-extensions==1.*", "types-ujson>=5.8.0,<5.9.0"] +pyinstaller = ["Pyinstaller==6.6.0"] +pytest = [ + "pytest==8.2.0", + "pytest-cov==5.0.0", + "pytest-qt==4.4.0", + "syrupy==4.7.1", +] +ruff = ["ruff==0.8.1"] -[tool.ruff.lint.per-file-ignores] -"tagstudio/tests/**" = ["D", "E402"] -"tagstudio/src/qt/helpers/vendored/**" = ["B", "E", "N", "UP", "SIM115"] +[project.gui-scripts] +tagstudio = "tagstudio.main:main" -[tool.ruff.lint.pydocstyle] -convention = "google" +[tool.hatch.build.targets.wheel] +packages = ["src/tagstudio"] -[tool.ruff.lint] -select = ["B", "D", "E", "F", "FBT003", "I", "N", "SIM", "T20", "UP"] -ignore = ["D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107"] +[tool.mypy] +mypy_path = ["src/tagstudio"] +disable_error_code = [ + "annotation-unchecked", + "func-returns-value", + "import-untyped", +] +explicit_package_bases = true +ignore_missing_imports = true +implicit_optional = true +strict_optional = false +warn_unused_ignores = true + +[[tool.mypy.overrides]] +module = "tagstudio.qt.main_window" +ignore_errors = true + +[[tool.mypy.overrides]] +module = "tagstudio.qt.ui.home_ui" +ignore_errors = true + +[[tool.mypy.overrides]] +module = "tagstudio.core.ts_core" +ignore_errors = true + +[tool.pytest.ini_options] +#addopts = "-m 'not qt'" +qt_api = "pyside6" [tool.pyright] ignore = [".venv/**"] -include = ["tagstudio/**"] +include = ["src/tagstudio/**"] reportAny = false reportImplicitStringConcatenation = false # reportOptionalMemberAccess = false @@ -31,30 +88,17 @@ reportUnknownArgumentType = false reportUnknownMemberType = false reportUnusedCallResult = false -[tool.mypy] -strict_optional = false -disable_error_code = ["func-returns-value", "import-untyped"] -explicit_package_bases = true -warn_unused_ignores = true -check_untyped_defs = true -mypy_path = ["tagstudio"] +[tool.ruff] +exclude = ["main_window.py", "home_ui.py", "resources.py", "resources_rc.py"] +line-length = 100 -[[tool.mypy.overrides]] -module = "tests.*" -ignore_errors = true +[tool.ruff.lint] +select = ["B", "D", "E", "F", "FBT003", "I", "N", "SIM", "T20", "UP"] +ignore = ["D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107"] -[[tool.mypy.overrides]] -module = "src.qt.main_window" -ignore_errors = true +[tool.ruff.lint.per-file-ignores] +"tests/**" = ["D", "E402"] +"src/tagstudio/qt/helpers/vendored/**" = ["B", "E", "N", "UP", "SIM115"] -[[tool.mypy.overrides]] -module = "src.qt.ui.home_ui" -ignore_errors = true - -[[tool.mypy.overrides]] -module = "src.core.ts_core" -ignore_errors = true - -[tool.pytest.ini_options] -#addopts = "-m 'not qt'" -qt_api = "pyside6" +[tool.ruff.lint.pydocstyle] +convention = "google" diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index c80848cc..00000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,8 +0,0 @@ -ruff==0.8.1 -pre-commit==3.7.0 -pytest==8.2.0 -Pyinstaller==6.6.0 -mypy==1.11.2 -syrupy==4.7.1 -pytest-qt==4.4.0 -pytest-cov==5.0.0 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index ff6e6d4a..00000000 --- a/requirements.txt +++ /dev/null @@ -1,20 +0,0 @@ -chardet==5.2.0 -ffmpeg-python==0.2.0 -humanfriendly==10.0 -mutagen==1.47.0 -numpy==2.1.0 -opencv_python==4.10.0.84 -pillow-heif==0.16.0 -pillow-jxl-plugin==1.3.0 -Pillow==10.3.0 -pydub==0.25.1 -PySide6_Addons==6.8.0.1 -PySide6_Essentials==6.8.0.1 -PySide6==6.8.0.1 -rawpy==0.22.0 -Send2Trash==1.8.3 -SQLAlchemy==2.0.34 -structlog==24.4.0 -typing_extensions>=3.10.0.0,<=4.11.0 -ujson>=5.8.0,<=5.9.0 -vtf2img==0.1.0 \ No newline at end of file diff --git a/src/tagstudio/core/driver.py b/src/tagstudio/core/driver.py index 1f7dd4f7..f4f073d8 100644 --- a/src/tagstudio/core/driver.py +++ b/src/tagstudio/core/driver.py @@ -2,9 +2,10 @@ from pathlib import Path import structlog from PySide6.QtCore import QSettings -from src.core.constants import TS_FOLDER_NAME -from src.core.enums import SettingItems -from src.core.library.alchemy.library import LibraryStatus + +from tagstudio.core.constants import TS_FOLDER_NAME +from tagstudio.core.enums import SettingItems +from tagstudio.core.library.alchemy.library import LibraryStatus logger = structlog.get_logger(__name__) diff --git a/src/tagstudio/core/enums.py b/src/tagstudio/core/enums.py index 84debc91..2a11a12f 100644 --- a/src/tagstudio/core/enums.py +++ b/src/tagstudio/core/enums.py @@ -70,6 +70,6 @@ class LibraryPrefs(DefaultEnum): """Library preferences with default value accessible via .default property.""" IS_EXCLUDE_LIST = True - EXTENSION_LIST: list[str] = [".json", ".xmp", ".aae"] - PAGE_SIZE: int = 500 - DB_VERSION: int = 8 + EXTENSION_LIST = [".json", ".xmp", ".aae"] + PAGE_SIZE = 500 + DB_VERSION = 8 diff --git a/src/tagstudio/core/library/__init__.py b/src/tagstudio/core/library/__init__.py deleted file mode 100644 index 98b662e5..00000000 --- a/src/tagstudio/core/library/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .alchemy import * # noqa diff --git a/src/tagstudio/core/library/alchemy/__init__.py b/src/tagstudio/core/library/alchemy/__init__.py deleted file mode 100644 index 2cfed3b1..00000000 --- a/src/tagstudio/core/library/alchemy/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .enums import ItemType -from .library import Library -from .models import Entry, Tag - -__all__ = ["Entry", "Library", "Tag", "ItemType"] diff --git a/src/tagstudio/core/library/alchemy/db.py b/src/tagstudio/core/library/alchemy/db.py index e57c4882..78a766f1 100644 --- a/src/tagstudio/core/library/alchemy/db.py +++ b/src/tagstudio/core/library/alchemy/db.py @@ -2,13 +2,15 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + from pathlib import Path import structlog from sqlalchemy import Dialect, Engine, String, TypeDecorator, create_engine, text from sqlalchemy.exc import OperationalError from sqlalchemy.orm import DeclarativeBase -from src.core.constants import RESERVED_TAG_END + +from tagstudio.core.constants import RESERVED_TAG_END logger = structlog.getLogger(__name__) diff --git a/src/tagstudio/core/library/alchemy/default_color_groups.py b/src/tagstudio/core/library/alchemy/default_color_groups.py index ccc5a649..9193fc09 100644 --- a/src/tagstudio/core/library/alchemy/default_color_groups.py +++ b/src/tagstudio/core/library/alchemy/default_color_groups.py @@ -5,7 +5,7 @@ import structlog -from .models import Namespace, TagColorGroup +from tagstudio.core.library.alchemy.models import Namespace, TagColorGroup logger = structlog.get_logger(__name__) diff --git a/src/tagstudio/core/library/alchemy/enums.py b/src/tagstudio/core/library/alchemy/enums.py index 7ae865ed..dc3b8b56 100644 --- a/src/tagstudio/core/library/alchemy/enums.py +++ b/src/tagstudio/core/library/alchemy/enums.py @@ -3,8 +3,9 @@ from dataclasses import dataclass, replace from pathlib import Path import structlog -from src.core.query_lang import AST as Query # noqa: N811 -from src.core.query_lang import Constraint, ConstraintType, Parser + +from tagstudio.core.query_lang.ast import AST, Constraint, ConstraintType +from tagstudio.core.query_lang.parser import Parser MAX_SQL_VARIABLES = 32766 # 32766 is the max sql bind parameter count as defined here: https://github.com/sqlite/sqlite/blob/master/src/sqliteLimit.h#L140 @@ -80,7 +81,7 @@ class FilterState: # these should be erased on update # Abstract Syntax Tree Of the current Search Query - ast: Query = None + ast: AST = None @property def limit(self): diff --git a/src/tagstudio/core/library/alchemy/fields.py b/src/tagstudio/core/library/alchemy/fields.py index 2c9ab41a..c4675dc4 100644 --- a/src/tagstudio/core/library/alchemy/fields.py +++ b/src/tagstudio/core/library/alchemy/fields.py @@ -2,6 +2,7 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + from __future__ import annotations from dataclasses import dataclass, field @@ -11,11 +12,11 @@ from typing import TYPE_CHECKING, Any from sqlalchemy import ForeignKey from sqlalchemy.orm import Mapped, declared_attr, mapped_column, relationship -from .db import Base -from .enums import FieldTypeEnum +from tagstudio.core.library.alchemy.db import Base +from tagstudio.core.library.alchemy.enums import FieldTypeEnum if TYPE_CHECKING: - from .models import Entry, ValueType + from tagstudio.core.library.alchemy.models import Entry, ValueType class BaseField(Base): diff --git a/src/tagstudio/core/library/alchemy/joins.py b/src/tagstudio/core/library/alchemy/joins.py index f8b569f8..d01e6764 100644 --- a/src/tagstudio/core/library/alchemy/joins.py +++ b/src/tagstudio/core/library/alchemy/joins.py @@ -2,10 +2,11 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + from sqlalchemy import ForeignKey from sqlalchemy.orm import Mapped, mapped_column -from .db import Base +from tagstudio.core.library.alchemy.db import Base class TagParent(Base): diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index 5219cdb3..9794a79d 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -2,6 +2,7 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + import re import shutil import time @@ -43,10 +44,8 @@ from sqlalchemy.orm import ( make_transient, selectinload, ) -from src.core.library.json.library import Library as JsonLibrary # type: ignore -from src.qt.translations import Translations -from ...constants import ( +from tagstudio.core.constants import ( BACKUP_FOLDER_NAME, LEGACY_TAG_FIELD_IDS, RESERVED_NAMESPACE_PREFIX, @@ -57,19 +56,35 @@ from ...constants import ( TAG_META, TS_FOLDER_NAME, ) -from ...enums import LibraryPrefs -from . import default_color_groups -from .db import make_tables -from .enums import MAX_SQL_VARIABLES, FieldTypeEnum, FilterState, SortingModeEnum -from .fields import ( +from tagstudio.core.enums import LibraryPrefs +from tagstudio.core.library.alchemy import default_color_groups +from tagstudio.core.library.alchemy.db import make_tables +from tagstudio.core.library.alchemy.enums import ( + MAX_SQL_VARIABLES, + FieldTypeEnum, + FilterState, + SortingModeEnum, +) +from tagstudio.core.library.alchemy.fields import ( BaseField, DatetimeField, TextField, _FieldID, ) -from .joins import TagEntry, TagParent -from .models import Entry, Folder, Namespace, Preferences, Tag, TagAlias, TagColorGroup, ValueType -from .visitors import SQLBoolExpressionBuilder +from tagstudio.core.library.alchemy.joins import TagEntry, TagParent +from tagstudio.core.library.alchemy.models import ( + Entry, + Folder, + Namespace, + Preferences, + Tag, + TagAlias, + TagColorGroup, + ValueType, +) +from tagstudio.core.library.alchemy.visitors import SQLBoolExpressionBuilder +from tagstudio.core.library.json.library import Library as JsonLibrary +from tagstudio.qt.translations import Translations if TYPE_CHECKING: from sqlalchemy import Select @@ -192,7 +207,7 @@ class Library: """Class for the Library object, and all CRUD operations made upon it.""" library_dir: Path | None = None - storage_path: Path | str | None + storage_path: Path | None engine: Engine | None = None folder: Folder | None included_files: set[Path] = set() @@ -291,7 +306,7 @@ class Library: self.set_prefs(LibraryPrefs.IS_EXCLUDE_LIST, json_lib.is_exclude_list) end_time = time.time() - logger.info(f"Library Converted! ({format_timespan(end_time-start_time)})") + logger.info(f"Library Converted! ({format_timespan(end_time - start_time)})") def get_field_name_from_id(self, field_id: int) -> _FieldID: for f in _FieldID: @@ -316,7 +331,7 @@ class Library: else: return tag.name - def open_library(self, library_dir: Path, storage_path: str | None = None) -> LibraryStatus: + def open_library(self, library_dir: Path, storage_path: Path | None = None) -> LibraryStatus: is_new: bool = True if storage_path == ":memory:": self.storage_path = storage_path @@ -324,7 +339,6 @@ class Library: return self.open_sqlite_library(library_dir, is_new) else: self.storage_path = library_dir / TS_FOLDER_NAME / self.SQL_FILENAME - if self.verify_ts_folder(library_dir) and (is_new := not self.storage_path.exists()): json_path = library_dir / TS_FOLDER_NAME / self.JSON_FILENAME if json_path.exists(): @@ -365,7 +379,7 @@ class Library: select(Preferences).where(Preferences.key == LibraryPrefs.DB_VERSION.name) ) if db_result: - db_version = db_result.value # type: ignore + db_version = db_result.value # NOTE: DB_VERSION 6 is the first supported SQL DB version. if db_version < 6 or db_version > LibraryPrefs.DB_VERSION.default: @@ -628,7 +642,7 @@ class Library: end_time = time.time() logger.info( f"[Library] Time it took to get entry: " - f"{format_timespan(end_time-start_time, max_units=5)}", + f"{format_timespan(end_time - start_time, max_units=5)}", with_fields=with_fields, with_tags=with_tags, ) @@ -840,7 +854,7 @@ class Library: query_count = select(func.count()).select_from(statement.alias("entries")) count_all: int = session.execute(query_count).scalar() end_time = time.time() - logger.info(f"finished counting ({format_timespan(end_time-start_time)})") + logger.info(f"finished counting ({format_timespan(end_time - start_time)})") sort_on: ColumnExpressionArgument = Entry.id match search.sorting_mode: @@ -1160,7 +1174,7 @@ class Library: if tag: tags.append(tag.id) else: - new = session.add(Tag(name=string)) + new = session.add(Tag(name=string)) # type: ignore if new: tags.append(new.id) session.flush() @@ -1348,7 +1362,7 @@ class Library: assert isinstance(self.library_dir, Path) makedirs(str(self.library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME), exist_ok=True) - filename = f'ts_library_backup_{datetime.now(UTC).strftime("%Y_%m_%d_%H%M%S")}.sqlite' + filename = f"ts_library_backup_{datetime.now(UTC).strftime('%Y_%m_%d_%H%M%S')}.sqlite" target_path = self.library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME / filename diff --git a/src/tagstudio/core/library/alchemy/models.py b/src/tagstudio/core/library/alchemy/models.py index 0e77161d..5df75b73 100644 --- a/src/tagstudio/core/library/alchemy/models.py +++ b/src/tagstudio/core/library/alchemy/models.py @@ -8,16 +8,16 @@ from pathlib import Path from sqlalchemy import JSON, ForeignKey, ForeignKeyConstraint, Integer, event from sqlalchemy.orm import Mapped, mapped_column, relationship -from ...constants import TAG_ARCHIVED, TAG_FAVORITE -from .db import Base, PathType -from .fields import ( +from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE +from tagstudio.core.library.alchemy.db import Base, PathType +from tagstudio.core.library.alchemy.enums import FieldTypeEnum +from tagstudio.core.library.alchemy.fields import ( BaseField, BooleanField, DatetimeField, - FieldTypeEnum, TextField, ) -from .joins import TagParent +from tagstudio.core.library.alchemy.joins import TagParent class Namespace(Base): @@ -304,7 +304,7 @@ class ValueType(Base): def slugify_field_key(mapper, connection, target): """Slugify the field key before inserting into the database.""" if not target.key: - from .library import slugify + from tagstudio.core.library.alchemy.library import slugify target.key = slugify(target.tag) diff --git a/src/tagstudio/core/library/alchemy/visitors.py b/src/tagstudio/core/library/alchemy/visitors.py index 1ba7b467..e242d2f6 100644 --- a/src/tagstudio/core/library/alchemy/visitors.py +++ b/src/tagstudio/core/library/alchemy/visitors.py @@ -9,18 +9,25 @@ import structlog from sqlalchemy import ColumnElement, and_, distinct, func, or_, select, text from sqlalchemy.orm import Session from sqlalchemy.sql.operators import ilike_op -from src.core.media_types import FILETYPE_EQUIVALENTS, MediaCategories -from src.core.query_lang import BaseVisitor -from src.core.query_lang.ast import ANDList, Constraint, ConstraintType, Not, ORList, Property -from .joins import TagEntry -from .models import Entry, Tag, TagAlias +from tagstudio.core.library.alchemy.joins import TagEntry +from tagstudio.core.library.alchemy.models import Entry, Tag, TagAlias +from tagstudio.core.media_types import FILETYPE_EQUIVALENTS, MediaCategories +from tagstudio.core.query_lang.ast import ( + ANDList, + BaseVisitor, + Constraint, + ConstraintType, + Not, + ORList, + Property, +) # Only import for type checking/autocompletion, will not be imported at runtime. if TYPE_CHECKING: - from .library import Library + from tagstudio.core.library.alchemy.library import Library else: - Library = None # don't import .library because of circular imports + Library = None # don't import library because of circular imports logger = structlog.get_logger(__name__) diff --git a/src/tagstudio/core/library/json/__init__.py b/src/tagstudio/core/library/json/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/tagstudio/core/library/json/library.py b/src/tagstudio/core/library/json/library.py index 43e041cb..79d23a26 100644 --- a/src/tagstudio/core/library/json/library.py +++ b/src/tagstudio/core/library/json/library.py @@ -7,29 +7,28 @@ """The Library object and related methods for TagStudio.""" import datetime +from enum import Enum import os +from pathlib import Path import time import traceback +from typing import Generator, cast import xml.etree.ElementTree as ET import structlog +from typing_extensions import Self import ujson -from enum import Enum -from pathlib import Path -from typing import cast, Generator -from typing_extensions import Self - -from .fields import DEFAULT_FIELDS, TEXT_FIELDS -from src.core.enums import OpenStatus -from src.core.utils.str import strip_punctuation -from src.core.utils.web import strip_web_protocol -from src.core.constants import ( +from tagstudio.core.constants import ( BACKUP_FOLDER_NAME, COLLAGE_FOLDER_NAME, TS_FOLDER_NAME, VERSION, ) +from tagstudio.core.enums import OpenStatus +from tagstudio.core.library.json.fields import DEFAULT_FIELDS, TEXT_FIELDS +from tagstudio.core.utils.str import strip_punctuation +from tagstudio.core.utils.web import strip_web_protocol TYPE = ["file", "meta", "alt", "mask"] @@ -128,7 +127,7 @@ class Entry: if field_index >= 0 and field_index == i: t: list[int] = library.get_field_attr(f, "content") logger.info( - f't:{tag_id}, i:{i}, idx:{field_index}, c:{library.get_field_attr(f, "content")}' + f"t:{tag_id}, i:{i}, idx:{field_index}, c:{library.get_field_attr(f, 'content')}" ) t.remove(tag_id) elif field_index < 0: @@ -208,9 +207,9 @@ class Tag: """Returns a formatted tag name intended for displaying.""" if self.subtag_ids: if library.get_tag(self.subtag_ids[0]).shorthand: - return f"{self.name}" f" ({library.get_tag(self.subtag_ids[0]).shorthand})" + return f"{self.name} ({library.get_tag(self.subtag_ids[0]).shorthand})" else: - return f"{self.name}" f" ({library.get_tag(self.subtag_ids[0]).name})" + return f"{self.name} ({library.get_tag(self.subtag_ids[0]).name})" else: return f"{self.name}" @@ -759,7 +758,7 @@ class Library: logger.info(f"[LIBRARY] Saving Library Backup to Disk...") start_time = time.time() - filename = f'ts_library_backup_{datetime.datetime.utcnow().strftime("%F_%T").replace(":", "")}.json' + filename = f"ts_library_backup_{datetime.datetime.utcnow().strftime('%F_%T').replace(':', '')}.json" self.verify_ts_folders() with open( diff --git a/src/tagstudio/core/media_types.py b/src/tagstudio/core/media_types.py index be9ebdbd..f784be86 100644 --- a/src/tagstudio/core/media_types.py +++ b/src/tagstudio/core/media_types.py @@ -16,35 +16,35 @@ FILETYPE_EQUIVALENTS = [set(["jpg", "jpeg"])] class MediaType(str, Enum): """Names of media types.""" - ADOBE_PHOTOSHOP: str = "adobe_photoshop" - AFFINITY_PHOTO: str = "affinity_photo" - ARCHIVE: str = "archive" - AUDIO_MIDI: str = "audio_midi" - AUDIO: str = "audio" - BLENDER: str = "blender" - DATABASE: str = "database" - DISK_IMAGE: str = "disk_image" - DOCUMENT: str = "document" - EBOOK: str = "ebook" - FONT: str = "font" - IMAGE_ANIMATED: str = "image_animated" - IMAGE_RAW: str = "image_raw" - IMAGE_VECTOR: str = "image_vector" - IMAGE: str = "image" - INSTALLER: str = "installer" - MATERIAL: str = "material" - MODEL: str = "model" - OPEN_DOCUMENT: str = "open_document" - PACKAGE: str = "package" - PDF: str = "pdf" - PLAINTEXT: str = "plaintext" - PRESENTATION: str = "presentation" - PROGRAM: str = "program" - SHORTCUT: str = "shortcut" - SOURCE_ENGINE: str = "source_engine" - SPREADSHEET: str = "spreadsheet" - TEXT: str = "text" - VIDEO: str = "video" + ADOBE_PHOTOSHOP = "adobe_photoshop" + AFFINITY_PHOTO = "affinity_photo" + ARCHIVE = "archive" + AUDIO_MIDI = "audio_midi" + AUDIO = "audio" + BLENDER = "blender" + DATABASE = "database" + DISK_IMAGE = "disk_image" + DOCUMENT = "document" + EBOOK = "ebook" + FONT = "font" + IMAGE_ANIMATED = "image_animated" + IMAGE_RAW = "image_raw" + IMAGE_VECTOR = "image_vector" + IMAGE = "image" + INSTALLER = "installer" + MATERIAL = "material" + MODEL = "model" + OPEN_DOCUMENT = "open_document" + PACKAGE = "package" + PDF = "pdf" + PLAINTEXT = "plaintext" + PRESENTATION = "presentation" + PROGRAM = "program" + SHORTCUT = "shortcut" + SOURCE_ENGINE = "source_engine" + SPREADSHEET = "spreadsheet" + TEXT = "text" + VIDEO = "video" @dataclass(frozen=True) @@ -337,188 +337,188 @@ class MediaCategories: ".wmv", } - ADOBE_PHOTOSHOP_TYPES: MediaCategory = MediaCategory( + ADOBE_PHOTOSHOP_TYPES = MediaCategory( media_type=MediaType.ADOBE_PHOTOSHOP, extensions=_ADOBE_PHOTOSHOP_SET, is_iana=False, name="photoshop", ) - AFFINITY_PHOTO_TYPES: MediaCategory = MediaCategory( + AFFINITY_PHOTO_TYPES = MediaCategory( media_type=MediaType.AFFINITY_PHOTO, extensions=_AFFINITY_PHOTO_SET, is_iana=False, name="affinity photo", ) - ARCHIVE_TYPES: MediaCategory = MediaCategory( + ARCHIVE_TYPES = MediaCategory( media_type=MediaType.ARCHIVE, extensions=_ARCHIVE_SET, is_iana=False, name="archive", ) - AUDIO_MIDI_TYPES: MediaCategory = MediaCategory( + AUDIO_MIDI_TYPES = MediaCategory( media_type=MediaType.AUDIO_MIDI, extensions=_AUDIO_MIDI_SET, is_iana=False, name="audio midi", ) - AUDIO_TYPES: MediaCategory = MediaCategory( + AUDIO_TYPES = MediaCategory( media_type=MediaType.AUDIO, extensions=_AUDIO_SET | _AUDIO_MIDI_SET, is_iana=True, name="audio", ) - BLENDER_TYPES: MediaCategory = MediaCategory( + BLENDER_TYPES = MediaCategory( media_type=MediaType.BLENDER, extensions=_BLENDER_SET, is_iana=False, name="blender", ) - DATABASE_TYPES: MediaCategory = MediaCategory( + DATABASE_TYPES = MediaCategory( media_type=MediaType.DATABASE, extensions=_DATABASE_SET, is_iana=False, name="database", ) - DISK_IMAGE_TYPES: MediaCategory = MediaCategory( + DISK_IMAGE_TYPES = MediaCategory( media_type=MediaType.DISK_IMAGE, extensions=_DISK_IMAGE_SET, is_iana=False, name="disk image", ) - DOCUMENT_TYPES: MediaCategory = MediaCategory( + DOCUMENT_TYPES = MediaCategory( media_type=MediaType.DOCUMENT, extensions=_DOCUMENT_SET, is_iana=False, name="document", ) - EBOOK_TYPES: MediaCategory = MediaCategory( + EBOOK_TYPES = MediaCategory( media_type=MediaType.EBOOK, extensions=_EBOOK_SET, is_iana=False, name="ebook", ) - FONT_TYPES: MediaCategory = MediaCategory( + FONT_TYPES = MediaCategory( media_type=MediaType.FONT, extensions=_FONT_SET, is_iana=True, name="font", ) - IMAGE_ANIMATED_TYPES: MediaCategory = MediaCategory( + IMAGE_ANIMATED_TYPES = MediaCategory( media_type=MediaType.IMAGE_ANIMATED, extensions=_IMAGE_ANIMATED_SET, is_iana=False, name="animated image", ) - IMAGE_RAW_TYPES: MediaCategory = MediaCategory( + IMAGE_RAW_TYPES = MediaCategory( media_type=MediaType.IMAGE_RAW, extensions=_IMAGE_RAW_SET, is_iana=False, name="raw image", ) - IMAGE_VECTOR_TYPES: MediaCategory = MediaCategory( + IMAGE_VECTOR_TYPES = MediaCategory( media_type=MediaType.IMAGE_VECTOR, extensions=_IMAGE_VECTOR_SET, is_iana=False, name="vector image", ) - IMAGE_RASTER_TYPES: MediaCategory = MediaCategory( + IMAGE_RASTER_TYPES = MediaCategory( media_type=MediaType.IMAGE, extensions=_IMAGE_RASTER_SET, is_iana=False, name="raster image", ) - IMAGE_TYPES: MediaCategory = MediaCategory( + IMAGE_TYPES = MediaCategory( media_type=MediaType.IMAGE, extensions=_IMAGE_RASTER_SET | _IMAGE_RAW_SET | _IMAGE_VECTOR_SET, is_iana=True, name="image", ) - INSTALLER_TYPES: MediaCategory = MediaCategory( + INSTALLER_TYPES = MediaCategory( media_type=MediaType.INSTALLER, extensions=_INSTALLER_SET, is_iana=False, name="installer", ) - MATERIAL_TYPES: MediaCategory = MediaCategory( + MATERIAL_TYPES = MediaCategory( media_type=MediaType.MATERIAL, extensions=_MATERIAL_SET, is_iana=False, name="material", ) - MODEL_TYPES: MediaCategory = MediaCategory( + MODEL_TYPES = MediaCategory( media_type=MediaType.MODEL, extensions=_MODEL_SET, is_iana=True, name="model", ) - OPEN_DOCUMENT_TYPES: MediaCategory = MediaCategory( + OPEN_DOCUMENT_TYPES = MediaCategory( media_type=MediaType.OPEN_DOCUMENT, extensions=_OPEN_DOCUMENT_SET, is_iana=False, name="open document", ) - PACKAGE_TYPES: MediaCategory = MediaCategory( + PACKAGE_TYPES = MediaCategory( media_type=MediaType.PACKAGE, extensions=_PACKAGE_SET, is_iana=False, name="package", ) - PDF_TYPES: MediaCategory = MediaCategory( + PDF_TYPES = MediaCategory( media_type=MediaType.PDF, extensions=_PDF_SET, is_iana=False, name="pdf", ) - PLAINTEXT_TYPES: MediaCategory = MediaCategory( + PLAINTEXT_TYPES = MediaCategory( media_type=MediaType.PLAINTEXT, extensions=_PLAINTEXT_SET, is_iana=False, name="plaintext", ) - PRESENTATION_TYPES: MediaCategory = MediaCategory( + PRESENTATION_TYPES = MediaCategory( media_type=MediaType.PRESENTATION, extensions=_PRESENTATION_SET, is_iana=False, name="presentation", ) - PROGRAM_TYPES: MediaCategory = MediaCategory( + PROGRAM_TYPES = MediaCategory( media_type=MediaType.PROGRAM, extensions=_PROGRAM_SET, is_iana=False, name="program", ) - SHORTCUT_TYPES: MediaCategory = MediaCategory( + SHORTCUT_TYPES = MediaCategory( media_type=MediaType.SHORTCUT, extensions=_SHORTCUT_SET, is_iana=False, name="shortcut", ) - SOURCE_ENGINE_TYPES: MediaCategory = MediaCategory( + SOURCE_ENGINE_TYPES = MediaCategory( media_type=MediaType.SOURCE_ENGINE, extensions=_SOURCE_ENGINE_SET, is_iana=False, name="source engine", ) - SPREADSHEET_TYPES: MediaCategory = MediaCategory( + SPREADSHEET_TYPES = MediaCategory( media_type=MediaType.SPREADSHEET, extensions=_SPREADSHEET_SET, is_iana=False, name="spreadsheet", ) - TEXT_TYPES: MediaCategory = MediaCategory( + TEXT_TYPES = MediaCategory( media_type=MediaType.TEXT, extensions=_DOCUMENT_SET | _PLAINTEXT_SET, is_iana=True, name="text", ) - VIDEO_TYPES: MediaCategory = MediaCategory( + VIDEO_TYPES = MediaCategory( media_type=MediaType.VIDEO, extensions=_VIDEO_SET, is_iana=True, name="video", ) - ALL_CATEGORIES: list[MediaCategory] = [ + ALL_CATEGORIES = [ ADOBE_PHOTOSHOP_TYPES, AFFINITY_PHOTO_TYPES, ARCHIVE_TYPES, diff --git a/src/tagstudio/core/palette.py b/src/tagstudio/core/palette.py index 356fdcc1..4e26bde7 100644 --- a/src/tagstudio/core/palette.py +++ b/src/tagstudio/core/palette.py @@ -1,12 +1,14 @@ # Copyright (C) 2025 Travis Abendshien (CyanVoxel). # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + import traceback from enum import IntEnum from typing import Any import structlog -from src.core.library.alchemy.enums import TagColorEnum + +from tagstudio.core.library.alchemy.enums import TagColorEnum logger = structlog.get_logger(__name__) diff --git a/src/tagstudio/core/query_lang/__init__.py b/src/tagstudio/core/query_lang/__init__.py deleted file mode 100644 index 9f1afca1..00000000 --- a/src/tagstudio/core/query_lang/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from src.core.query_lang.ast import ( # noqa - AST, - ANDList, - BaseVisitor, - Constraint, - ConstraintType, - ORList, - Property, -) -from src.core.query_lang.parser import Parser # noqa -from src.core.query_lang.util import ParsingError # noqa diff --git a/src/tagstudio/core/query_lang/parser.py b/src/tagstudio/core/query_lang/parser.py index 7194faad..8566d5db 100644 --- a/src/tagstudio/core/query_lang/parser.py +++ b/src/tagstudio/core/query_lang/parser.py @@ -1,6 +1,14 @@ -from .ast import AST, ANDList, Constraint, Not, ORList, Property -from .tokenizer import ConstraintType, Token, Tokenizer, TokenType -from .util import ParsingError +from tagstudio.core.query_lang.ast import ( + AST, + ANDList, + Constraint, + ConstraintType, + Not, + ORList, + Property, +) +from tagstudio.core.query_lang.tokenizer import Token, Tokenizer, TokenType +from tagstudio.core.query_lang.util import ParsingError class Parser: diff --git a/src/tagstudio/core/query_lang/tokenizer.py b/src/tagstudio/core/query_lang/tokenizer.py index cc87664c..07c40e7d 100644 --- a/src/tagstudio/core/query_lang/tokenizer.py +++ b/src/tagstudio/core/query_lang/tokenizer.py @@ -1,8 +1,8 @@ from enum import Enum from typing import Any -from .ast import ConstraintType -from .util import ParsingError +from tagstudio.core.query_lang.ast import ConstraintType +from tagstudio.core.query_lang.util import ParsingError class TokenType(Enum): diff --git a/src/tagstudio/core/ts_core.py b/src/tagstudio/core/ts_core.py index 3d412d9e..05958bda 100644 --- a/src/tagstudio/core/ts_core.py +++ b/src/tagstudio/core/ts_core.py @@ -7,10 +7,11 @@ import json from pathlib import Path -from src.core.constants import TS_FOLDER_NAME -from src.core.library import Entry, Library -from src.core.library.alchemy.fields import _FieldID -from src.core.utils.missing_files import logger +from tagstudio.core.constants import TS_FOLDER_NAME +from tagstudio.core.library.alchemy.fields import _FieldID +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.library.alchemy.models import Entry +from tagstudio.core.utils.missing_files import logger class TagStudioCore: diff --git a/src/tagstudio/core/utils/dupe_files.py b/src/tagstudio/core/utils/dupe_files.py index 3c1d55d1..673cefe8 100644 --- a/src/tagstudio/core/utils/dupe_files.py +++ b/src/tagstudio/core/utils/dupe_files.py @@ -3,8 +3,10 @@ from dataclasses import dataclass, field from pathlib import Path import structlog -from src.core.library import Entry, Library -from src.core.library.alchemy.enums import FilterState + +from tagstudio.core.library.alchemy.enums import FilterState +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.library.alchemy.models import Entry logger = structlog.get_logger() diff --git a/src/tagstudio/core/utils/missing_files.py b/src/tagstudio/core/utils/missing_files.py index bb180849..a17379e4 100644 --- a/src/tagstudio/core/utils/missing_files.py +++ b/src/tagstudio/core/utils/missing_files.py @@ -3,8 +3,10 @@ from dataclasses import dataclass, field from pathlib import Path import structlog -from src.core.library import Entry, Library -from src.core.utils.refresh_dir import GLOBAL_IGNORE_SET + +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.library.alchemy.models import Entry +from tagstudio.core.utils.refresh_dir import GLOBAL_IGNORE_SET logger = structlog.get_logger() diff --git a/src/tagstudio/core/utils/refresh_dir.py b/src/tagstudio/core/utils/refresh_dir.py index 6f3aa151..ddbff94e 100644 --- a/src/tagstudio/core/utils/refresh_dir.py +++ b/src/tagstudio/core/utils/refresh_dir.py @@ -5,8 +5,10 @@ from pathlib import Path from time import time import structlog -from src.core.constants import TS_FOLDER_NAME -from src.core.library import Entry, Library + +from tagstudio.core.constants import TS_FOLDER_NAME +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.library.alchemy.models import Entry logger = structlog.get_logger(__name__) diff --git a/src/tagstudio/main.py b/src/tagstudio/main.py index ceceb077..18ebc3c1 100755 --- a/src/tagstudio/main.py +++ b/src/tagstudio/main.py @@ -3,6 +3,7 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + """TagStudio launcher.""" import argparse @@ -10,7 +11,8 @@ import logging import traceback import structlog -from src.qt.ts_qt import QtDriver + +from tagstudio.qt.ts_qt import QtDriver structlog.configure( wrapper_class=structlog.make_filtering_bound_logger(logging.INFO), @@ -55,9 +57,8 @@ def main(): help="User interface option for TagStudio. Options: qt, cli (Default: qt)", ) args = parser.parse_args() - from src.core.library import alchemy as backend - driver = QtDriver(backend, args) + driver = QtDriver(args) ui_name = "Qt" # Run the chosen frontend driver. diff --git a/src/tagstudio/qt/cache_manager.py b/src/tagstudio/qt/cache_manager.py index 1024f3ff..cfd59454 100644 --- a/src/tagstudio/qt/cache_manager.py +++ b/src/tagstudio/qt/cache_manager.py @@ -9,15 +9,14 @@ from datetime import datetime as dt from pathlib import Path import structlog -from PIL import ( - Image, -) -from src.core.constants import THUMB_CACHE_NAME, TS_FOLDER_NAME -from src.core.singleton import Singleton +from PIL import Image + +from tagstudio.core.constants import THUMB_CACHE_NAME, TS_FOLDER_NAME +from tagstudio.core.singleton import Singleton # Only import for type checking/autocompletion, will not be imported at runtime. if typing.TYPE_CHECKING: - from src.core.library import Library + from tagstudio.core.library import Library logger = structlog.get_logger(__name__) diff --git a/src/tagstudio/qt/flowlayout.py b/src/tagstudio/qt/flowlayout.py index 046b93c9..d1c7b0a4 100644 --- a/src/tagstudio/qt/flowlayout.py +++ b/src/tagstudio/qt/flowlayout.py @@ -2,6 +2,7 @@ # Copyright (C) 2022 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + """PySide6 port of the widgets/layouts/flowlayout example from Qt v6.x.""" from PySide6.QtCore import QMargins, QPoint, QRect, QSize, Qt diff --git a/src/tagstudio/qt/helpers/blender_thumbnailer.py b/src/tagstudio/qt/helpers/blender_thumbnailer.py index 2df0a350..012c1503 100644 --- a/src/tagstudio/qt/helpers/blender_thumbnailer.py +++ b/src/tagstudio/qt/helpers/blender_thumbnailer.py @@ -29,10 +29,7 @@ import os import struct from io import BufferedReader -from PIL import ( - Image, - ImageOps, -) +from PIL import Image, ImageOps def blend_extract_thumb(path): diff --git a/src/tagstudio/qt/helpers/color_overlay.py b/src/tagstudio/qt/helpers/color_overlay.py index 4ef8b192..0050447f 100644 --- a/src/tagstudio/qt/helpers/color_overlay.py +++ b/src/tagstudio/qt/helpers/color_overlay.py @@ -6,7 +6,8 @@ from PIL import Image from PySide6.QtCore import Qt from PySide6.QtGui import QGuiApplication -from src.qt.helpers.gradient import linear_gradient + +from tagstudio.qt.helpers.gradient import linear_gradient # TODO: Consolidate the built-in QT theme values with the values # here, in enums.py, and in palette.py. diff --git a/src/tagstudio/qt/helpers/file_deleter.py b/src/tagstudio/qt/helpers/file_deleter.py index a8e50e17..dce7168f 100644 --- a/src/tagstudio/qt/helpers/file_deleter.py +++ b/src/tagstudio/qt/helpers/file_deleter.py @@ -2,6 +2,7 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + import logging from pathlib import Path diff --git a/src/tagstudio/qt/helpers/file_opener.py b/src/tagstudio/qt/helpers/file_opener.py index 8650be0b..39316941 100644 --- a/src/tagstudio/qt/helpers/file_opener.py +++ b/src/tagstudio/qt/helpers/file_opener.py @@ -11,7 +11,8 @@ from pathlib import Path import structlog from PySide6.QtCore import Qt from PySide6.QtWidgets import QLabel -from src.qt.helpers.silent_popen import silent_Popen + +from tagstudio.qt.helpers.silent_popen import silent_Popen logger = structlog.get_logger(__name__) diff --git a/src/tagstudio/qt/helpers/file_tester.py b/src/tagstudio/qt/helpers/file_tester.py index 022ac191..6db0b37b 100644 --- a/src/tagstudio/qt/helpers/file_tester.py +++ b/src/tagstudio/qt/helpers/file_tester.py @@ -6,7 +6,8 @@ from pathlib import Path import ffmpeg -from src.qt.helpers.vendored.ffmpeg import _probe + +from tagstudio.qt.helpers.vendored.ffmpeg import _probe def is_readable_video(filepath: Path | str): diff --git a/src/tagstudio/qt/helpers/function_iterator.py b/src/tagstudio/qt/helpers/function_iterator.py index a07f16da..9f213790 100644 --- a/src/tagstudio/qt/helpers/function_iterator.py +++ b/src/tagstudio/qt/helpers/function_iterator.py @@ -1,6 +1,8 @@ # Copyright (C) 2024 Travis Abendshien (CyanVoxel). # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + from collections.abc import Callable from PySide6.QtCore import QObject, Signal diff --git a/src/tagstudio/qt/helpers/gradient.py b/src/tagstudio/qt/helpers/gradient.py index b407b487..5c9a2657 100644 --- a/src/tagstudio/qt/helpers/gradient.py +++ b/src/tagstudio/qt/helpers/gradient.py @@ -2,6 +2,7 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + from PIL import Image diff --git a/src/tagstudio/qt/helpers/image_effects.py b/src/tagstudio/qt/helpers/image_effects.py index 139d5274..ca764744 100644 --- a/src/tagstudio/qt/helpers/image_effects.py +++ b/src/tagstudio/qt/helpers/image_effects.py @@ -2,6 +2,7 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + import numpy as np from PIL import Image diff --git a/src/tagstudio/qt/helpers/qbutton_wrapper.py b/src/tagstudio/qt/helpers/qbutton_wrapper.py index 31d2ad81..96b9b6d3 100644 --- a/src/tagstudio/qt/helpers/qbutton_wrapper.py +++ b/src/tagstudio/qt/helpers/qbutton_wrapper.py @@ -2,6 +2,7 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + from PySide6.QtWidgets import QPushButton diff --git a/src/tagstudio/qt/helpers/rounded_pixmap_style.py b/src/tagstudio/qt/helpers/rounded_pixmap_style.py index 36c33c79..47c2b580 100644 --- a/src/tagstudio/qt/helpers/rounded_pixmap_style.py +++ b/src/tagstudio/qt/helpers/rounded_pixmap_style.py @@ -4,10 +4,9 @@ # https://creativecommons.org/licenses/by-sa/4.0/ # Modified for TagStudio: https://github.com/CyanVoxel/TagStudio + from PySide6.QtGui import QBrush, QColor, QPainter, QPixmap -from PySide6.QtWidgets import ( - QProxyStyle, -) +from PySide6.QtWidgets import QProxyStyle class RoundedPixmapStyle(QProxyStyle): diff --git a/src/tagstudio/qt/helpers/silent_popen.py b/src/tagstudio/qt/helpers/silent_popen.py index 84beccde..ad251c41 100644 --- a/src/tagstudio/qt/helpers/silent_popen.py +++ b/src/tagstudio/qt/helpers/silent_popen.py @@ -1,6 +1,8 @@ # Copyright (C) 2024 Travis Abendshien (CyanVoxel). # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + import os import subprocess import sys diff --git a/src/tagstudio/qt/helpers/text_wrapper.py b/src/tagstudio/qt/helpers/text_wrapper.py index 073f6e15..401e2f89 100644 --- a/src/tagstudio/qt/helpers/text_wrapper.py +++ b/src/tagstudio/qt/helpers/text_wrapper.py @@ -2,6 +2,7 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + from PIL import Image, ImageDraw, ImageFont diff --git a/src/tagstudio/qt/helpers/vendored/ffmpeg.py b/src/tagstudio/qt/helpers/vendored/ffmpeg.py index 13f9e2a3..efeefa07 100644 --- a/src/tagstudio/qt/helpers/vendored/ffmpeg.py +++ b/src/tagstudio/qt/helpers/vendored/ffmpeg.py @@ -9,7 +9,8 @@ import subprocess import ffmpeg import structlog -from src.qt.helpers.silent_popen import silent_Popen + +from tagstudio.qt.helpers.silent_popen import silent_Popen logger = structlog.get_logger(__name__) diff --git a/src/tagstudio/qt/helpers/vendored/pydub/audio_segment.py b/src/tagstudio/qt/helpers/vendored/pydub/audio_segment.py index be507058..29a590d5 100644 --- a/src/tagstudio/qt/helpers/vendored/pydub/audio_segment.py +++ b/src/tagstudio/qt/helpers/vendored/pydub/audio_segment.py @@ -17,7 +17,8 @@ from tempfile import NamedTemporaryFile from pydub.logging_utils import log_conversion, log_subprocess_output from pydub.utils import fsdecode -from src.qt.helpers.vendored.ffmpeg import FFMPEG_CMD + +from tagstudio.qt.helpers.vendored.ffmpeg import FFMPEG_CMD try: from itertools import izip @@ -40,8 +41,9 @@ from pydub.utils import ( get_array_type, ratio_to_db, ) -from src.qt.helpers.silent_popen import silent_Popen -from src.qt.helpers.vendored.pydub.utils import _mediainfo_json + +from tagstudio.qt.helpers.silent_popen import silent_Popen +from tagstudio.qt.helpers.vendored.pydub.utils import _mediainfo_json basestring = str xrange = range @@ -361,11 +363,11 @@ class _AudioSegment: """Permit use of sum() builtin with an iterable of AudioSegments.""" if rarg == 0: return self - raise TypeError("Gains must be the second addend after the " "AudioSegment") + raise TypeError("Gains must be the second addend after the AudioSegment") def __sub__(self, arg): if isinstance(arg, _AudioSegment): - raise TypeError("AudioSegment objects can't be subtracted from " "each other") + raise TypeError("AudioSegment objects can't be subtracted from each other") else: return self.apply_gain(-arg) @@ -1345,8 +1347,7 @@ class _AudioSegment: """ if None not in [duration, end, start]: raise TypeError( - 'Only two of the three arguments, "start", ' - '"end", and "duration" may be specified' + 'Only two of the three arguments, "start", "end", and "duration" may be specified' ) # no fade == the same audio diff --git a/src/tagstudio/qt/helpers/vendored/pydub/utils.py b/src/tagstudio/qt/helpers/vendored/pydub/utils.py index 0a8c873f..a325551d 100644 --- a/src/tagstudio/qt/helpers/vendored/pydub/utils.py +++ b/src/tagstudio/qt/helpers/vendored/pydub/utils.py @@ -7,8 +7,9 @@ from pydub.utils import ( fsdecode, get_extra_info, ) -from src.qt.helpers.silent_popen import silent_Popen -from src.qt.helpers.vendored.ffmpeg import FFPROBE_CMD + +from tagstudio.qt.helpers.silent_popen import silent_Popen +from tagstudio.qt.helpers.vendored.ffmpeg import FFPROBE_CMD def _mediainfo_json(filepath, read_ahead_limit=-1): diff --git a/src/tagstudio/qt/main_window.py b/src/tagstudio/qt/main_window.py index d51a8afd..485a7a1c 100644 --- a/src/tagstudio/qt/main_window.py +++ b/src/tagstudio/qt/main_window.py @@ -5,21 +5,34 @@ import logging import typing -from PySide6.QtCore import (QCoreApplication, QMetaObject, QRect,QSize, Qt, QStringListModel) -from PySide6.QtGui import QFont -from PySide6.QtWidgets import (QComboBox, QFrame, QGridLayout, - QHBoxLayout, QVBoxLayout, QLayout, QLineEdit, QMainWindow, - QPushButton, QScrollArea, QSizePolicy, - QStatusBar, QWidget, QSplitter, QCheckBox, - QSpacerItem, QCompleter) -from src.qt.pagination import Pagination -from src.qt.widgets.landing import LandingWidget -from src.qt.translations import Translations +from PySide6.QtCore import QMetaObject, QRect, QSize, QStringListModel, Qt +from PySide6.QtWidgets import ( + QComboBox, + QCompleter, + QFrame, + QGridLayout, + QHBoxLayout, + QLayout, + QLineEdit, + QMainWindow, + QPushButton, + QScrollArea, + QSizePolicy, + QSpacerItem, + QSplitter, + QStatusBar, + QVBoxLayout, + QWidget, +) + +from tagstudio.qt.pagination import Pagination +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.landing import LandingWidget # Only import for type checking/autocompletion, will not be imported at runtime. if typing.TYPE_CHECKING: - from src.qt.ts_qt import QtDriver + from tagstudio.qt.ts_qt import QtDriver logging.basicConfig(format="%(message)s", level=logging.INFO) @@ -192,4 +205,4 @@ class Ui_MainWindow(QMainWindow): self.landing_widget.setHidden(True) self.landing_widget.set_status_label("") self.scrollArea.setHidden(False) - \ No newline at end of file + diff --git a/src/tagstudio/qt/modals/about.py b/src/tagstudio/qt/modals/about.py index 87264f6c..cb0a9d86 100644 --- a/src/tagstudio/qt/modals/about.py +++ b/src/tagstudio/qt/modals/about.py @@ -7,11 +7,12 @@ from PIL import ImageQt from PySide6.QtCore import Qt from PySide6.QtGui import QPixmap from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget -from src.core.constants import VERSION, VERSION_BRANCH -from src.core.palette import ColorType, UiColor, get_ui_color -from src.qt.modals.ffmpeg_checker import FfmpegChecker -from src.qt.resource_manager import ResourceManager -from src.qt.translations import Translations + +from tagstudio.core.constants import VERSION, VERSION_BRANCH +from tagstudio.core.palette import ColorType, UiColor, get_ui_color +from tagstudio.qt.modals.ffmpeg_checker import FfmpegChecker +from tagstudio.qt.resource_manager import ResourceManager +from tagstudio.qt.translations import Translations class AboutModal(QWidget): @@ -31,7 +32,7 @@ class AboutModal(QWidget): self.logo_widget = QLabel() self.logo_widget.setObjectName("logo") - self.logo_pixmap = QPixmap.fromImage(ImageQt.ImageQt(self.rm.get("logo"))) + self.logo_pixmap = QPixmap.fromImage(ImageQt.ImageQt(self.rm.get("icon"))) self.logo_pixmap = self.logo_pixmap.scaledToWidth( 128, Qt.TransformationMode.SmoothTransformation ) diff --git a/src/tagstudio/qt/modals/add_field.py b/src/tagstudio/qt/modals/add_field.py index ee5d3c25..7395a1a4 100644 --- a/src/tagstudio/qt/modals/add_field.py +++ b/src/tagstudio/qt/modals/add_field.py @@ -3,6 +3,8 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio +from typing import override + import structlog from PySide6 import QtCore, QtGui from PySide6.QtCore import Qt, Signal @@ -15,8 +17,9 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) -from src.core.library import Library -from src.qt.translations import Translations + +from tagstudio.core.library.alchemy.library import Library +from tagstudio.qt.translations import Translations logger = structlog.get_logger(__name__) @@ -40,7 +43,7 @@ class AddFieldModal(QWidget): self.title_widget = QLabel(Translations["library.field.add"]) self.title_widget.setObjectName("fieldTitle") self.title_widget.setWordWrap(True) - self.title_widget.setStyleSheet("font-weight:bold;" "font-size:14px;" "padding-top: 6px;") + self.title_widget.setStyleSheet("font-weight:bold;font-size:14px;padding-top: 6px;") self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) self.list_widget = QListWidget() @@ -80,6 +83,7 @@ class AddFieldModal(QWidget): super().show() + @override def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802 if event.key() == QtCore.Qt.Key.Key_Escape: self.cancel_button.click() diff --git a/src/tagstudio/qt/modals/build_color.py b/src/tagstudio/qt/modals/build_color.py index e9abcbf8..67d86c86 100644 --- a/src/tagstudio/qt/modals/build_color.py +++ b/src/tagstudio/qt/modals/build_color.py @@ -19,20 +19,20 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) -from src.core import palette -from src.core.library import Library -from src.core.library.alchemy.enums import TagColorEnum -from src.core.library.alchemy.library import slugify -from src.core.library.alchemy.models import TagColorGroup -from src.core.palette import ColorType, UiColor, get_tag_color, get_ui_color -from src.qt.translations import Translations -from src.qt.widgets.panel import PanelWidget -from src.qt.widgets.tag import ( + +from tagstudio.core import palette +from tagstudio.core.library.alchemy.enums import TagColorEnum +from tagstudio.core.library.alchemy.library import Library, slugify +from tagstudio.core.library.alchemy.models import TagColorGroup +from tagstudio.core.palette import ColorType, UiColor, get_tag_color, get_ui_color +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.panel import PanelWidget +from tagstudio.qt.widgets.tag import ( get_border_color, get_highlight_color, get_text_color, ) -from src.qt.widgets.tag_color_preview import TagColorPreview +from tagstudio.qt.widgets.tag_color_preview import TagColorPreview logger = structlog.get_logger(__name__) @@ -317,7 +317,7 @@ class BuildColorPanel(PanelWidget): def update_preview_text(self): self.preview_button.button.setText( - f"{self.name_field.text().strip() or Translations["color.placeholder"]} " + f"{self.name_field.text().strip() or Translations['color.placeholder']} " f"({self.lib.get_namespace_name(self.color_group.namespace)})" ) self.preview_button.button.setMaximumWidth(self.preview_button.button.sizeHint().width()) @@ -333,7 +333,7 @@ class BuildColorPanel(PanelWidget): if suffix: try: suffix_num: int = int(suffix) - return self.no_collide(f"{split_slug[0]}-{suffix_num+1}") + return self.no_collide(f"{split_slug[0]}-{suffix_num + 1}") except ValueError: return self.no_collide(f"{slug}-2") else: diff --git a/src/tagstudio/qt/modals/build_namespace.py b/src/tagstudio/qt/modals/build_namespace.py index 6566d0e4..a358a7d9 100644 --- a/src/tagstudio/qt/modals/build_namespace.py +++ b/src/tagstudio/qt/modals/build_namespace.py @@ -8,19 +8,14 @@ from uuid import uuid4 import structlog from PySide6.QtCore import Qt, Signal -from PySide6.QtWidgets import ( - QLabel, - QLineEdit, - QVBoxLayout, - QWidget, -) -from src.core.constants import RESERVED_NAMESPACE_PREFIX -from src.core.library import Library -from src.core.library.alchemy.library import ReservedNamespaceError, slugify -from src.core.library.alchemy.models import Namespace -from src.core.palette import ColorType, UiColor, get_ui_color -from src.qt.translations import Translations -from src.qt.widgets.panel import PanelWidget +from PySide6.QtWidgets import QLabel, QLineEdit, QVBoxLayout, QWidget + +from tagstudio.core.constants import RESERVED_NAMESPACE_PREFIX +from tagstudio.core.library.alchemy.library import Library, ReservedNamespaceError, slugify +from tagstudio.core.library.alchemy.models import Namespace +from tagstudio.core.palette import ColorType, UiColor, get_ui_color +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.panel import PanelWidget logger = structlog.get_logger(__name__) @@ -145,7 +140,7 @@ class BuildNamespacePanel(PanelWidget): if suffix: try: suffix_num: int = int(suffix) - return self.no_collide(f"{split_slug[0]}-{suffix_num+1}") + return self.no_collide(f"{split_slug[0]}-{suffix_num + 1}") except ValueError: return self.no_collide(f"{slug}-2") else: diff --git a/src/tagstudio/qt/modals/build_tag.py b/src/tagstudio/qt/modals/build_tag.py index c9657b8f..42250ec7 100644 --- a/src/tagstudio/qt/modals/build_tag.py +++ b/src/tagstudio/qt/modals/build_tag.py @@ -4,11 +4,11 @@ import sys -from typing import cast +from typing import cast, override import structlog from PySide6.QtCore import Qt, Signal -from PySide6.QtGui import QColor +from PySide6.QtGui import QColor, QKeyEvent from PySide6.QtWidgets import ( QApplication, QButtonGroup, @@ -24,22 +24,23 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) -from src.core.library import Library, Tag -from src.core.library.alchemy.enums import TagColorEnum -from src.core.library.alchemy.models import TagColorGroup -from src.core.palette import ColorType, UiColor, get_tag_color, get_ui_color -from src.qt.modals.tag_color_selection import TagColorSelection -from src.qt.modals.tag_search import TagSearchPanel -from src.qt.translations import Translations -from src.qt.widgets.panel import PanelModal, PanelWidget -from src.qt.widgets.tag import ( + +from tagstudio.core.library.alchemy.enums import TagColorEnum +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.library.alchemy.models import Tag, TagColorGroup +from tagstudio.core.palette import ColorType, UiColor, get_tag_color, get_ui_color +from tagstudio.qt.modals.tag_color_selection import TagColorSelection +from tagstudio.qt.modals.tag_search import TagSearchPanel +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.panel import PanelModal, PanelWidget +from tagstudio.qt.widgets.tag import ( TagWidget, get_border_color, get_highlight_color, get_primary_color, get_text_color, ) -from src.qt.widgets.tag_color_preview import TagColorPreview +from tagstudio.qt.widgets.tag_color_preview import TagColorPreview logger = structlog.get_logger(__name__) @@ -54,19 +55,20 @@ class CustomTableItem(QLineEdit): def set_id(self, id): self.id = id - def keyPressEvent(self, event): # noqa: N802 - if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter: + @override + def keyPressEvent(self, arg__1: QKeyEvent): # noqa: N802 + if arg__1.key() == Qt.Key.Key_Return or arg__1.key() == Qt.Key.Key_Enter: self.on_return() - elif event.key() == Qt.Key.Key_Backspace and self.text().strip() == "": + elif arg__1.key() == Qt.Key.Key_Backspace and self.text().strip() == "": self.on_backspace() else: - super().keyPressEvent(event) + super().keyPressEvent(arg__1) class BuildTagPanel(PanelWidget): on_edit = Signal(Tag) - def __init__(self, library: Library, tag: Tag | None = None): + def __init__(self, library: Library, tag: Tag | None = None) -> None: super().__init__() self.lib = library self.tag: Tag # NOTE: This gets set at the end of the init. @@ -469,7 +471,7 @@ class BuildTagPanel(PanelWidget): def _update_new_alias_name_dict(self): for i in range(0, self.aliases_table.rowCount()): widget = self.aliases_table.cellWidget(i, 1) - self.new_alias_names[widget.id] = widget.text() # type: ignore + self.new_alias_names[widget.id] = widget.text() def _set_aliases(self): self._update_new_alias_name_dict() diff --git a/src/tagstudio/qt/modals/delete_unlinked.py b/src/tagstudio/qt/modals/delete_unlinked.py index a8a88102..f3902cce 100644 --- a/src/tagstudio/qt/modals/delete_unlinked.py +++ b/src/tagstudio/qt/modals/delete_unlinked.py @@ -2,20 +2,29 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + from typing import TYPE_CHECKING, override from PySide6 import QtCore, QtGui from PySide6.QtCore import Qt, QThreadPool, Signal from PySide6.QtGui import QStandardItem, QStandardItemModel -from PySide6.QtWidgets import QHBoxLayout, QLabel, QListView, QPushButton, QVBoxLayout, QWidget -from src.core.utils.missing_files import MissingRegistry -from src.qt.helpers.custom_runnable import CustomRunnable -from src.qt.translations import Translations -from src.qt.widgets.progress import ProgressWidget +from PySide6.QtWidgets import ( + QHBoxLayout, + QLabel, + QListView, + QPushButton, + QVBoxLayout, + QWidget, +) + +from tagstudio.core.utils.missing_files import MissingRegistry +from tagstudio.qt.helpers.custom_runnable import CustomRunnable +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.progress import ProgressWidget # Only import for type checking/autocompletion, will not be imported at runtime. if TYPE_CHECKING: - from src.qt.ts_qt import QtDriver + from tagstudio.qt.ts_qt import QtDriver class DeleteUnlinkedEntriesModal(QWidget): diff --git a/src/tagstudio/qt/modals/drop_import.py b/src/tagstudio/qt/modals/drop_import.py index fcc84178..495740e7 100644 --- a/src/tagstudio/qt/modals/drop_import.py +++ b/src/tagstudio/qt/modals/drop_import.py @@ -2,6 +2,7 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + import enum import shutil from pathlib import Path @@ -11,12 +12,20 @@ import structlog from PySide6 import QtCore, QtGui from PySide6.QtCore import Qt, QUrl from PySide6.QtGui import QStandardItem, QStandardItemModel -from PySide6.QtWidgets import QHBoxLayout, QLabel, QListView, QPushButton, QVBoxLayout, QWidget -from src.qt.translations import Translations -from src.qt.widgets.progress import ProgressWidget +from PySide6.QtWidgets import ( + QHBoxLayout, + QLabel, + QListView, + QPushButton, + QVBoxLayout, + QWidget, +) + +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.progress import ProgressWidget if TYPE_CHECKING: - from src.qt.ts_qt import QtDriver + from tagstudio.qt.ts_qt import QtDriver logger = structlog.get_logger(__name__) diff --git a/src/tagstudio/qt/modals/ffmpeg_checker.py b/src/tagstudio/qt/modals/ffmpeg_checker.py index fba494c6..287f4d3f 100644 --- a/src/tagstudio/qt/modals/ffmpeg_checker.py +++ b/src/tagstudio/qt/modals/ffmpeg_checker.py @@ -6,7 +6,8 @@ import structlog from PySide6.QtCore import Qt, QUrl from PySide6.QtGui import QDesktopServices from PySide6.QtWidgets import QMessageBox -from src.qt.helpers.vendored.ffmpeg import FFMPEG_CMD, FFPROBE_CMD + +from tagstudio.qt.helpers.vendored.ffmpeg import FFMPEG_CMD, FFPROBE_CMD logger = structlog.get_logger(__name__) diff --git a/src/tagstudio/qt/modals/file_extension.py b/src/tagstudio/qt/modals/file_extension.py index 8e9c2529..02b02b58 100644 --- a/src/tagstudio/qt/modals/file_extension.py +++ b/src/tagstudio/qt/modals/file_extension.py @@ -16,10 +16,11 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) -from src.core.enums import LibraryPrefs -from src.core.library import Library -from src.qt.translations import Translations -from src.qt.widgets.panel import PanelWidget + +from tagstudio.core.enums import LibraryPrefs +from tagstudio.core.library.alchemy.library import Library +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.panel import PanelWidget class FileExtensionItemDelegate(QStyledItemDelegate): diff --git a/src/tagstudio/qt/modals/fix_dupes.py b/src/tagstudio/qt/modals/fix_dupes.py index 78b57438..be8fb3de 100644 --- a/src/tagstudio/qt/modals/fix_dupes.py +++ b/src/tagstudio/qt/modals/fix_dupes.py @@ -7,15 +7,23 @@ from typing import TYPE_CHECKING, override from PySide6 import QtCore, QtGui from PySide6.QtCore import Qt -from PySide6.QtWidgets import QFileDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget -from src.core.library import Library -from src.core.utils.dupe_files import DupeRegistry -from src.qt.modals.mirror_entities import MirrorEntriesModal -from src.qt.translations import Translations +from PySide6.QtWidgets import ( + QFileDialog, + QHBoxLayout, + QLabel, + QPushButton, + QVBoxLayout, + QWidget, +) + +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.utils.dupe_files import DupeRegistry +from tagstudio.qt.modals.mirror_entities import MirrorEntriesModal +from tagstudio.qt.translations import Translations # Only import for type checking/autocompletion, will not be imported at runtime. if TYPE_CHECKING: - from src.qt.ts_qt import QtDriver + from tagstudio.qt.ts_qt import QtDriver class FixDupeFilesModal(QWidget): @@ -41,7 +49,7 @@ class FixDupeFilesModal(QWidget): self.dupe_count = QLabel() self.dupe_count.setObjectName("dupeCountLabel") - self.dupe_count.setStyleSheet("font-weight:bold;" "font-size:14px;" "") + self.dupe_count.setStyleSheet("font-weight:bold;font-size:14px;") self.dupe_count.setAlignment(Qt.AlignmentFlag.AlignCenter) self.file_label = QLabel(Translations["file.duplicates.dupeguru.no_file"]) diff --git a/src/tagstudio/qt/modals/fix_unlinked.py b/src/tagstudio/qt/modals/fix_unlinked.py index aa301644..23b7c4d2 100644 --- a/src/tagstudio/qt/modals/fix_unlinked.py +++ b/src/tagstudio/qt/modals/fix_unlinked.py @@ -8,17 +8,18 @@ from typing import TYPE_CHECKING, override from PySide6 import QtCore, QtGui from PySide6.QtCore import Qt from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget -from src.core.library import Library -from src.core.utils.missing_files import MissingRegistry -from src.qt.modals.delete_unlinked import DeleteUnlinkedEntriesModal -from src.qt.modals.merge_dupe_entries import MergeDuplicateEntries -from src.qt.modals.relink_unlinked import RelinkUnlinkedEntries -from src.qt.translations import Translations -from src.qt.widgets.progress import ProgressWidget + +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.utils.missing_files import MissingRegistry +from tagstudio.qt.modals.delete_unlinked import DeleteUnlinkedEntriesModal +from tagstudio.qt.modals.merge_dupe_entries import MergeDuplicateEntries +from tagstudio.qt.modals.relink_unlinked import RelinkUnlinkedEntries +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.progress import ProgressWidget # Only import for type checking/autocompletion, will not be imported at runtime. if TYPE_CHECKING: - from src.qt.ts_qt import QtDriver + from tagstudio.qt.ts_qt import QtDriver class FixUnlinkedEntriesModal(QWidget): @@ -44,12 +45,12 @@ class FixUnlinkedEntriesModal(QWidget): self.missing_count_label = QLabel() self.missing_count_label.setObjectName("missingCountLabel") - self.missing_count_label.setStyleSheet("font-weight:bold;" "font-size:14px;") + self.missing_count_label.setStyleSheet("font-weight:bold;font-size:14px;") self.missing_count_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.dupe_count_label = QLabel() self.dupe_count_label.setObjectName("dupeCountLabel") - self.dupe_count_label.setStyleSheet("font-weight:bold;" "font-size:14px;") + self.dupe_count_label.setStyleSheet("font-weight:bold;font-size:14px;") self.dupe_count_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.refresh_unlinked_button = QPushButton(Translations["entries.unlinked.refresh_all"]) diff --git a/src/tagstudio/qt/modals/folders_to_tags.py b/src/tagstudio/qt/modals/folders_to_tags.py index fc045158..11fb3b33 100644 --- a/src/tagstudio/qt/modals/folders_to_tags.py +++ b/src/tagstudio/qt/modals/folders_to_tags.py @@ -20,15 +20,17 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) -from src.core.constants import TAG_ARCHIVED, TAG_FAVORITE -from src.core.library import Library, Tag -from src.core.library.alchemy.enums import TagColorEnum -from src.core.palette import ColorType, get_tag_color -from src.qt.flowlayout import FlowLayout -from src.qt.translations import Translations + +from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE +from tagstudio.core.library.alchemy.enums import TagColorEnum +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.library.alchemy.models import Tag +from tagstudio.core.palette import ColorType, get_tag_color +from tagstudio.qt.flowlayout import FlowLayout +from tagstudio.qt.translations import Translations if TYPE_CHECKING: - from src.qt.ts_qt import QtDriver + from tagstudio.qt.ts_qt import QtDriver logger = structlog.get_logger(__name__) @@ -175,7 +177,7 @@ class FoldersToTagsModal(QWidget): self.title_widget = QLabel(Translations["folders_to_tags.title"]) self.title_widget.setObjectName("title") self.title_widget.setWordWrap(True) - self.title_widget.setStyleSheet("font-weight:bold;" "font-size:14px;" "padding-top: 6px") + self.title_widget.setStyleSheet("font-weight:bold;font-size:14px;padding-top: 6px") self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) self.desc_widget = QLabel() diff --git a/src/tagstudio/qt/modals/merge_dupe_entries.py b/src/tagstudio/qt/modals/merge_dupe_entries.py index cd9baed8..d999f4ea 100644 --- a/src/tagstudio/qt/modals/merge_dupe_entries.py +++ b/src/tagstudio/qt/modals/merge_dupe_entries.py @@ -2,17 +2,19 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + import typing from PySide6.QtCore import QObject, Signal -from src.core.library import Library -from src.core.utils.dupe_files import DupeRegistry -from src.qt.translations import Translations -from src.qt.widgets.progress import ProgressWidget + +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.utils.dupe_files import DupeRegistry +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.progress import ProgressWidget # Only import for type checking/autocompletion, will not be imported at runtime. if typing.TYPE_CHECKING: - from src.qt.ts_qt import QtDriver + from tagstudio.qt.ts_qt import QtDriver class MergeDuplicateEntries(QObject): diff --git a/src/tagstudio/qt/modals/mirror_entities.py b/src/tagstudio/qt/modals/mirror_entities.py index 4b7390e4..198b2aea 100644 --- a/src/tagstudio/qt/modals/mirror_entities.py +++ b/src/tagstudio/qt/modals/mirror_entities.py @@ -9,13 +9,14 @@ from time import sleep from PySide6.QtCore import Qt, Signal from PySide6.QtGui import QStandardItem, QStandardItemModel from PySide6.QtWidgets import QHBoxLayout, QLabel, QListView, QPushButton, QVBoxLayout, QWidget -from src.core.utils.dupe_files import DupeRegistry -from src.qt.translations import Translations -from src.qt.widgets.progress import ProgressWidget + +from tagstudio.core.utils.dupe_files import DupeRegistry +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.progress import ProgressWidget # Only import for type checking/autocompletion, will not be imported at runtime. if typing.TYPE_CHECKING: - from src.qt.ts_qt import QtDriver + from tagstudio.qt.ts_qt import QtDriver class MirrorEntriesModal(QWidget): diff --git a/src/tagstudio/qt/modals/relink_unlinked.py b/src/tagstudio/qt/modals/relink_unlinked.py index 7fec73aa..8bd3ea81 100644 --- a/src/tagstudio/qt/modals/relink_unlinked.py +++ b/src/tagstudio/qt/modals/relink_unlinked.py @@ -4,9 +4,10 @@ from PySide6.QtCore import QObject, Signal -from src.core.utils.missing_files import MissingRegistry -from src.qt.translations import Translations -from src.qt.widgets.progress import ProgressWidget + +from tagstudio.core.utils.missing_files import MissingRegistry +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.progress import ProgressWidget class RelinkUnlinkedEntries(QObject): diff --git a/src/tagstudio/qt/modals/settings_panel.py b/src/tagstudio/qt/modals/settings_panel.py index 476f3cf4..c400c4a0 100644 --- a/src/tagstudio/qt/modals/settings_panel.py +++ b/src/tagstudio/qt/modals/settings_panel.py @@ -5,9 +5,10 @@ from PySide6.QtCore import Qt from PySide6.QtWidgets import QComboBox, QFormLayout, QLabel, QVBoxLayout, QWidget -from src.core.enums import SettingItems -from src.qt.translations import Translations -from src.qt.widgets.panel import PanelWidget + +from tagstudio.core.enums import SettingItems +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.panel import PanelWidget class SettingsPanel(PanelWidget): diff --git a/src/tagstudio/qt/modals/tag_color_manager.py b/src/tagstudio/qt/modals/tag_color_manager.py index ecd6f625..3f2fe0af 100644 --- a/src/tagstudio/qt/modals/tag_color_manager.py +++ b/src/tagstudio/qt/modals/tag_color_manager.py @@ -19,19 +19,20 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) -from src.core.constants import RESERVED_NAMESPACE_PREFIX -from src.core.enums import Theme -from src.qt.modals.build_namespace import BuildNamespacePanel -from src.qt.translations import Translations -from src.qt.widgets.color_box import ColorBoxWidget -from src.qt.widgets.fields import FieldContainer -from src.qt.widgets.panel import PanelModal + +from tagstudio.core.constants import RESERVED_NAMESPACE_PREFIX +from tagstudio.core.enums import Theme +from tagstudio.qt.modals.build_namespace import BuildNamespacePanel +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.color_box import ColorBoxWidget +from tagstudio.qt.widgets.fields import FieldContainer +from tagstudio.qt.widgets.panel import PanelModal logger = structlog.get_logger(__name__) # Only import for type checking/autocompletion, will not be imported at runtime. if TYPE_CHECKING: - from src.qt.ts_qt import QtDriver + from tagstudio.qt.ts_qt import QtDriver class TagColorManager(QWidget): @@ -60,7 +61,7 @@ class TagColorManager(QWidget): self.title_label = QLabel() self.title_label.setObjectName("titleLabel") self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.title_label.setText(f"

{Translations["color_manager.title"]}

") + self.title_label.setText(f"

{Translations['color_manager.title']}

") self.scroll_layout = QVBoxLayout() self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop) @@ -81,7 +82,7 @@ class TagColorManager(QWidget): self.scroll_area.setFrameShape(QFrame.Shape.NoFrame) self.scroll_area.setStyleSheet( - "QWidget#entryScrollContainer{" f"background:{panel_bg_color};" "border-radius:6px;" "}" + f"QWidget#entryScrollContainer{{background:{panel_bg_color};border-radius:6px;}}" ) self.scroll_area.setWidget(scroll_container) @@ -108,7 +109,6 @@ class TagColorManager(QWidget): self.root_layout.addWidget(self.title_label) self.root_layout.addWidget(self.scroll_area) self.root_layout.addWidget(self.button_container) - logger.info(self.root_layout.dumpObjectTree()) def setup_color_groups(self): all_default = True diff --git a/src/tagstudio/qt/modals/tag_color_selection.py b/src/tagstudio/qt/modals/tag_color_selection.py index 0d647fa2..70472c93 100644 --- a/src/tagstudio/qt/modals/tag_color_selection.py +++ b/src/tagstudio/qt/modals/tag_color_selection.py @@ -17,14 +17,15 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) -from src.core.library import Library -from src.core.library.alchemy.enums import TagColorEnum -from src.core.library.alchemy.models import TagColorGroup -from src.core.palette import ColorType, get_tag_color -from src.qt.flowlayout import FlowLayout -from src.qt.translations import Translations -from src.qt.widgets.panel import PanelWidget -from src.qt.widgets.tag import ( + +from tagstudio.core.library.alchemy.enums import TagColorEnum +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.library.alchemy.models import TagColorGroup +from tagstudio.core.palette import ColorType, get_tag_color +from tagstudio.qt.flowlayout import FlowLayout +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.panel import PanelWidget +from tagstudio.qt.widgets.tag import ( get_border_color, get_highlight_color, get_text_color, diff --git a/src/tagstudio/qt/modals/tag_database.py b/src/tagstudio/qt/modals/tag_database.py index 293b9c4e..423df993 100644 --- a/src/tagstudio/qt/modals/tag_database.py +++ b/src/tagstudio/qt/modals/tag_database.py @@ -2,14 +2,17 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + import structlog from PySide6.QtWidgets import QMessageBox, QPushButton -from src.core.constants import RESERVED_TAG_END, RESERVED_TAG_START -from src.core.library import Library, Tag -from src.qt.modals.build_tag import BuildTagPanel -from src.qt.modals.tag_search import TagSearchPanel -from src.qt.translations import Translations -from src.qt.widgets.panel import PanelModal + +from tagstudio.core.constants import RESERVED_TAG_END, RESERVED_TAG_START +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.library.alchemy.models import Tag +from tagstudio.qt.modals.build_tag import BuildTagPanel +from tagstudio.qt.modals.tag_search import TagSearchPanel +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.panel import PanelModal logger = structlog.get_logger(__name__) diff --git a/src/tagstudio/qt/modals/tag_search.py b/src/tagstudio/qt/modals/tag_search.py index 1c1ae9e1..f7413764 100644 --- a/src/tagstudio/qt/modals/tag_search.py +++ b/src/tagstudio/qt/modals/tag_search.py @@ -4,10 +4,9 @@ import contextlib -from typing import TYPE_CHECKING, override +from typing import TYPE_CHECKING from warnings import catch_warnings -import src.qt.modals.build_tag as build_tag import structlog from PySide6 import QtCore, QtGui from PySide6.QtCore import QSize, Qt, Signal @@ -23,21 +22,22 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) -from src.core.constants import RESERVED_TAG_END, RESERVED_TAG_START -from src.core.library import Library, Tag -from src.core.library.alchemy.enums import FilterState, TagColorEnum -from src.core.palette import ColorType, get_tag_color -from src.qt.translations import Translations -from src.qt.widgets.panel import PanelModal, PanelWidget -from src.qt.widgets.tag import ( - TagWidget, -) + +from tagstudio.core.constants import RESERVED_TAG_END, RESERVED_TAG_START +from tagstudio.core.library.alchemy.enums import FilterState, TagColorEnum +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.library.alchemy.models import Tag +from tagstudio.core.palette import ColorType, get_tag_color +from tagstudio.qt.modals import build_tag +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.panel import PanelModal, PanelWidget +from tagstudio.qt.widgets.tag import TagWidget logger = structlog.get_logger(__name__) # Only import for type checking/autocompletion, will not be imported at runtime. if TYPE_CHECKING: - from src.qt.modals.build_tag import BuildTagPanel + from tagstudio.qt.modals.build_tag import BuildTagPanel class TagSearchPanel(PanelWidget): @@ -262,7 +262,7 @@ class TagSearchPanel(PanelWidget): self.scroll_layout.addWidget(new_tw) # Assign the tag to the widget at the given index. - tag_widget: TagWidget = self.scroll_layout.itemAt(index).widget() # type: ignore + tag_widget: TagWidget = self.scroll_layout.itemAt(index).widget() tag_widget.set_tag(tag) # Set tag widget viability and potentially return early @@ -336,7 +336,6 @@ class TagSearchPanel(PanelWidget): self.search_field.setFocus() return super().showEvent(event) - @override def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802 # When Escape is pressed, focus back on the search box. # If focus is already on the search box, close the modal. diff --git a/src/tagstudio/qt/pagination.py b/src/tagstudio/qt/pagination.py index 3acdc6f2..fa6d9d5d 100644 --- a/src/tagstudio/qt/pagination.py +++ b/src/tagstudio/qt/pagination.py @@ -2,18 +2,14 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + """A pagination widget created for TagStudio.""" from PySide6.QtCore import QObject, QSize, Signal from PySide6.QtGui import QIntValidator -from PySide6.QtWidgets import ( - QHBoxLayout, - QLabel, - QLineEdit, - QSizePolicy, - QWidget, -) -from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper +from PySide6.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QSizePolicy, QWidget + +from tagstudio.qt.helpers.qbutton_wrapper import QPushButtonWrapper class Pagination(QWidget, QObject): @@ -215,7 +211,7 @@ class Pagination(QWidget, QObject): str(i + 1) ) self._assign_click( - self.start_buffer_layout.itemAt(i - start_offset).widget(), # type: ignore + self.start_buffer_layout.itemAt(i - start_offset).widget(), i, ) sbc += 1 @@ -231,7 +227,7 @@ class Pagination(QWidget, QObject): str(i + 1) ) self._assign_click( - self.end_buffer_layout.itemAt(i - end_offset).widget(), # type: ignore + self.end_buffer_layout.itemAt(i - end_offset).widget(), i, ) else: diff --git a/src/tagstudio/qt/platform_strings.py b/src/tagstudio/qt/platform_strings.py index 70426b12..6144896f 100644 --- a/src/tagstudio/qt/platform_strings.py +++ b/src/tagstudio/qt/platform_strings.py @@ -2,11 +2,12 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + """A collection of platform-dependant strings.""" import platform -from src.qt.translations import Translations +from tagstudio.qt.translations import Translations def open_file_str() -> str: diff --git a/src/tagstudio/qt/resource_manager.py b/src/tagstudio/qt/resource_manager.py index a45b2962..7e1434cb 100644 --- a/src/tagstudio/qt/resource_manager.py +++ b/src/tagstudio/qt/resource_manager.py @@ -2,15 +2,13 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + from pathlib import Path from typing import Any import structlog import ujson -from PIL import ( - Image, - ImageQt, -) +from PIL import Image, ImageQt from PySide6.QtGui import QPixmap logger = structlog.get_logger(__name__) @@ -22,7 +20,7 @@ class ResourceManager: _map: dict = {} _cache: dict[str, Any] = {} _initialized: bool = False - _res_folder: Path = Path(__file__).parents[2] + _res_folder: Path = Path(__file__).parents[1] def __init__(self) -> None: # Load JSON resource map diff --git a/src/tagstudio/qt/resources.json b/src/tagstudio/qt/resources.json index 80b625ee..8966e89f 100644 --- a/src/tagstudio/qt/resources.json +++ b/src/tagstudio/qt/resources.json @@ -7,7 +7,7 @@ "path": "qt/images/splash/goo_gears.png", "mode": "qpixmap" }, - "logo": { + "icon": { "path": "icon.png", "mode": "pil" }, diff --git a/src/tagstudio/qt/splash.py b/src/tagstudio/qt/splash.py index eec11be7..c6b778f7 100644 --- a/src/tagstudio/qt/splash.py +++ b/src/tagstudio/qt/splash.py @@ -2,26 +2,16 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + import math import structlog from PySide6.QtCore import QRect, Qt -from PySide6.QtGui import ( - QColor, - QFont, - QPainter, - QPen, - QPixmap, -) -from PySide6.QtWidgets import ( - QSplashScreen, - QWidget, -) -from src.core.constants import ( - VERSION, - VERSION_BRANCH, -) -from src.qt.resource_manager import ResourceManager +from PySide6.QtGui import QColor, QFont, QPainter, QPen, QPixmap +from PySide6.QtWidgets import QSplashScreen, QWidget + +from tagstudio.core.constants import VERSION, VERSION_BRANCH +from tagstudio.qt.resource_manager import ResourceManager logger = structlog.get_logger(__name__) @@ -32,7 +22,7 @@ class Splash: COPYRIGHT_YEARS: str = "2021-2025" COPYRIGHT_STR: str = f"Ā© {COPYRIGHT_YEARS} Travis Abendshien (CyanVoxel)" VERSION_STR: str = ( - f"Version {VERSION} {(" (" + VERSION_BRANCH + ")") if VERSION_BRANCH else ""}" + f"Version {VERSION} {(' (' + VERSION_BRANCH + ')') if VERSION_BRANCH else ''}" ) SPLASH_CLASSIC: str = "classic" diff --git a/src/tagstudio/qt/translations.py b/src/tagstudio/qt/translations.py index b47e4ae1..ef775b5d 100644 --- a/src/tagstudio/qt/translations.py +++ b/src/tagstudio/qt/translations.py @@ -20,7 +20,7 @@ class Translator: def __get_translation_dict(self, lang: str) -> dict[str, str]: with open( - Path(__file__).parents[2] / "resources" / "translations" / f"{lang}.json", + Path(__file__).parents[1] / "resources" / "translations" / f"{lang}.json", encoding="utf-8", ) as f: return ujson.loads(f.read()) diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index 0d22e034..33ba48e1 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -5,6 +5,7 @@ # SIGTERM handling based on the implementation by Virgil Dupras for dupeGuru: # https://github.com/arsenetar/dupeguru/blob/master/run.py#L71 + """A Qt driver for TagStudio.""" import contextlib @@ -19,8 +20,6 @@ from pathlib import Path from queue import Queue from warnings import catch_warnings -# this import has side-effect of import PySide resources -import src.qt.resources_rc # noqa: F401 import structlog from humanfriendly import format_size, format_timespan from PySide6 import QtCore @@ -50,57 +49,55 @@ from PySide6.QtWidgets import ( QScrollArea, QWidget, ) -from src.core.constants import ( - TAG_ARCHIVED, - TAG_FAVORITE, - VERSION, - VERSION_BRANCH, -) -from src.core.driver import DriverMixin -from src.core.enums import LibraryPrefs, MacroID, SettingItems -from src.core.library.alchemy import Library -from src.core.library.alchemy.enums import ( + +# this import has side-effect of import PySide resources +import tagstudio.qt.resources_rc # noqa: F401 +from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE, VERSION, VERSION_BRANCH +from tagstudio.core.driver import DriverMixin +from tagstudio.core.enums import LibraryPrefs, MacroID, SettingItems +from tagstudio.core.library.alchemy.enums import ( FieldTypeEnum, FilterState, ItemType, SortingModeEnum, ) -from src.core.library.alchemy.fields import _FieldID -from src.core.library.alchemy.library import Entry, LibraryStatus -from src.core.media_types import MediaCategories -from src.core.palette import ColorType, UiColor, get_ui_color -from src.core.query_lang.util import ParsingError -from src.core.ts_core import TagStudioCore -from src.core.utils.refresh_dir import RefreshDirTracker -from src.core.utils.web import strip_web_protocol -from src.qt.cache_manager import CacheManager -from src.qt.flowlayout import FlowLayout -from src.qt.helpers.custom_runnable import CustomRunnable -from src.qt.helpers.file_deleter import delete_file -from src.qt.helpers.function_iterator import FunctionIterator -from src.qt.main_window import Ui_MainWindow -from src.qt.modals.about import AboutModal -from src.qt.modals.build_tag import BuildTagPanel -from src.qt.modals.drop_import import DropImportModal -from src.qt.modals.ffmpeg_checker import FfmpegChecker -from src.qt.modals.file_extension import FileExtensionModal -from src.qt.modals.fix_dupes import FixDupeFilesModal -from src.qt.modals.fix_unlinked import FixUnlinkedEntriesModal -from src.qt.modals.folders_to_tags import FoldersToTagsModal -from src.qt.modals.settings_panel import SettingsPanel -from src.qt.modals.tag_color_manager import TagColorManager -from src.qt.modals.tag_database import TagDatabasePanel -from src.qt.modals.tag_search import TagSearchPanel -from src.qt.platform_strings import trash_term -from src.qt.resource_manager import ResourceManager -from src.qt.splash import Splash -from src.qt.translations import Translations -from src.qt.widgets.item_thumb import BadgeType, ItemThumb -from src.qt.widgets.migration_modal import JsonMigrationModal -from src.qt.widgets.panel import PanelModal -from src.qt.widgets.preview_panel import PreviewPanel -from src.qt.widgets.progress import ProgressWidget -from src.qt.widgets.thumb_renderer import ThumbRenderer +from tagstudio.core.library.alchemy.fields import _FieldID +from tagstudio.core.library.alchemy.library import Library, LibraryStatus +from tagstudio.core.library.alchemy.models import Entry +from tagstudio.core.media_types import MediaCategories +from tagstudio.core.palette import ColorType, UiColor, get_ui_color +from tagstudio.core.query_lang.util import ParsingError +from tagstudio.core.ts_core import TagStudioCore +from tagstudio.core.utils.refresh_dir import RefreshDirTracker +from tagstudio.core.utils.web import strip_web_protocol +from tagstudio.qt.cache_manager import CacheManager +from tagstudio.qt.flowlayout import FlowLayout +from tagstudio.qt.helpers.custom_runnable import CustomRunnable +from tagstudio.qt.helpers.file_deleter import delete_file +from tagstudio.qt.helpers.function_iterator import FunctionIterator +from tagstudio.qt.main_window import Ui_MainWindow +from tagstudio.qt.modals.about import AboutModal +from tagstudio.qt.modals.build_tag import BuildTagPanel +from tagstudio.qt.modals.drop_import import DropImportModal +from tagstudio.qt.modals.ffmpeg_checker import FfmpegChecker +from tagstudio.qt.modals.file_extension import FileExtensionModal +from tagstudio.qt.modals.fix_dupes import FixDupeFilesModal +from tagstudio.qt.modals.fix_unlinked import FixUnlinkedEntriesModal +from tagstudio.qt.modals.folders_to_tags import FoldersToTagsModal +from tagstudio.qt.modals.settings_panel import SettingsPanel +from tagstudio.qt.modals.tag_color_manager import TagColorManager +from tagstudio.qt.modals.tag_database import TagDatabasePanel +from tagstudio.qt.modals.tag_search import TagSearchPanel +from tagstudio.qt.platform_strings import trash_term +from tagstudio.qt.resource_manager import ResourceManager +from tagstudio.qt.splash import Splash +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.item_thumb import BadgeType, ItemThumb +from tagstudio.qt.widgets.migration_modal import JsonMigrationModal +from tagstudio.qt.widgets.panel import PanelModal +from tagstudio.qt.widgets.preview_panel import PreviewPanel +from tagstudio.qt.widgets.progress import ProgressWidget +from tagstudio.qt.widgets.thumb_renderer import ThumbRenderer BADGE_TAGS = { BadgeType.FAVORITE: TAG_FAVORITE, @@ -151,11 +148,11 @@ class QtDriver(DriverMixin, QObject): lib: Library - def __init__(self, backend, args): + def __init__(self, args): super().__init__() # prevent recursive badges update when multiple items selected self.badge_update_lock = False - self.lib = backend.Library() + self.lib = Library() self.rm: ResourceManager = ResourceManager() self.args = args self.filter = FilterState.show_all() @@ -259,7 +256,6 @@ class QtDriver(DriverMixin, QObject): app = QApplication(sys.argv) app.setStyle("Fusion") - icon_path = Path(__file__).parents[2] / "resources/icon.png" if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark: pal: QPalette = app.palette() @@ -283,10 +279,10 @@ class QtDriver(DriverMixin, QObject): # self.main_window = loader.load(home_path) self.main_window = Ui_MainWindow(self) self.main_window.setWindowTitle(self.base_title) - self.main_window.mousePressEvent = self.mouse_navigation # type: ignore[method-assign] - self.main_window.dragEnterEvent = self.drag_enter_event # type: ignore[method-assign] - self.main_window.dragMoveEvent = self.drag_move_event # type: ignore[method-assign] - self.main_window.dropEvent = self.drop_event # type: ignore[method-assign] + self.main_window.mousePressEvent = self.mouse_navigation + self.main_window.dragEnterEvent = self.drag_enter_event + self.main_window.dragMoveEvent = self.drag_move_event + self.main_window.dropEvent = self.drop_event self.splash: Splash = Splash( resource_manager=self.rm, @@ -302,7 +298,7 @@ class QtDriver(DriverMixin, QObject): if sys.platform != "darwin": icon = QIcon() - icon.addFile(str(icon_path)) + icon.addFile(str(self.rm.get_path("icon"))) app.setWindowIcon(icon) # Initialize the Tag Manager panel @@ -646,7 +642,7 @@ class QtDriver(DriverMixin, QObject): splitter.addWidget(self.preview_panel) QFontDatabase.addApplicationFont( - str(Path(__file__).parents[2] / "resources/qt/fonts/Oxanium-Bold.ttf") + str(Path(__file__).parents[1] / "resources/qt/fonts/Oxanium-Bold.ttf") ) # TODO this doesn't update when the language is changed @@ -714,8 +710,8 @@ class QtDriver(DriverMixin, QObject): ) except ParsingError as e: self.main_window.statusbar.showMessage( - f"{Translations["status.results.invalid_syntax"]} " - f"\"{self.main_window.searchField.text()}\"" + f"{Translations['status.results.invalid_syntax']} " + f'"{self.main_window.searchField.text()}"' ) logger.error("[QtDriver] Could not filter items", error=e) @@ -1055,7 +1051,7 @@ class QtDriver(DriverMixin, QObject): ) msg.setText( f"

{msg_text}

" - f"

{Translations["trash.dialog.disambiguation_warning.singular"]}

" + f"

{Translations['trash.dialog.disambiguation_warning.singular']}

" f"{filename if filename else ''}" f"{perm_warning}
" ) @@ -1067,7 +1063,7 @@ class QtDriver(DriverMixin, QObject): ) msg.setText( f"

{msg_text}

" - f"

{Translations["trash.dialog.disambiguation_warning.plural"]}

" + f"

{Translations['trash.dialog.disambiguation_warning.plural']}

" f"{perm_warning}
" ) @@ -1100,7 +1096,7 @@ class QtDriver(DriverMixin, QObject): "library.refresh.scanning.plural" if x + 1 != 1 else "library.refresh.scanning.singular", - searched_count=f"{x+1:n}", + searched_count=f"{x + 1:n}", found_count=f"{tracker.files_count:n}", ) ), @@ -1150,7 +1146,7 @@ class QtDriver(DriverMixin, QObject): pw.hide(), pw.deleteLater(), # refresh the library only when new items are added - files_count and self.filter_items(), + files_count and self.filter_items(), # type: ignore ) ) QThreadPool.globalInstance().start(r) diff --git a/src/tagstudio/qt/widgets/clickable_label.py b/src/tagstudio/qt/widgets/clickable_label.py index d4e57445..6d14f63d 100644 --- a/src/tagstudio/qt/widgets/clickable_label.py +++ b/src/tagstudio/qt/widgets/clickable_label.py @@ -2,6 +2,7 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + from PySide6.QtCore import Signal from PySide6.QtWidgets import QLabel diff --git a/src/tagstudio/qt/widgets/collage_icon.py b/src/tagstudio/qt/widgets/collage_icon.py index 4757747a..b4911d9c 100644 --- a/src/tagstudio/qt/widgets/collage_icon.py +++ b/src/tagstudio/qt/widgets/collage_icon.py @@ -2,6 +2,7 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + import math from pathlib import Path @@ -9,13 +10,11 @@ import cv2 import structlog from PIL import Image, ImageChops, UnidentifiedImageError from PIL.Image import DecompressionBombError -from PySide6.QtCore import ( - QObject, - Signal, -) -from src.core.library import Library -from src.core.media_types import MediaCategories -from src.qt.helpers.file_tester import is_readable_video +from PySide6.QtCore import QObject, Signal + +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.media_types import MediaCategories +from tagstudio.qt.helpers.file_tester import is_readable_video logger = structlog.get_logger(__name__) @@ -106,7 +105,7 @@ class CollageIconRenderer(QObject): except (UnidentifiedImageError, FileNotFoundError): logger.error("Couldn't read entry", entry=entry.path) with Image.open( - str(Path(__file__).parents[2] / "resources/qt/images/thumb_broken_512.png") + str(Path(__file__).parents[1] / "resources/qt/images/thumb_broken_512.png") ) as pic: pic.thumbnail(size) if data_tint_mode and color: diff --git a/src/tagstudio/qt/widgets/color_box.py b/src/tagstudio/qt/widgets/color_box.py index 3c20c41e..f9225439 100644 --- a/src/tagstudio/qt/widgets/color_box.py +++ b/src/tagstudio/qt/widgets/color_box.py @@ -9,19 +9,20 @@ from collections.abc import Iterable import structlog from PySide6.QtCore import Signal from PySide6.QtWidgets import QMessageBox, QPushButton -from src.core.constants import RESERVED_NAMESPACE_PREFIX -from src.core.library.alchemy.enums import TagColorEnum -from src.core.library.alchemy.models import TagColorGroup -from src.core.palette import ColorType, get_tag_color -from src.qt.flowlayout import FlowLayout -from src.qt.modals.build_color import BuildColorPanel -from src.qt.translations import Translations -from src.qt.widgets.fields import FieldWidget -from src.qt.widgets.panel import PanelModal -from src.qt.widgets.tag_color_label import TagColorLabel + +from tagstudio.core.constants import RESERVED_NAMESPACE_PREFIX +from tagstudio.core.library.alchemy.enums import TagColorEnum +from tagstudio.core.library.alchemy.models import TagColorGroup +from tagstudio.core.palette import ColorType, get_tag_color +from tagstudio.qt.flowlayout import FlowLayout +from tagstudio.qt.modals.build_color import BuildColorPanel +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.fields import FieldWidget +from tagstudio.qt.widgets.panel import PanelModal +from tagstudio.qt.widgets.tag_color_label import TagColorLabel if typing.TYPE_CHECKING: - from src.core.library import Library + from tagstudio.core.library import Library logger = structlog.get_logger(__name__) @@ -138,7 +139,7 @@ class ColorBoxWidget(FieldWidget): ) self.edit_modal.saved.connect( - lambda: (self.lib.update_color(*build_color_panel.build_color()), self.updated.emit()) + lambda: (self.lib.update_color(*build_color_panel.build_color()), self.updated.emit()) # type: ignore ) self.edit_modal.show() diff --git a/src/tagstudio/qt/widgets/fields.py b/src/tagstudio/qt/widgets/fields.py index 9cda274b..616a7f2f 100644 --- a/src/tagstudio/qt/widgets/fields.py +++ b/src/tagstudio/qt/widgets/fields.py @@ -13,25 +13,26 @@ from PIL import Image, ImageQt from PySide6.QtCore import QEvent, Qt from PySide6.QtGui import QEnterEvent, QPixmap, QResizeEvent from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget -from src.core.enums import Theme + +from tagstudio.core.enums import Theme logger = structlog.get_logger(__name__) class FieldContainer(QWidget): - # TODO: reference a resources folder rather than path.parents[3]? + # TODO: reference a resources folder rather than path.parents[2]? clipboard_icon_128: Image.Image = Image.open( - str(Path(__file__).parents[3] / "resources/qt/images/clipboard_icon_128.png") + str(Path(__file__).parents[2] / "resources/qt/images/clipboard_icon_128.png") ).resize((math.floor(24 * 1.25), math.floor(24 * 1.25))) clipboard_icon_128.load() edit_icon_128: Image.Image = Image.open( - str(Path(__file__).parents[3] / "resources/qt/images/edit_icon_128.png") + str(Path(__file__).parents[2] / "resources/qt/images/edit_icon_128.png") ).resize((math.floor(24 * 1.25), math.floor(24 * 1.25))) edit_icon_128.load() trash_icon_128: Image.Image = Image.open( - str(Path(__file__).parents[3] / "resources/qt/images/trash_icon_128.png") + str(Path(__file__).parents[2] / "resources/qt/images/trash_icon_128.png") ).resize((math.floor(24 * 1.25), math.floor(24 * 1.25))) trash_icon_128.load() diff --git a/src/tagstudio/qt/widgets/item_thumb.py b/src/tagstudio/qt/widgets/item_thumb.py index d1f45752..8b4f15d2 100644 --- a/src/tagstudio/qt/widgets/item_thumb.py +++ b/src/tagstudio/qt/widgets/item_thumb.py @@ -1,6 +1,8 @@ # Copyright (C) 2025 Travis Abendshien (CyanVoxel). # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + import time import typing from enum import Enum @@ -21,21 +23,20 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) -from src.core.constants import ( - TAG_ARCHIVED, - TAG_FAVORITE, -) -from src.core.library import ItemType, Library -from src.core.media_types import MediaCategories, MediaType -from src.qt.flowlayout import FlowWidget -from src.qt.helpers.file_opener import FileOpenerHelper -from src.qt.platform_strings import open_file_str, trash_term -from src.qt.translations import Translations -from src.qt.widgets.thumb_button import ThumbButton -from src.qt.widgets.thumb_renderer import ThumbRenderer + +from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE +from tagstudio.core.library.alchemy.enums import ItemType +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.media_types import MediaCategories, MediaType +from tagstudio.qt.flowlayout import FlowWidget +from tagstudio.qt.helpers.file_opener import FileOpenerHelper +from tagstudio.qt.platform_strings import open_file_str, trash_term +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.thumb_button import ThumbButton +from tagstudio.qt.widgets.thumb_renderer import ThumbRenderer if TYPE_CHECKING: - from src.qt.ts_qt import QtDriver + from tagstudio.qt.ts_qt import QtDriver logger = structlog.get_logger(__name__) @@ -76,12 +77,12 @@ class ItemThumb(FlowWidget): update_cutoff: float = time.time() collation_icon_128: Image.Image = Image.open( - str(Path(__file__).parents[3] / "resources/qt/images/collation_icon_128.png") + str(Path(__file__).parents[2] / "resources/qt/images/collation_icon_128.png") ) collation_icon_128.load() tag_group_icon_128: Image.Image = Image.open( - str(Path(__file__).parents[3] / "resources/qt/images/tag_group_icon_128.png") + str(Path(__file__).parents[2] / "resources/qt/images/tag_group_icon_128.png") ) tag_group_icon_128.load() @@ -208,7 +209,7 @@ class ItemThumb(FlowWidget): self.update_thumb(timestamp, image=image), self.update_size(timestamp, size=size), self.set_filename_text(filename), - self.set_extension(ext), + self.set_extension(ext), # type: ignore ) ) self.thumb_button.setFlat(True) diff --git a/src/tagstudio/qt/widgets/landing.py b/src/tagstudio/qt/widgets/landing.py index 111f5daa..d60ab84e 100644 --- a/src/tagstudio/qt/widgets/landing.py +++ b/src/tagstudio/qt/widgets/landing.py @@ -12,13 +12,14 @@ from PIL import Image, ImageQt from PySide6.QtCore import QEasingCurve, QPoint, QPropertyAnimation, Qt from PySide6.QtGui import QPixmap from PySide6.QtWidgets import QLabel, QPushButton, QVBoxLayout, QWidget -from src.qt.helpers.color_overlay import gradient_overlay, theme_fg_overlay -from src.qt.translations import Translations -from src.qt.widgets.clickable_label import ClickableLabel + +from tagstudio.qt.helpers.color_overlay import gradient_overlay, theme_fg_overlay +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.clickable_label import ClickableLabel # Only import for type checking/autocompletion, will not be imported at runtime. if typing.TYPE_CHECKING: - from src.qt.ts_qt import QtDriver + from tagstudio.qt.ts_qt import QtDriver logging.basicConfig(format="%(message)s", level=logging.INFO) @@ -41,7 +42,7 @@ class LandingWidget(QWidget): # Create landing logo -------------------------------------------------- # self.landing_logo_pixmap = QPixmap(":/images/tagstudio_logo_text_mono.png") self.logo_raw: Image.Image = Image.open( - Path(__file__).parents[3] / "resources/qt/images/tagstudio_logo_text_mono.png" + Path(__file__).parents[2] / "resources/qt/images/tagstudio_logo_text_mono.png" ) self.landing_pixmap: QPixmap = QPixmap() self.update_logo_color() diff --git a/src/tagstudio/qt/widgets/media_player.py b/src/tagstudio/qt/widgets/media_player.py index b1d86071..1f42066e 100644 --- a/src/tagstudio/qt/widgets/media_player.py +++ b/src/tagstudio/qt/widgets/media_player.py @@ -22,7 +22,7 @@ from PySide6.QtWidgets import ( ) if typing.TYPE_CHECKING: - from src.qt.ts_qt import QtDriver + from tagstudio.qt.ts_qt import QtDriver class MediaPlayer(QWidget): diff --git a/src/tagstudio/qt/widgets/migration_modal.py b/src/tagstudio/qt/widgets/migration_modal.py index 9ad16a88..068334fd 100644 --- a/src/tagstudio/qt/widgets/migration_modal.py +++ b/src/tagstudio/qt/widgets/migration_modal.py @@ -2,6 +2,7 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + import traceback from pathlib import Path @@ -20,22 +21,28 @@ from PySide6.QtWidgets import ( ) from sqlalchemy import select from sqlalchemy.orm import Session -from src.core.constants import LEGACY_TAG_FIELD_IDS, TS_FOLDER_NAME -from src.core.enums import LibraryPrefs -from src.core.library.alchemy import default_color_groups -from src.core.library.alchemy.joins import TagParent -from src.core.library.alchemy.library import TAG_ARCHIVED, TAG_FAVORITE, TAG_META -from src.core.library.alchemy.library import Library as SqliteLibrary -from src.core.library.alchemy.models import Entry, TagAlias -from src.core.library.json.library import Library as JsonLibrary # type: ignore -from src.core.library.json.library import Tag as JsonTag # type: ignore -from src.qt.helpers.custom_runnable import CustomRunnable -from src.qt.helpers.function_iterator import FunctionIterator -from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper -from src.qt.translations import Translations -from src.qt.widgets.paged_panel.paged_body_wrapper import PagedBodyWrapper -from src.qt.widgets.paged_panel.paged_panel import PagedPanel -from src.qt.widgets.paged_panel.paged_panel_state import PagedPanelState + +from tagstudio.core.constants import ( + LEGACY_TAG_FIELD_IDS, + TAG_ARCHIVED, + TAG_FAVORITE, + TAG_META, + TS_FOLDER_NAME, +) +from tagstudio.core.enums import LibraryPrefs +from tagstudio.core.library.alchemy import default_color_groups +from tagstudio.core.library.alchemy.joins import TagParent +from tagstudio.core.library.alchemy.library import Library as SqliteLibrary +from tagstudio.core.library.alchemy.models import Entry, TagAlias +from tagstudio.core.library.json.library import Library as JsonLibrary +from tagstudio.core.library.json.library import Tag as JsonTag +from tagstudio.qt.helpers.custom_runnable import CustomRunnable +from tagstudio.qt.helpers.function_iterator import FunctionIterator +from tagstudio.qt.helpers.qbutton_wrapper import QPushButtonWrapper +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.paged_panel.paged_body_wrapper import PagedBodyWrapper +from tagstudio.qt.widgets.paged_panel.paged_panel import PagedPanel +from tagstudio.qt.widgets.paged_panel.paged_panel_state import PagedPanelState logger = structlog.get_logger(__name__) @@ -365,7 +372,7 @@ class JsonMigrationModal(QObject): iterator = FunctionIterator(self.migration_iterator) iterator.value.connect( lambda x: ( - pb.setLabelText(f"

{x}

"), + pb.setLabelText(f"

{x}

"), # type: ignore self.update_sql_value_ui(show_msg_box=False) if x == Translations["json_migration.checking_for_parity"] else (), @@ -378,10 +385,10 @@ class JsonMigrationModal(QObject): r.done.connect( lambda: ( self.update_sql_value_ui(show_msg_box=not skip_ui), - pb.setMinimum(1), - pb.setValue(1), + pb.setMinimum(1), # type: ignore + pb.setValue(1), # type: ignore # Enable the finish button - self.stack[1].buttons[4].setDisabled(False), # type: ignore + self.stack[1].buttons[4].setDisabled(False), ) ) QThreadPool.globalInstance().start(r) @@ -557,10 +564,10 @@ class JsonMigrationModal(QObject): if not sql_entry: logger.info( "[Field Comparison]", - message=f"NEW (SQL): SQL Entry ID mismatch: {json_entry.id+1}", + message=f"NEW (SQL): SQL Entry ID mismatch: {json_entry.id + 1}", ) self.discrepancies.append( - f"[Field Comparison]:\nNEW (SQL): SQL Entry ID not found: {json_entry.id+1}" + f"[Field Comparison]:\nNEW (SQL): SQL Entry ID not found: {json_entry.id + 1}" ) self.field_parity = False return self.field_parity diff --git a/src/tagstudio/qt/widgets/paged_panel/paged_body_wrapper.py b/src/tagstudio/qt/widgets/paged_panel/paged_body_wrapper.py index 7e2e87f2..cb713b3e 100644 --- a/src/tagstudio/qt/widgets/paged_panel/paged_body_wrapper.py +++ b/src/tagstudio/qt/widgets/paged_panel/paged_body_wrapper.py @@ -2,11 +2,9 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + from PySide6.QtCore import Qt -from PySide6.QtWidgets import ( - QVBoxLayout, - QWidget, -) +from PySide6.QtWidgets import QVBoxLayout, QWidget class PagedBodyWrapper(QWidget): diff --git a/src/tagstudio/qt/widgets/paged_panel/paged_panel.py b/src/tagstudio/qt/widgets/paged_panel/paged_panel.py index 3cdc09f5..020f9889 100644 --- a/src/tagstudio/qt/widgets/paged_panel/paged_panel.py +++ b/src/tagstudio/qt/widgets/paged_panel/paged_panel.py @@ -2,18 +2,15 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + from typing import override import structlog from PySide6 import QtCore, QtGui from PySide6.QtCore import Qt -from PySide6.QtWidgets import ( - QHBoxLayout, - QLabel, - QVBoxLayout, - QWidget, -) -from src.qt.widgets.paged_panel.paged_panel_state import PagedPanelState +from PySide6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget + +from tagstudio.qt.widgets.paged_panel.paged_panel_state import PagedPanelState logger = structlog.get_logger(__name__) diff --git a/src/tagstudio/qt/widgets/paged_panel/paged_panel_state.py b/src/tagstudio/qt/widgets/paged_panel/paged_panel_state.py index 20ecafba..66e04b28 100644 --- a/src/tagstudio/qt/widgets/paged_panel/paged_panel_state.py +++ b/src/tagstudio/qt/widgets/paged_panel/paged_panel_state.py @@ -4,7 +4,8 @@ from PySide6.QtWidgets import QPushButton -from src.qt.widgets.paged_panel.paged_body_wrapper import PagedBodyWrapper + +from tagstudio.qt.widgets.paged_panel.paged_body_wrapper import PagedBodyWrapper class PagedPanelState: diff --git a/src/tagstudio/qt/widgets/panel.py b/src/tagstudio/qt/widgets/panel.py index 26de75f9..cdd59b2c 100755 --- a/src/tagstudio/qt/widgets/panel.py +++ b/src/tagstudio/qt/widgets/panel.py @@ -1,13 +1,16 @@ # Copyright (C) 2025 Travis Abendshien (CyanVoxel). # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -from typing import Callable + + +from typing import Callable, override import structlog from PySide6 import QtCore, QtGui from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget -from src.qt.translations import Translations + +from tagstudio.qt.translations import Translations logger = structlog.get_logger(__name__) @@ -39,7 +42,7 @@ class PanelModal(QWidget): self.title_widget = QLabel() self.title_widget.setObjectName("fieldTitle") self.title_widget.setWordWrap(True) - self.title_widget.setStyleSheet("font-weight:bold;" "font-size:14px;" "padding-top: 6px") + self.title_widget.setStyleSheet("font-weight:bold;font-size:14px;padding-top: 6px") self.setTitle(title) self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) @@ -129,6 +132,7 @@ class PanelWidget(QWidget): def add_callback(self, callback: Callable, event: str = "returnPressed"): logger.warning(f"[PanelModal] add_callback not implemented for {self.__class__.__name__}") + @override def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802 if event.key() == QtCore.Qt.Key.Key_Escape: if self.panel_cancel_button: diff --git a/src/tagstudio/qt/widgets/preview/field_containers.py b/src/tagstudio/qt/widgets/preview/field_containers.py index 4e732199..76dc4860 100644 --- a/src/tagstudio/qt/widgets/preview/field_containers.py +++ b/src/tagstudio/qt/widgets/preview/field_containers.py @@ -2,6 +2,7 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + import sys import typing from collections.abc import Callable @@ -20,29 +21,27 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) -from src.core.constants import ( - TAG_ARCHIVED, - TAG_FAVORITE, -) -from src.core.enums import Theme -from src.core.library.alchemy.fields import ( + +from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE +from tagstudio.core.enums import Theme +from tagstudio.core.library.alchemy.fields import ( BaseField, DatetimeField, FieldTypeEnum, TextField, ) -from src.core.library.alchemy.library import Library -from src.core.library.alchemy.models import Entry, Tag -from src.qt.translations import Translations -from src.qt.widgets.fields import FieldContainer -from src.qt.widgets.panel import PanelModal -from src.qt.widgets.tag_box import TagBoxWidget -from src.qt.widgets.text import TextWidget -from src.qt.widgets.text_box_edit import EditTextBox -from src.qt.widgets.text_line_edit import EditTextLine +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.library.alchemy.models import Entry, Tag +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.fields import FieldContainer +from tagstudio.qt.widgets.panel import PanelModal +from tagstudio.qt.widgets.tag_box import TagBoxWidget +from tagstudio.qt.widgets.text import TextWidget +from tagstudio.qt.widgets.text_box_edit import EditTextBox +from tagstudio.qt.widgets.text_line_edit import EditTextLine if typing.TYPE_CHECKING: - from src.qt.ts_qt import QtDriver + from tagstudio.qt.ts_qt import QtDriver logger = structlog.get_logger(__name__) @@ -98,10 +97,7 @@ class FieldContainers(QWidget): # rounded corners are maintained when scrolling. I was unable to # find the right trick to only select that particular element. self.scroll_area.setStyleSheet( - "QWidget#entryScrollContainer{" - f"background:{self.panel_bg_color};" - "border-radius:6px;" - "}" + f"QWidget#entryScrollContainer{{background:{self.panel_bg_color};border-radius:6px;}}" ) self.scroll_area.setWidget(scroll_container) @@ -323,14 +319,14 @@ class FieldContainers(QWidget): window_title=f"Edit {field.type.type.value}", save_callback=( lambda content: ( - self.update_field(field, content), + self.update_field(field, content), # type: ignore self.update_from_entry(self.cached_entries[0].id), ) ), ) if "pytest" in sys.modules: # for better testability - container.modal = modal # type: ignore + container.modal = modal container.set_edit_callback(modal.show) container.set_remove_callback( @@ -362,7 +358,7 @@ class FieldContainers(QWidget): window_title=f"Edit {field.type.name}", save_callback=( lambda content: ( - self.update_field(field, content), + self.update_field(field, content), # type: ignore self.update_from_entry(self.cached_entries[0].id), ) ), diff --git a/src/tagstudio/qt/widgets/preview/file_attributes.py b/src/tagstudio/qt/widgets/preview/file_attributes.py index dd309735..c9e84c70 100644 --- a/src/tagstudio/qt/widgets/preview/file_attributes.py +++ b/src/tagstudio/qt/widgets/preview/file_attributes.py @@ -2,6 +2,7 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + import os import platform import typing @@ -14,19 +15,16 @@ from humanfriendly import format_size from PIL import ImageFont from PySide6.QtCore import Qt from PySide6.QtGui import QGuiApplication -from PySide6.QtWidgets import ( - QLabel, - QVBoxLayout, - QWidget, -) -from src.core.enums import Theme -from src.core.library.alchemy.library import Library -from src.core.media_types import MediaCategories -from src.qt.helpers.file_opener import FileOpenerHelper, FileOpenerLabel -from src.qt.translations import Translations +from PySide6.QtWidgets import QLabel, QVBoxLayout, QWidget + +from tagstudio.core.enums import Theme +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.media_types import MediaCategories +from tagstudio.qt.helpers.file_opener import FileOpenerHelper, FileOpenerLabel +from tagstudio.qt.translations import Translations if typing.TYPE_CHECKING: - from src.qt.ts_qt import QtDriver + from tagstudio.qt.ts_qt import QtDriver logger = structlog.get_logger(__name__) @@ -109,21 +107,20 @@ class FileAttributes(QWidget): created = dt.fromtimestamp(filepath.stat().st_ctime) modified: dt = dt.fromtimestamp(filepath.stat().st_mtime) self.date_created_label.setText( - f"{Translations["file.date_created"]}: " - f"{dt.strftime(created, "%a, %x, %X")}" + f"{Translations['file.date_created']}: {dt.strftime(created, '%a, %x, %X')}" ) self.date_modified_label.setText( - f"{Translations["file.date_modified"]}: " - f"{dt.strftime(modified, "%a, %x, %X")}" + f"{Translations['file.date_modified']}: " + f"{dt.strftime(modified, '%a, %x, %X')}" ) self.date_created_label.setHidden(False) self.date_modified_label.setHidden(False) elif filepath: self.date_created_label.setText( - f"{Translations["file.date_created"]}: N/A" + f"{Translations['file.date_created']}: N/A" ) self.date_modified_label.setText( - f"{Translations["file.date_modified"]}: N/A" + f"{Translations['file.date_modified']}: N/A" ) self.date_created_label.setHidden(False) self.date_modified_label.setHidden(False) @@ -139,7 +136,7 @@ class FileAttributes(QWidget): if not filepath: self.layout().setSpacing(0) self.file_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.file_label.setText(f"{Translations["preview.no_selection"]}") + self.file_label.setText(f"{Translations['preview.no_selection']}") self.file_label.set_file_path("") self.file_label.setCursor(Qt.CursorShape.ArrowCursor) self.dimensions_label.setText("") @@ -155,9 +152,9 @@ class FileAttributes(QWidget): for i, part in enumerate(filepath.parts): part_ = part.strip(os.path.sep) if i != len(filepath.parts) - 1: - file_str += f"{"\u200b".join(part_)}{separator}" + file_str += f"{'\u200b'.join(part_)}{separator}" else: - file_str += f"
{"\u200b".join(part_)}" + file_str += f"
{'\u200b'.join(part_)}" self.file_label.setText(file_str) self.file_label.setCursor(Qt.CursorShape.PointingHandCursor) self.opener = FileOpenerHelper(filepath) diff --git a/src/tagstudio/qt/widgets/preview/preview_thumb.py b/src/tagstudio/qt/widgets/preview/preview_thumb.py index e1f2443c..000debd3 100644 --- a/src/tagstudio/qt/widgets/preview/preview_thumb.py +++ b/src/tagstudio/qt/widgets/preview/preview_thumb.py @@ -14,26 +14,23 @@ import structlog from PIL import Image, UnidentifiedImageError from PySide6.QtCore import QBuffer, QByteArray, QSize, Qt from PySide6.QtGui import QAction, QMovie, QResizeEvent -from PySide6.QtWidgets import ( - QHBoxLayout, - QLabel, - QWidget, -) -from src.core.library.alchemy.library import Library -from src.core.media_types import MediaCategories -from src.qt.helpers.file_opener import FileOpenerHelper, open_file -from src.qt.helpers.file_tester import is_readable_video -from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper -from src.qt.helpers.rounded_pixmap_style import RoundedPixmapStyle -from src.qt.platform_strings import open_file_str, trash_term -from src.qt.resource_manager import ResourceManager -from src.qt.translations import Translations -from src.qt.widgets.media_player import MediaPlayer -from src.qt.widgets.thumb_renderer import ThumbRenderer -from src.qt.widgets.video_player import VideoPlayer +from PySide6.QtWidgets import QHBoxLayout, QLabel, QWidget + +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.media_types import MediaCategories +from tagstudio.qt.helpers.file_opener import FileOpenerHelper, open_file +from tagstudio.qt.helpers.file_tester import is_readable_video +from tagstudio.qt.helpers.qbutton_wrapper import QPushButtonWrapper +from tagstudio.qt.helpers.rounded_pixmap_style import RoundedPixmapStyle +from tagstudio.qt.platform_strings import open_file_str, trash_term +from tagstudio.qt.resource_manager import ResourceManager +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.media_player import MediaPlayer +from tagstudio.qt.widgets.thumb_renderer import ThumbRenderer +from tagstudio.qt.widgets.video_player import VideoPlayer if typing.TYPE_CHECKING: - from src.qt.ts_qt import QtDriver + from tagstudio.qt.ts_qt import QtDriver logger = structlog.get_logger(__name__) diff --git a/src/tagstudio/qt/widgets/preview_panel.py b/src/tagstudio/qt/widgets/preview_panel.py index 252c4556..5af5c5bc 100644 --- a/src/tagstudio/qt/widgets/preview_panel.py +++ b/src/tagstudio/qt/widgets/preview_panel.py @@ -2,6 +2,7 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + import traceback import typing from pathlib import Path @@ -9,27 +10,22 @@ from warnings import catch_warnings import structlog from PySide6.QtCore import Qt -from PySide6.QtWidgets import ( - QHBoxLayout, - QPushButton, - QSplitter, - QVBoxLayout, - QWidget, -) -from src.core.enums import Theme -from src.core.library.alchemy.library import Library -from src.core.library.alchemy.models import Entry -from src.core.palette import ColorType, UiColor, get_ui_color -from src.qt.modals.add_field import AddFieldModal -from src.qt.modals.tag_search import TagSearchPanel -from src.qt.translations import Translations -from src.qt.widgets.panel import PanelModal -from src.qt.widgets.preview.field_containers import FieldContainers -from src.qt.widgets.preview.file_attributes import FileAttributes -from src.qt.widgets.preview.preview_thumb import PreviewThumb +from PySide6.QtWidgets import QHBoxLayout, QPushButton, QSplitter, QVBoxLayout, QWidget + +from tagstudio.core.enums import Theme +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.library.alchemy.models import Entry +from tagstudio.core.palette import ColorType, UiColor, get_ui_color +from tagstudio.qt.modals.add_field import AddFieldModal +from tagstudio.qt.modals.tag_search import TagSearchPanel +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.panel import PanelModal +from tagstudio.qt.widgets.preview.field_containers import FieldContainers +from tagstudio.qt.widgets.preview.file_attributes import FileAttributes +from tagstudio.qt.widgets.preview.preview_thumb import PreviewThumb if typing.TYPE_CHECKING: - from src.qt.ts_qt import QtDriver + from tagstudio.qt.ts_qt import QtDriver logger = structlog.get_logger(__name__) diff --git a/src/tagstudio/qt/widgets/progress.py b/src/tagstudio/qt/widgets/progress.py index d9bc09a5..dcd1ed16 100644 --- a/src/tagstudio/qt/widgets/progress.py +++ b/src/tagstudio/qt/widgets/progress.py @@ -2,12 +2,14 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -from typing import Callable, Optional + +from typing import Callable from PySide6.QtCore import Qt, QThreadPool from PySide6.QtWidgets import QProgressDialog, QVBoxLayout, QWidget -from src.qt.helpers.custom_runnable import CustomRunnable -from src.qt.helpers.function_iterator import FunctionIterator + +from tagstudio.qt.helpers.custom_runnable import CustomRunnable +from tagstudio.qt.helpers.function_iterator import FunctionIterator class ProgressWidget(QWidget): @@ -18,7 +20,7 @@ class ProgressWidget(QWidget): *, window_title: str = "", label_text: str = "", - cancel_button_text: Optional[str], + cancel_button_text: str | None, minimum: int, maximum: int, ): @@ -61,6 +63,6 @@ class ProgressWidget(QWidget): r = CustomRunnable(lambda: iterator.run()) r.done.connect( - lambda: (self.hide(), self.deleteLater(), [callback() for callback in done_callbacks]) + lambda: (self.hide(), self.deleteLater(), [callback() for callback in done_callbacks]) # type: ignore ) QThreadPool.globalInstance().start(r) diff --git a/src/tagstudio/qt/widgets/tag.py b/src/tagstudio/qt/widgets/tag.py index 8eb5252e..ca124070 100644 --- a/src/tagstudio/qt/widgets/tag.py +++ b/src/tagstudio/qt/widgets/tag.py @@ -9,24 +9,19 @@ from types import FunctionType import structlog from PySide6.QtCore import QEvent, Qt, Signal from PySide6.QtGui import QAction, QColor, QEnterEvent, QFontMetrics -from PySide6.QtWidgets import ( - QHBoxLayout, - QLineEdit, - QPushButton, - QVBoxLayout, - QWidget, -) -from src.core.library import Tag -from src.core.library.alchemy.enums import TagColorEnum -from src.core.palette import ColorType, get_tag_color -from src.qt.helpers.escape_text import escape_text -from src.qt.translations import Translations +from PySide6.QtWidgets import QHBoxLayout, QLineEdit, QPushButton, QVBoxLayout, QWidget + +from tagstudio.core.library.alchemy.enums import TagColorEnum +from tagstudio.core.library.alchemy.models import Tag +from tagstudio.core.palette import ColorType, get_tag_color +from tagstudio.qt.helpers.escape_text import escape_text +from tagstudio.qt.translations import Translations logger = structlog.get_logger(__name__) # Only import for type checking/autocompletion, will not be imported at runtime. if typing.TYPE_CHECKING: - from src.core.library.alchemy import Library + from tagstudio.core.library.alchemy.library import Library class TagAliasWidget(QWidget): diff --git a/src/tagstudio/qt/widgets/tag_box.py b/src/tagstudio/qt/widgets/tag_box.py index 953f0759..8fdf1e9c 100644 --- a/src/tagstudio/qt/widgets/tag_box.py +++ b/src/tagstudio/qt/widgets/tag_box.py @@ -7,16 +7,17 @@ import typing import structlog from PySide6.QtCore import Signal -from src.core.library import Tag -from src.core.library.alchemy.enums import FilterState -from src.qt.flowlayout import FlowLayout -from src.qt.modals.build_tag import BuildTagPanel -from src.qt.widgets.fields import FieldWidget -from src.qt.widgets.panel import PanelModal -from src.qt.widgets.tag import TagWidget + +from tagstudio.core.library.alchemy.enums import FilterState +from tagstudio.core.library.alchemy.models import Tag +from tagstudio.qt.flowlayout import FlowLayout +from tagstudio.qt.modals.build_tag import BuildTagPanel +from tagstudio.qt.widgets.fields import FieldWidget +from tagstudio.qt.widgets.panel import PanelModal +from tagstudio.qt.widgets.tag import TagWidget if typing.TYPE_CHECKING: - from src.qt.ts_qt import QtDriver + from tagstudio.qt.ts_qt import QtDriver logger = structlog.get_logger(__name__) diff --git a/src/tagstudio/qt/widgets/tag_color_label.py b/src/tagstudio/qt/widgets/tag_color_label.py index 2557eb81..05cdacaf 100644 --- a/src/tagstudio/qt/widgets/tag_color_label.py +++ b/src/tagstudio/qt/widgets/tag_color_label.py @@ -8,16 +8,12 @@ import typing import structlog from PySide6.QtCore import QEvent, Qt, Signal from PySide6.QtGui import QAction, QColor, QEnterEvent -from PySide6.QtWidgets import ( - QHBoxLayout, - QPushButton, - QVBoxLayout, - QWidget, -) -from src.core.library.alchemy.models import TagColorGroup -from src.qt.helpers.escape_text import escape_text -from src.qt.translations import Translations -from src.qt.widgets.tag import ( +from PySide6.QtWidgets import QHBoxLayout, QPushButton, QVBoxLayout, QWidget + +from tagstudio.core.library.alchemy.models import TagColorGroup +from tagstudio.qt.helpers.escape_text import escape_text +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.tag import ( get_border_color, get_highlight_color, get_text_color, @@ -27,7 +23,7 @@ logger = structlog.get_logger(__name__) # Only import for type checking/autocompletion, will not be imported at runtime. if typing.TYPE_CHECKING: - from src.core.library.alchemy import Library + from tagstudio.core.library.alchemy.library import Library class TagColorLabel(QWidget): diff --git a/src/tagstudio/qt/widgets/tag_color_preview.py b/src/tagstudio/qt/widgets/tag_color_preview.py index be8dba47..2e0fb584 100644 --- a/src/tagstudio/qt/widgets/tag_color_preview.py +++ b/src/tagstudio/qt/widgets/tag_color_preview.py @@ -8,23 +8,20 @@ import typing import structlog from PySide6.QtCore import Qt, Signal from PySide6.QtGui import QColor -from PySide6.QtWidgets import ( - QPushButton, - QVBoxLayout, - QWidget, -) -from src.core.library.alchemy.enums import TagColorEnum -from src.core.library.alchemy.models import TagColorGroup -from src.core.palette import ColorType, get_tag_color -from src.qt.translations import Translations -from src.qt.widgets.tag import ( +from PySide6.QtWidgets import QPushButton, QVBoxLayout, QWidget + +from tagstudio.core.library.alchemy.enums import TagColorEnum +from tagstudio.core.library.alchemy.models import TagColorGroup +from tagstudio.core.palette import ColorType, get_tag_color +from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.tag import ( get_border_color, get_highlight_color, get_text_color, ) if typing.TYPE_CHECKING: - from src.core.library import Library + from tagstudio.core.library.alchemy.library import Library logger = structlog.get_logger(__name__) diff --git a/src/tagstudio/qt/widgets/text.py b/src/tagstudio/qt/widgets/text.py index 28e21b03..90b3008e 100644 --- a/src/tagstudio/qt/widgets/text.py +++ b/src/tagstudio/qt/widgets/text.py @@ -5,7 +5,8 @@ from PySide6.QtCore import Qt from PySide6.QtWidgets import QHBoxLayout, QLabel -from src.qt.widgets.fields import FieldWidget + +from tagstudio.qt.widgets.fields import FieldWidget class TextWidget(FieldWidget): diff --git a/src/tagstudio/qt/widgets/text_box_edit.py b/src/tagstudio/qt/widgets/text_box_edit.py index 5f554501..0af6f83f 100644 --- a/src/tagstudio/qt/widgets/text_box_edit.py +++ b/src/tagstudio/qt/widgets/text_box_edit.py @@ -4,7 +4,8 @@ from PySide6.QtWidgets import QPlainTextEdit, QVBoxLayout -from src.qt.widgets.panel import PanelWidget + +from tagstudio.qt.widgets.panel import PanelWidget class EditTextBox(PanelWidget): diff --git a/src/tagstudio/qt/widgets/text_line_edit.py b/src/tagstudio/qt/widgets/text_line_edit.py index 09e2681c..9822abea 100644 --- a/src/tagstudio/qt/widgets/text_line_edit.py +++ b/src/tagstudio/qt/widgets/text_line_edit.py @@ -1,10 +1,13 @@ # Copyright (C) 2025 Travis Abendshien (CyanVoxel). # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + from typing import Callable from PySide6.QtWidgets import QLineEdit, QVBoxLayout -from src.qt.widgets.panel import PanelWidget + +from tagstudio.qt.widgets.panel import PanelWidget class EditTextLine(PanelWidget): diff --git a/src/tagstudio/qt/widgets/thumb_button.py b/src/tagstudio/qt/widgets/thumb_button.py index cfd45535..51c17ad6 100644 --- a/src/tagstudio/qt/widgets/thumb_button.py +++ b/src/tagstudio/qt/widgets/thumb_button.py @@ -17,7 +17,8 @@ from PySide6.QtGui import ( QPen, ) from PySide6.QtWidgets import QWidget -from src.qt.helpers.qbutton_wrapper import QPushButtonWrapper + +from tagstudio.qt.helpers.qbutton_wrapper import QPushButtonWrapper class ThumbButton(QPushButtonWrapper): diff --git a/src/tagstudio/qt/widgets/thumb_renderer.py b/src/tagstudio/qt/widgets/thumb_renderer.py index 8e373e6d..c73dbaa4 100644 --- a/src/tagstudio/qt/widgets/thumb_renderer.py +++ b/src/tagstudio/qt/widgets/thumb_renderer.py @@ -2,6 +2,7 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + import contextlib import hashlib import math @@ -43,23 +44,29 @@ from PySide6.QtCore import ( from PySide6.QtGui import QGuiApplication, QImage, QPainter, QPixmap from PySide6.QtPdf import QPdfDocument, QPdfDocumentRenderOptions from PySide6.QtSvg import QSvgRenderer -from src.core.constants import FONT_SAMPLE_SIZES, FONT_SAMPLE_TEXT, THUMB_CACHE_NAME, TS_FOLDER_NAME -from src.core.exceptions import NoRendererError -from src.core.media_types import MediaCategories, MediaType -from src.core.palette import ColorType, UiColor, get_ui_color -from src.core.utils.encoding import detect_char_encoding -from src.qt.cache_manager import CacheManager -from src.qt.helpers.blender_thumbnailer import blend_thumb -from src.qt.helpers.color_overlay import theme_fg_overlay -from src.qt.helpers.file_tester import is_readable_video -from src.qt.helpers.gradient import four_corner_gradient -from src.qt.helpers.image_effects import replace_transparent_pixels -from src.qt.helpers.text_wrapper import wrap_full_text -from src.qt.helpers.vendored.pydub.audio_segment import ( # type: ignore +from vtf2img import Parser + +from tagstudio.core.constants import ( + FONT_SAMPLE_SIZES, + FONT_SAMPLE_TEXT, + THUMB_CACHE_NAME, + TS_FOLDER_NAME, +) +from tagstudio.core.exceptions import NoRendererError +from tagstudio.core.media_types import MediaCategories, MediaType +from tagstudio.core.palette import ColorType, UiColor, get_ui_color +from tagstudio.core.utils.encoding import detect_char_encoding +from tagstudio.qt.cache_manager import CacheManager +from tagstudio.qt.helpers.blender_thumbnailer import blend_thumb +from tagstudio.qt.helpers.color_overlay import theme_fg_overlay +from tagstudio.qt.helpers.file_tester import is_readable_video +from tagstudio.qt.helpers.gradient import four_corner_gradient +from tagstudio.qt.helpers.image_effects import replace_transparent_pixels +from tagstudio.qt.helpers.text_wrapper import wrap_full_text +from tagstudio.qt.helpers.vendored.pydub.audio_segment import ( _AudioSegment as AudioSegment, ) -from src.qt.resource_manager import ResourceManager -from vtf2img import Parser +from tagstudio.qt.resource_manager import ResourceManager ImageFile.LOAD_TRUNCATED_IMAGES = True diff --git a/src/tagstudio/qt/widgets/video_player.py b/src/tagstudio/qt/widgets/video_player.py index 1260a18a..49e2e89c 100644 --- a/src/tagstudio/qt/widgets/video_player.py +++ b/src/tagstudio/qt/widgets/video_player.py @@ -15,26 +15,19 @@ from PySide6.QtCore import ( QUrl, QVariantAnimation, ) -from PySide6.QtGui import ( - QAction, - QBitmap, - QBrush, - QColor, - QPen, - QRegion, - QResizeEvent, -) +from PySide6.QtGui import QAction, QBitmap, QBrush, QColor, QPen, QRegion, QResizeEvent from PySide6.QtMultimedia import QAudioOutput, QMediaDevices, QMediaPlayer from PySide6.QtMultimediaWidgets import QGraphicsVideoItem from PySide6.QtSvgWidgets import QSvgWidget from PySide6.QtWidgets import QGraphicsScene, QGraphicsView -from src.core.enums import SettingItems -from src.qt.helpers.file_opener import FileOpenerHelper -from src.qt.platform_strings import open_file_str -from src.qt.translations import Translations + +from tagstudio.core.enums import SettingItems +from tagstudio.qt.helpers.file_opener import FileOpenerHelper +from tagstudio.qt.platform_strings import open_file_str +from tagstudio.qt.translations import Translations if typing.TYPE_CHECKING: - from src.qt.ts_qt import QtDriver + from tagstudio.qt.ts_qt import QtDriver class VideoPlayer(QGraphicsView): @@ -120,7 +113,7 @@ class VideoPlayer(QGraphicsView): autoplay_action.setCheckable(True) self.addAction(autoplay_action) autoplay_action.setChecked( - self.driver.settings.value(SettingItems.AUTOPLAY, defaultValue=True, type=bool) # type: ignore + self.driver.settings.value(SettingItems.AUTOPLAY, defaultValue=True, type=bool) ) autoplay_action.triggered.connect(lambda: self.toggle_autoplay()) self.autoplay = autoplay_action diff --git a/start_win.bat b/start_win.bat deleted file mode 100644 index 73dcfb0f..00000000 --- a/start_win.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -.venv\Scripts\python.exe .\TagStudio\tag_studio.py --ui qt %* \ No newline at end of file diff --git a/tagstudio.spec b/tagstudio.spec index 940b3e9d..bf83b61f 100644 --- a/tagstudio.spec +++ b/tagstudio.spec @@ -1,32 +1,33 @@ -# -*- mode: python ; coding: utf-8 -*- -# vi: ft=python - - +import platform from argparse import ArgumentParser -import sys + 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() -parser.add_argument('--portable', action='store_true') +parser.add_argument("--portable", action="store_true") options = parser.parse_args() +with open("pyproject.toml", "rb") as file: + pyproject = load(file)["project"] -name = 'TagStudio' if sys.platform == 'win32' else 'tagstudio' +system = platform.system() + +name = pyproject["name"] if system == "Windows" else "tagstudio" icon = None -if sys.platform == 'win32': - icon = 'tagstudio/resources/icon.ico' -elif sys.platform == 'darwin': - icon = 'tagstudio/resources/icon.icns' +if system == "Windows": + icon = "src/tagstudio/resources/icon.ico" +elif system == "Darwin": + icon = "src/tagstudio/resources/icon.icns" a = Analysis( - ['tagstudio/tag_studio.py'], + ["src/tagstudio/main.py"], pathex=[], binaries=[], - datas=[('tagstudio/resources', 'resources'), ('tagstudio/src', 'src')], + datas=[("src/tagstudio", "tagstudio")], hiddenimports=[], hookspath=[], hooksconfig={}, @@ -47,7 +48,7 @@ exe = EXE( [], bootloader_ignore_signals=False, console=False, - hide_console='hide-early', + hide_console="hide-early", disable_windowed_traceback=False, debug=False, name=name, @@ -60,27 +61,34 @@ exe = EXE( strip=False, upx=True, upx_exclude=[], - runtime_tmpdir=None + runtime_tmpdir=None, ) -coll = None if options.portable else COLLECT( - exe, - a.binaries, - a.datas, - name=name, - strip=False, - upx=True, - upx_exclude=[], +coll = ( + None + if options.portable + else COLLECT( + exe, + a.binaries, + a.datas, + name=name, + strip=False, + upx=True, + upx_exclude=[], + ) ) -app = BUNDLE( - exe if coll is None else coll, - name='TagStudio.app', - icon=icon, - bundle_identifier='com.cyanvoxel.tagstudio', - version='9.5.1', - info_plist={ - 'NSAppleScriptEnabled': False, - 'NSPrincipalClass': 'NSApplication', - } -) +if system == "Darwin": + app = BUNDLE( + exe if coll is None else coll, + name=f"{pyproject['name']}.app", + icon=icon, + bundle_identifier="com.cyanvoxel.tagstudio", + version=pyproject["version"], + info_plist={ + "NSAppleScriptEnabled": False, + "NSPrincipalClass": "NSApplication", + }, + ) + +# vi: ft=python diff --git a/tests/conftest.py b/tests/conftest.py index 15f1cd0f..989ecad6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,9 +9,9 @@ CWD = Path(__file__).parent # this needs to be above `src` imports sys.path.insert(0, str(CWD.parent)) -from src.core.library import Entry, Library, Tag -from src.core.library import alchemy as backend -from src.qt.ts_qt import QtDriver +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.library.alchemy.models import Entry, Tag +from tagstudio.qt.ts_qt import QtDriver @pytest.fixture @@ -137,8 +137,8 @@ def qt_driver(qtbot, library): open = Path(tmp_dir) ci = True - with patch("src.qt.ts_qt.Consumer"), patch("src.qt.ts_qt.CustomRunnable"): - driver = QtDriver(backend, Args()) + with patch("tagstudio.qt.ts_qt.Consumer"), patch("tagstudio.qt.ts_qt.CustomRunnable"): + driver = QtDriver(Args()) driver.main_window = Mock() driver.preview_panel = Mock() diff --git a/tests/macros/test_dupe_entries.py b/tests/macros/test_dupe_entries.py index 67247c45..6e36c819 100644 --- a/tests/macros/test_dupe_entries.py +++ b/tests/macros/test_dupe_entries.py @@ -1,7 +1,7 @@ from pathlib import Path -from src.core.library import Entry -from src.core.utils.dupe_files import DupeRegistry +from tagstudio.core.library.alchemy.models import Entry +from tagstudio.core.utils.dupe_files import DupeRegistry CWD = Path(__file__).parent diff --git a/tests/macros/test_folders_tags.py b/tests/macros/test_folders_tags.py index 669124b8..c22223a1 100644 --- a/tests/macros/test_folders_tags.py +++ b/tests/macros/test_folders_tags.py @@ -1,4 +1,4 @@ -from src.qt.modals.folders_to_tags import folders_to_tags +from tagstudio.qt.modals.folders_to_tags import folders_to_tags def test_folders_to_tags(library): diff --git a/tests/macros/test_missing_files.py b/tests/macros/test_missing_files.py index 283d358b..05f0c9ee 100644 --- a/tests/macros/test_missing_files.py +++ b/tests/macros/test_missing_files.py @@ -2,9 +2,10 @@ from pathlib import Path from tempfile import TemporaryDirectory import pytest -from src.core.library import Library -from src.core.library.alchemy.enums import FilterState -from src.core.utils.missing_files import MissingRegistry + +from tagstudio.core.library.alchemy.enums import FilterState +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.utils.missing_files import MissingRegistry CWD = Path(__file__).parent diff --git a/tests/macros/test_refresh_dir.py b/tests/macros/test_refresh_dir.py index 6359d63f..3df513c3 100644 --- a/tests/macros/test_refresh_dir.py +++ b/tests/macros/test_refresh_dir.py @@ -2,8 +2,9 @@ from pathlib import Path from tempfile import TemporaryDirectory import pytest -from src.core.enums import LibraryPrefs -from src.core.utils.refresh_dir import RefreshDirTracker + +from tagstudio.core.enums import LibraryPrefs +from tagstudio.core.utils.refresh_dir import RefreshDirTracker CWD = Path(__file__).parent diff --git a/tests/macros/test_sidecar.py b/tests/macros/test_sidecar.py index 3ed35f2c..61425787 100644 --- a/tests/macros/test_sidecar.py +++ b/tests/macros/test_sidecar.py @@ -3,8 +3,8 @@ # from tempfile import TemporaryDirectory # import pytest -# from src.core.enums import MacroID -# from src.core.library.alchemy.fields import _FieldID +# from tagstudio.core.enums import MacroID +# from tagstudio.core.library.alchemy.fields import _FieldID # @pytest.mark.parametrize("library", [TemporaryDirectory()], indirect=True) diff --git a/tests/qt/test_build_tag_panel.py b/tests/qt/test_build_tag_panel.py index e631d133..04f77ff0 100644 --- a/tests/qt/test_build_tag_panel.py +++ b/tests/qt/test_build_tag_panel.py @@ -1,6 +1,6 @@ -from src.core.library.alchemy.models import Tag -from src.qt.modals.build_tag import BuildTagPanel -from src.qt.translations import Translations +from tagstudio.core.library.alchemy.models import Tag +from tagstudio.qt.modals.build_tag import BuildTagPanel +from tagstudio.qt.translations import Translations def test_build_tag_panel_add_sub_tag_callback(library, generate_tag): diff --git a/tests/qt/test_field_containers.py b/tests/qt/test_field_containers.py index 12bffd67..5e583c1a 100644 --- a/tests/qt/test_field_containers.py +++ b/tests/qt/test_field_containers.py @@ -1,4 +1,4 @@ -from src.qt.widgets.preview_panel import PreviewPanel +from tagstudio.qt.widgets.preview_panel import PreviewPanel def test_update_selection_empty(qt_driver, library): diff --git a/tests/qt/test_flow_widget.py b/tests/qt/test_flow_widget.py index 4ec7119f..a03f2116 100644 --- a/tests/qt/test_flow_widget.py +++ b/tests/qt/test_flow_widget.py @@ -1,6 +1,7 @@ from PySide6.QtCore import QRect from PySide6.QtWidgets import QPushButton, QWidget -from src.qt.flowlayout import FlowLayout + +from tagstudio.qt.flowlayout import FlowLayout def test_flow_layout_happy_path(qtbot): diff --git a/tests/qt/test_folders_to_tags.py b/tests/qt/test_folders_to_tags.py index b7f079f1..5e47a383 100644 --- a/tests/qt/test_folders_to_tags.py +++ b/tests/qt/test_folders_to_tags.py @@ -1,4 +1,4 @@ -from src.qt.modals.folders_to_tags import generate_preview_data +from tagstudio.qt.modals.folders_to_tags import generate_preview_data def test_generate_preview_data(library, snapshot): diff --git a/tests/qt/test_item_thumb.py b/tests/qt/test_item_thumb.py index 09c527a0..07884087 100644 --- a/tests/qt/test_item_thumb.py +++ b/tests/qt/test_item_thumb.py @@ -1,6 +1,7 @@ import pytest -from src.core.library import ItemType -from src.qt.widgets.item_thumb import BadgeType, ItemThumb + +from tagstudio.core.library.alchemy.enums import ItemType +from tagstudio.qt.widgets.item_thumb import BadgeType, ItemThumb @pytest.mark.parametrize("new_value", (True, False)) diff --git a/tests/qt/test_preview_panel.py b/tests/qt/test_preview_panel.py index 12b64af9..461c735c 100644 --- a/tests/qt/test_preview_panel.py +++ b/tests/qt/test_preview_panel.py @@ -1,4 +1,4 @@ -from src.qt.widgets.preview_panel import PreviewPanel +from tagstudio.qt.widgets.preview_panel import PreviewPanel def test_update_selection_empty(qt_driver, library): diff --git a/tests/qt/test_qt_driver.py b/tests/qt/test_qt_driver.py index c0545373..86b2c176 100644 --- a/tests/qt/test_qt_driver.py +++ b/tests/qt/test_qt_driver.py @@ -1,6 +1,6 @@ -from src.core.library.alchemy.enums import FilterState -from src.core.library.json.library import ItemType -from src.qt.widgets.item_thumb import ItemThumb +from tagstudio.core.library.alchemy.enums import FilterState +from tagstudio.core.library.json.library import ItemType +from tagstudio.qt.widgets.item_thumb import ItemThumb # def test_update_thumbs(qt_driver): # qt_driver.frame_content = [ diff --git a/tests/qt/test_tag_panel.py b/tests/qt/test_tag_panel.py index c0c31fda..cfd5af1f 100644 --- a/tests/qt/test_tag_panel.py +++ b/tests/qt/test_tag_panel.py @@ -1,5 +1,5 @@ -from src.core.library import Tag -from src.qt.modals.build_tag import BuildTagPanel +from tagstudio.core.library.alchemy.models import Tag +from tagstudio.qt.modals.build_tag import BuildTagPanel def test_tag_panel(qtbot, library): diff --git a/tests/qt/test_tag_search_panel.py b/tests/qt/test_tag_search_panel.py index 6ea6590d..01150de2 100644 --- a/tests/qt/test_tag_search_panel.py +++ b/tests/qt/test_tag_search_panel.py @@ -1,4 +1,4 @@ -from src.qt.modals.tag_search import TagSearchPanel +from tagstudio.qt.modals.tag_search import TagSearchPanel def test_update_tags(qtbot, library): diff --git a/tests/qt/test_thumb_renderer.py b/tests/qt/test_thumb_renderer.py index 8290ff05..721d0ffc 100644 --- a/tests/qt/test_thumb_renderer.py +++ b/tests/qt/test_thumb_renderer.py @@ -8,9 +8,10 @@ from pathlib import Path import pytest from PIL import Image -from src.qt.widgets.thumb_renderer import ThumbRenderer from syrupy.extensions.image import PNGImageSnapshotExtension +from tagstudio.qt.widgets.thumb_renderer import ThumbRenderer + @pytest.mark.parametrize( ["fixture_file", "thumbnailer"], diff --git a/tests/test_db_migrations.py b/tests/test_db_migrations.py index dd73440b..368fb412 100644 --- a/tests/test_db_migrations.py +++ b/tests/test_db_migrations.py @@ -2,12 +2,14 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + import shutil from pathlib import Path import pytest -from src.core.constants import TS_FOLDER_NAME -from src.core.library.alchemy.library import Library + +from tagstudio.core.constants import TS_FOLDER_NAME +from tagstudio.core.library.alchemy.library import Library CWD = Path(__file__) FIXTURES = "fixtures" diff --git a/tests/test_driver.py b/tests/test_driver.py index 240406f9..fc30a115 100644 --- a/tests/test_driver.py +++ b/tests/test_driver.py @@ -3,10 +3,11 @@ from pathlib import Path from tempfile import TemporaryDirectory from PySide6.QtCore import QSettings -from src.core.constants import TS_FOLDER_NAME -from src.core.driver import DriverMixin -from src.core.enums import SettingItems -from src.core.library.alchemy.library import LibraryStatus + +from tagstudio.core.constants import TS_FOLDER_NAME +from tagstudio.core.driver import DriverMixin +from tagstudio.core.enums import SettingItems +from tagstudio.core.library.alchemy.library import LibraryStatus class TestDriver(DriverMixin): diff --git a/tests/test_json_migration.py b/tests/test_json_migration.py index f956ba1f..a4040457 100644 --- a/tests/test_json_migration.py +++ b/tests/test_json_migration.py @@ -2,11 +2,12 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio + from pathlib import Path from time import time -from src.core.enums import LibraryPrefs -from src.qt.widgets.migration_modal import JsonMigrationModal +from tagstudio.core.enums import LibraryPrefs +from tagstudio.qt.widgets.migration_modal import JsonMigrationModal CWD = Path(__file__) diff --git a/tests/test_library.py b/tests/test_library.py index 549c5900..6ed6efe0 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -2,11 +2,12 @@ from pathlib import Path from tempfile import TemporaryDirectory import pytest -from src.core.enums import DefaultEnum, LibraryPrefs -from src.core.library.alchemy import Entry, Library -from src.core.library.alchemy.enums import FilterState -from src.core.library.alchemy.fields import TextField, _FieldID -from src.core.library.alchemy.models import Tag + +from tagstudio.core.enums import DefaultEnum, LibraryPrefs +from tagstudio.core.library.alchemy.enums import FilterState +from tagstudio.core.library.alchemy.fields import TextField, _FieldID +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.library.alchemy.models import Entry, Tag def test_library_add_alias(library, generate_tag): diff --git a/tests/test_search.py b/tests/test_search.py index 3af48799..98e3b8b4 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -1,7 +1,8 @@ import pytest -from src.core.library.alchemy.enums import FilterState -from src.core.library.alchemy.library import Library -from src.core.query_lang.util import ParsingError + +from tagstudio.core.library.alchemy.enums import FilterState +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.query_lang.util import ParsingError def verify_count(lib: Library, query: str, count: int): diff --git a/tests/test_translations.py b/tests/test_translations.py index 92c2941c..9ec004ad 100644 --- a/tests/test_translations.py +++ b/tests/test_translations.py @@ -5,7 +5,7 @@ import pytest import ujson as json CWD = Path(__file__).parent -TRANSLATION_DIR = CWD / ".." / "resources" / "translations" +TRANSLATION_DIR = CWD / ".." / "src" / "tagstudio" / "resources" / "translations" def load_translation(filename: str) -> dict[str, str]: @@ -17,6 +17,10 @@ def get_translation_filenames() -> list[tuple[str]]: return [(a.name,) for a in TRANSLATION_DIR.glob("*.json")] +def test_translation_dir(): + assert TRANSLATION_DIR.exists() + + def find_format_keys(format_string: str) -> set[str]: formatter = string.Formatter() return set([field[1] for field in formatter.parse(format_string) if field[1] is not None]) @@ -31,20 +35,18 @@ def test_format_key_validity(translation_filename: str): continue default_keys = find_format_keys(default_translation[key]) translation_keys = find_format_keys(translation[key]) - assert default_keys.issuperset( - translation_keys - ), f"Translation {translation_filename} for key {key} is using an invalid format key ({translation_keys.difference(default_keys)})" # noqa: E501 - assert translation_keys.issuperset( - default_keys - ), f"Translation {translation_filename} for key {key} is missing format keys ({default_keys.difference(translation_keys)})" # noqa: E501 + assert default_keys.issuperset(translation_keys), ( + f"Translation {translation_filename} for key {key} is using an invalid format key ({translation_keys.difference(default_keys)})" # noqa: E501 + ) + assert translation_keys.issuperset(default_keys), ( + f"Translation {translation_filename} for key {key} is missing format keys ({default_keys.difference(translation_keys)})" # noqa: E501 + ) @pytest.mark.parametrize(["translation_filename"], get_translation_filenames()) def test_for_unnecessary_translations(translation_filename: str): default_translation = load_translation("en.json") translation = load_translation(translation_filename) - assert set( - default_translation.keys() - ).issuperset( - translation.keys() - ), f"Translation {translation_filename} has unnecessary keys ({set(translation.keys()).difference(default_translation.keys())})" # noqa: E501 + assert set(default_translation.keys()).issuperset(translation.keys()), ( + f"Translation {translation_filename} has unnecessary keys ({set(translation.keys()).difference(default_translation.keys())})" # noqa: E501 + )