Merge branch 'master' into cccolors

This commit is contained in:
Zach H 2025-06-13 04:01:29 +02:00 committed by GitHub
commit f022ee793c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
78 changed files with 2299 additions and 1203 deletions

View file

@ -7,6 +7,7 @@ RUN pacman --sync --refresh --sysupgrade --needed --noconfirm \
git \ git \
gtest \ gtest \
mariadb-libs \ mariadb-libs \
ninja \
protobuf \ protobuf \
qt6-base \ qt6-base \
qt6-imageformats \ qt6-imageformats \

View file

@ -16,6 +16,7 @@ RUN apt-get update && \
libqt5sql5-mysql \ libqt5sql5-mysql \
libqt5svg5-dev \ libqt5svg5-dev \
libqt5websockets5-dev \ libqt5websockets5-dev \
ninja-build \
protobuf-compiler \ protobuf-compiler \
qt5-image-formats-plugins \ qt5-image-formats-plugins \
qtmultimedia5-dev \ qtmultimedia5-dev \

View file

@ -15,13 +15,14 @@ RUN apt-get update && \
libprotobuf-dev \ libprotobuf-dev \
libqt6multimedia6 \ libqt6multimedia6 \
libqt6sql6-mysql \ libqt6sql6-mysql \
qt6-svg-dev \ ninja-build \
qt6-websockets-dev \
protobuf-compiler \ protobuf-compiler \
qt6-image-formats-plugins \ qt6-image-formats-plugins \
qt6-l10n-tools \ qt6-l10n-tools \
qt6-multimedia-dev \ qt6-multimedia-dev \
qt6-svg-dev \
qt6-tools-dev \ qt6-tools-dev \
qt6-tools-dev-tools \ qt6-tools-dev-tools \
qt6-websockets-dev \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*

View file

@ -6,6 +6,7 @@ RUN dnf install -y \
gcc-c++ \ gcc-c++ \
git \ git \
mariadb-devel \ mariadb-devel \
ninja-build \
protobuf-devel \ protobuf-devel \
qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
qt6-qtimageformats \ qt6-qtimageformats \

View file

@ -6,6 +6,7 @@ RUN dnf install -y \
gcc-c++ \ gcc-c++ \
git \ git \
mariadb-devel \ mariadb-devel \
ninja-build \
protobuf-devel \ protobuf-devel \
qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
qt6-qtimageformats \ qt6-qtimageformats \

View file

@ -1,26 +0,0 @@
FROM ubuntu:20.04
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential \
ccache \
clang-format \
cmake \
file \
g++ \
git \
liblzma-dev \
libmariadb-dev-compat \
libprotobuf-dev \
libqt5multimedia5-plugins \
libqt5sql5-mysql \
libqt5svg5-dev \
libqt5websockets5-dev \
protobuf-compiler \
qt5-default \
qt5-image-formats-plugins \
qtmultimedia5-dev \
qttools5-dev \
qttools5-dev-tools \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

View file

@ -17,6 +17,7 @@ RUN apt-get update && \
libqt6sql6-mysql \ libqt6sql6-mysql \
libqt6svg6-dev \ libqt6svg6-dev \
libqt6websockets6-dev \ libqt6websockets6-dev \
ninja-build \
protobuf-compiler \ protobuf-compiler \
qt6-image-formats-plugins \ qt6-image-formats-plugins \
qt6-l10n-tools \ qt6-l10n-tools \

View file

@ -15,13 +15,14 @@ RUN apt-get update && \
libprotobuf-dev \ libprotobuf-dev \
libqt6multimedia6 \ libqt6multimedia6 \
libqt6sql6-mysql \ libqt6sql6-mysql \
qt6-svg-dev \ ninja-build \
qt6-websockets-dev \
protobuf-compiler \ protobuf-compiler \
qt6-image-formats-plugins \ qt6-image-formats-plugins \
qt6-l10n-tools \ qt6-l10n-tools \
qt6-multimedia-dev \ qt6-multimedia-dev \
qt6-svg-dev \
qt6-tools-dev \ qt6-tools-dev \
qt6-tools-dev-tools \ qt6-tools-dev-tools \
qt6-websockets-dev \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*

View file

@ -11,9 +11,8 @@
# --debug or --release sets the build type ie CMAKE_BUILD_TYPE # --debug or --release sets the build type ie CMAKE_BUILD_TYPE
# --ccache [<size>] uses ccache and shows stats, optionally provide size # --ccache [<size>] uses ccache and shows stats, optionally provide size
# --dir <dir> sets the name of the build dir, default is "build" # --dir <dir> sets the name of the build dir, default is "build"
# --parallel <core count> sets how many cores cmake should build with in parallel # uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_TEST USE_CCACHE CCACHE_SIZE BUILD_DIR CMAKE_GENERATOR
# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_TEST USE_CCACHE CCACHE_SIZE BUILD_DIR PARALLEL_COUNT # (correspond to args: --debug/--release --install --package <package type> --suffix <suffix> --server --test --ccache <ccache_size> --dir <dir>)
# (correspond to args: --debug/--release --install --package <package type> --suffix <suffix> --server --test --ccache <ccache_size> --dir <dir> --parallel <core_count>)
# exitcode: 1 for failure, 3 for invalid arguments # exitcode: 1 for failure, 3 for invalid arguments
# Read arguments # Read arguments
@ -76,15 +75,6 @@ while [[ $# != 0 ]]; do
BUILD_DIR="$1" BUILD_DIR="$1"
shift shift
;; ;;
'--parallel')
shift
if [[ $# == 0 ]]; then
echo "::error file=$0::--parallel expects an argument"
exit 3
fi
PARALLEL_COUNT="$1"
shift
;;
*) *)
echo "::error file=$0::unrecognized option: $1" echo "::error file=$0::unrecognized option: $1"
exit 3 exit 3
@ -126,16 +116,6 @@ fi
# Add cmake --build flags # Add cmake --build flags
buildflags=(--config "$BUILDTYPE") buildflags=(--config "$BUILDTYPE")
if [[ $PARALLEL_COUNT ]]; then
if [[ $(cmake --build /not_a_dir --parallel 2>&1 | head -1) =~ parallel ]]; then
# workaround for bionic having an old cmake
echo "this version of cmake does not support --parallel, using native build tool -j instead"
buildflags+=(-- -j "$PARALLEL_COUNT")
# note, no normal build flags should be added after this
else
buildflags+=(--parallel "$PARALLEL_COUNT")
fi
fi
function ccachestatsverbose() { function ccachestatsverbose() {
# note, verbose only works on newer ccache, discard the error # note, verbose only works on newer ccache, discard the error

View file

@ -148,6 +148,9 @@ function RUN ()
args+=(--mount "type=bind,source=$CCACHE_DIR,target=/.ccache") args+=(--mount "type=bind,source=$CCACHE_DIR,target=/.ccache")
args+=(--env "CCACHE_DIR=/.ccache") args+=(--env "CCACHE_DIR=/.ccache")
fi fi
if [[ -n "$CMAKE_GENERATOR" ]]; then
args+=(--env "CMAKE_GENERATOR=$CMAKE_GENERATOR")
fi
docker run "${args[@]}" $RUN_ARGS "$IMAGE_NAME" bash "$BUILD_SCRIPT" $RUN_OPTS "$@" docker run "${args[@]}" $RUN_ARGS "$IMAGE_NAME" bash "$BUILD_SCRIPT" $RUN_OPTS "$@"
return $? return $?
else else

View file

@ -20,7 +20,6 @@ Available pre-compiled binaries for installation:
<b>Linux</b> <b>Linux</b>
<kbd>Ubuntu 24.04 LTS</kbd> <sub><i>Noble Numbat</i></sub> <kbd>Ubuntu 24.04 LTS</kbd> <sub><i>Noble Numbat</i></sub>
<kbd>Ubuntu 22.04 LTS</kbd> <sub><i>Jammy Jellyfish</i></sub> <kbd>Ubuntu 22.04 LTS</kbd> <sub><i>Jammy Jellyfish</i></sub>
<kbd>Ubuntu 20.04 LTS</kbd> <sub><i>Focal Fossa</i></sub>
<kbd>Debian 12</kbd> <sub><i>Bookworm</i></sub> <kbd>Debian 12</kbd> <sub><i>Bookworm</i></sub>
<kbd>Debian 11</kbd> <sub><i>Bullseye</i></sub> <kbd>Debian 11</kbd> <sub><i>Bullseye</i></sub>
<kbd>Fedora 42</kbd> <kbd>Fedora 42</kbd>

View file

@ -10,5 +10,5 @@ Last changes are based on commit {{ .commit }}.
*This PR is automatically generated and updated by the workflow at `.github/workflows/translations-push.yml`. Review [action runs][2].*<br> *This PR is automatically generated and updated by the workflow at `.github/workflows/translations-push.yml`. Review [action runs][2].*<br>
*After merging, all changes to the source language are available for translation at [Transifex][1] shortly.* *After merging, all changes to the source language are available for translation at [Transifex][1] shortly.*
[1]: https://app.transifex.com/cockatrice/cockatrice/ [1]: https://explore.transifex.com/cockatrice/cockatrice/
[2]: https://github.com/Cockatrice/Cockatrice/actions/workflows/translations-push.yml?query=branch%3Amaster [2]: https://github.com/Cockatrice/Cockatrice/actions/workflows/translations-push.yml?query=branch%3Amaster

View file

@ -9,14 +9,24 @@ assignees: ''
--- ---
<!-- READ THIS BEFORE POSTING <!-- READ THIS BEFORE POSTING
Go to "Help → View Debug Log" in Cockatrice and copy all information at the In Cockatrice, go to "Help" → "View Debug Log" and copy all information displayed at the
top (above the separation line) below "System Information" in this ticket! top (above the separation line "----"), to below "System Information" section in this ticket!
If you can't start Cockatrice to access these details, make If you can't start Cockatrice to access these details, make
sure to post your OS and the file name of the setup binary instead. --> sure to post your OS and the file name of the setup binary instead.
-->
**System Information:** **System Information:**
<!-- Read the hint above on where to find the important information to provide here! -->
<details><summary>Debug Log:</summary>
<!--
In Cockatrice, go to "Help" → "View Debug Log", click the "Copy to clickboard" button and paste the output here.
-->
</details>
_______________________________________________________________________________________ _______________________________________________________________________________________
<!-- Explain your issue in detail here! Please attach screenshots if possible. --> <!-- Explain your issue in detail here! Please attach screenshots if possible. -->
@ -26,7 +36,7 @@ ________________________________________________________________________________
_______________________________________________________________________________________ _______________________________________________________________________________________
<!-- Describe the sequence of actions needed to experience the bug --> <!-- Describe the sequence of actions needed to experience the bug. -->
**Steps to reproduce:** **Steps to reproduce:**
- Do A - Do A

View file

@ -4,6 +4,6 @@ contact_links:
url: https://discord.gg/3Z9yzmA url: https://discord.gg/3Z9yzmA
about: Need help with using the client? Want to find some games? Try the Discord server! about: Need help with using the client? Want to find some games? Try the Discord server!
- name: 🌐 Translations (Help improve the localization of the app) - name: 🌐 Translations (Help improve the localization of the app)
url: https://www.transifex.com/cockatrice/cockatrice/ url: https://explore.transifex.com/cockatrice/cockatrice/
# it is not possible to add a link to the wiki to this description # it is not possible to add a link to the wiki to this description
about: For more information and guidance check our Translation FAQ on our wiki! about: For more information and guidance check our Translation FAQ on our wiki!

View file

@ -110,11 +110,6 @@ jobs:
version: 42 version: 42
package: RPM package: RPM
- distro: Ubuntu
version: 20.04
package: DEB
test: skip # Ubuntu 20.04 has a broken Qt for debug builds
- distro: Ubuntu - distro: Ubuntu
version: 22.04 version: 22.04
package: DEB package: DEB
@ -130,29 +125,25 @@ jobs:
continue-on-error: ${{matrix.allow-failure == 'yes'}} continue-on-error: ${{matrix.allow-failure == 'yes'}}
env: env:
NAME: ${{matrix.distro}}${{matrix.version}} NAME: ${{matrix.distro}}${{matrix.version}}
CACHE: /tmp/${{matrix.distro}}${{matrix.version}}-cache # ${{runner.temp}} does not work? CACHE: ${{github.workspace}}/.cache/${{matrix.distro}}${{matrix.version}} # directory for caching docker image and ccache
# Cache size over the entire repo is 10Gi: # Cache size over the entire repo is 10Gi:
# https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
CCACHE_SIZE: 500M CCACHE_SIZE: 500M
CMAKE_GENERATOR: 'Ninja'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Generate cache timestamp - name: Restore compiler cache (ccache)
id: cache_timestamp id: ccache_restore
shell: bash uses: actions/cache/restore@v4
run: echo "timestamp=$(date -u '+%Y%m%d%H%M%S')" >>"$GITHUB_OUTPUT"
- name: Restore cache
uses: actions/cache@v4
env: env:
timestamp: ${{steps.cache_timestamp.outputs.timestamp}} BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
with: with:
path: ${{env.CACHE}} path: ${{env.CACHE}}
key: docker-${{matrix.distro}}${{matrix.version}}-cache-${{env.timestamp}} key: ccache-${{matrix.distro}}${{matrix.version}}-${{env.BRANCH_NAME}}
restore-keys: | restore-keys: ccache-${{matrix.distro}}${{matrix.version}}-
docker-${{matrix.distro}}${{matrix.version}}-cache-
- name: Build ${{matrix.distro}} ${{matrix.version}} Docker image - name: Build ${{matrix.distro}} ${{matrix.version}} Docker image
shell: bash shell: bash
@ -161,9 +152,11 @@ jobs:
- name: Build debug and test - name: Build debug and test
if: matrix.test != 'skip' if: matrix.test != 'skip'
shell: bash shell: bash
env:
CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}'
run: | run: |
source .ci/docker.sh source .ci/docker.sh
RUN --server --debug --test --ccache "$CCACHE_SIZE" --parallel 4 RUN --server --debug --test --ccache "$CCACHE_SIZE"
- name: Build release package - name: Build release package
id: build id: build
@ -172,14 +165,23 @@ jobs:
env: env:
BUILD_DIR: build BUILD_DIR: build
SUFFIX: '-${{matrix.distro}}${{matrix.version}}' SUFFIX: '-${{matrix.distro}}${{matrix.version}}'
type: '${{matrix.package}}' package: '${{matrix.package}}'
CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}'
run: | run: |
source .ci/docker.sh source .ci/docker.sh
RUN --server --release --package "$type" --dir "$BUILD_DIR" \ RUN --server --release --package "$package" --dir "$BUILD_DIR" \
--ccache "$CCACHE_SIZE" --parallel 4 --ccache "$CCACHE_SIZE"
.ci/name_build.sh .ci/name_build.sh
- name: Save compiler cache (ccache)
if: github.ref == 'refs/heads/master'
uses: actions/cache/save@v4
with:
path: ${{env.CACHE}}
key: ${{ steps.ccache_restore.outputs.cache-primary-key }}
- name: Upload artifact - name: Upload artifact
id: upload_artifact
if: matrix.package != 'skip' if: matrix.package != 'skip'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
@ -188,6 +190,7 @@ jobs:
if-no-files-found: error if-no-files-found: error
- name: Upload to release - name: Upload to release
id: upload_release
if: matrix.package != 'skip' && needs.configure.outputs.tag != null if: matrix.package != 'skip' && needs.configure.outputs.tag != null
shell: bash shell: bash
env: env:
@ -197,6 +200,20 @@ jobs:
asset_name: ${{steps.build.outputs.name}} asset_name: ${{steps.build.outputs.name}}
run: gh release upload "$tag_name" "$asset_path#$asset_name" run: gh release upload "$tag_name" "$asset_path#$asset_name"
- name: Attest binary provenance
id: attestation
if: steps.upload_release.outcome == 'success'
uses: actions/attest-build-provenance@v2
with:
subject-path: ${{steps.build.outputs.path}}
subject-name: ${{steps.build.outputs.name}}
subject-digest: sha256:${{ steps.upload_artifact.outputs.artifact-digest }}
- name: Verify binary attestation
if: steps.attestation.outcome == 'success'
shell: bash
run: gh attestation verify ${{steps.build.outputs.path}} -R Cockatrice/Cockatrice
build-macos: build-macos:
strategy: strategy:
fail-fast: false fail-fast: false
@ -207,7 +224,6 @@ jobs:
os: macos-13 os: macos-13
xcode: "14.3.1" xcode: "14.3.1"
type: Release type: Release
core_count: 4
make_package: 1 make_package: 1
- target: 14 - target: 14
@ -215,7 +231,6 @@ jobs:
os: macos-14 os: macos-14
xcode: "15.4" xcode: "15.4"
type: Release type: Release
core_count: 3
make_package: 1 make_package: 1
- target: 15 - target: 15
@ -223,7 +238,6 @@ jobs:
os: macos-15 os: macos-15
xcode: "16.2" xcode: "16.2"
type: Release type: Release
core_count: 3
make_package: 1 make_package: 1
- target: 15 - target: 15
@ -231,15 +245,17 @@ jobs:
os: macos-15 os: macos-15
xcode: "16.2" xcode: "16.2"
type: Debug type: Debug
core_count: 3
name: macOS ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} name: macOS ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
needs: configure needs: configure
runs-on: ${{matrix.os}} runs-on: ${{matrix.os}}
continue-on-error: ${{matrix.allow-failure == 'yes'}} continue-on-error: ${{matrix.allow-failure == 'yes'}}
env: env:
CCACHE_DIR: ${{github.workspace}}/.ccache/${{matrix.os}}-${{matrix.type}}
CCACHE_SIZE: 500M
DEVELOPER_DIR: DEVELOPER_DIR:
/Applications/Xcode_${{matrix.xcode}}.app/Contents/Developer /Applications/Xcode_${{matrix.xcode}}.app/Contents/Developer
CMAKE_GENERATOR: 'Ninja'
steps: steps:
- name: Checkout - name: Checkout
@ -253,23 +269,30 @@ jobs:
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
run: | run: |
brew update brew update
brew install protobuf qt --force-bottle brew install ccache protobuf qt --force-bottle
- name: Restore compiler cache (ccache)
id: ccache_restore
uses: actions/cache/restore@v4
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
with:
path: ${{env.CCACHE_DIR}}
key: ccache-${{matrix.os}}-${{matrix.type}}-${{env.BRANCH_NAME}}
restore-keys: ccache-${{matrix.os}}-${{matrix.type}}-
- name: Build & Sign on Xcode ${{matrix.xcode}} - name: Build & Sign on Xcode ${{matrix.xcode}}
shell: bash shell: bash
id: build id: build
env: env:
BUILDTYPE: '${{matrix.type}}' BUILDTYPE: '${{matrix.type}}'
MAKE_TEST: 1
MAKE_PACKAGE: '${{matrix.make_package}}' MAKE_PACKAGE: '${{matrix.make_package}}'
PACKAGE_SUFFIX: '-macOS${{matrix.target}}_${{matrix.soc}}' PACKAGE_SUFFIX: '-macOS${{matrix.target}}_${{matrix.soc}}'
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }} MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }} MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }} MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
# macOS runner have 3 cores usually - only the macos-13 image has 4: CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}'
# https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories
# https://github.com/actions/runner-images?tab=readme-ov-file#available-images
run: | run: |
if [[ -n "$MACOS_CERTIFICATE_NAME" ]] if [[ -n "$MACOS_CERTIFICATE_NAME" ]]
then then
@ -281,10 +304,17 @@ jobs:
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain
fi fi
.ci/compile.sh --server --parallel ${{matrix.core_count}} .ci/compile.sh --server --test --ccache "$CCACHE_SIZE"
- name: Save compiler cache (ccache)
if: github.ref == 'refs/heads/master'
uses: actions/cache/save@v4
with:
path: ${{env.CCACHE_DIR}}
key: ${{ steps.ccache_restore.outputs.cache-primary-key }}
- name: Sign app bundle - name: Sign app bundle
if: matrix.make_package if: matrix.make_package && (github.ref == 'refs/heads/master' || needs.configure.outputs.tag != null)
env: env:
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }} MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
@ -296,7 +326,7 @@ jobs:
fi fi
- name: Notarize app bundle - name: Notarize app bundle
if: matrix.make_package if: matrix.make_package && (github.ref == 'refs/heads/master' || needs.configure.outputs.tag != null)
env: env:
MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
@ -328,6 +358,7 @@ jobs:
fi fi
- name: Upload artifact - name: Upload artifact
id: upload_artifact
if: matrix.make_package if: matrix.make_package
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
@ -336,7 +367,8 @@ jobs:
if-no-files-found: error if-no-files-found: error
- name: Upload to release - name: Upload to release
if: matrix.package != 'skip' && needs.configure.outputs.tag != null id: upload_release
if: matrix.make_package && needs.configure.outputs.tag != null
shell: bash shell: bash
env: env:
GH_TOKEN: ${{github.token}} GH_TOKEN: ${{github.token}}
@ -345,6 +377,20 @@ jobs:
asset_name: ${{steps.build.outputs.name}} asset_name: ${{steps.build.outputs.name}}
run: gh release upload "$tag_name" "$asset_path#$asset_name" run: gh release upload "$tag_name" "$asset_path#$asset_name"
- name: Attest binary provenance
id: attestation
if: steps.upload_release.outcome == 'success'
uses: actions/attest-build-provenance@v2
with:
subject-path: ${{steps.build.outputs.path}}
subject-name: ${{steps.build.outputs.name}}
subject-digest: sha256:${{ steps.upload_artifact.outputs.artifact-digest }}
- name: Verify binary attestation
if: steps.attestation.outcome == 'success'
shell: bash
run: gh attestation verify ${{steps.build.outputs.path}} -R Cockatrice/Cockatrice
build-windows: build-windows:
strategy: strategy:
fail-fast: false fail-fast: false
@ -387,16 +433,11 @@ jobs:
tools: ${{matrix.qt_tools}} tools: ${{matrix.qt_tools}}
modules: ${{matrix.qt_modules}} modules: ${{matrix.qt_modules}}
# TODO: re-enable when https://github.com/lukka/run-vcpkg/issues/243 is fixed - name: Setup vcpkg cache
- if: false id: vcpkg-cache
name: Run vcpkg (disabled) uses: TAServers/vcpkg-cache@v3
uses: lukka/run-vcpkg@v11
with: with:
runVcpkgInstall: true token: ${{ secrets.GITHUB_TOKEN }}
doNotCache: false
env:
VCPKG_DEFAULT_TRIPLET: 'x64-windows'
VCPKG_DISABLE_METRICS: 1
- name: Build Cockatrice - name: Build Cockatrice
id: build id: build
@ -406,11 +447,14 @@ jobs:
CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}' CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}'
CMAKE_GENERATOR_PLATFORM: 'x64' CMAKE_GENERATOR_PLATFORM: 'x64'
QTDIR: '${{github.workspace}}\Qt\${{matrix.qt_version}}\win64_${{matrix.qt_arch}}' QTDIR: '${{github.workspace}}\Qt\${{matrix.qt_version}}\win64_${{matrix.qt_arch}}'
VCPKG_DISABLE_METRICS: 1
VCPKG_BINARY_SOURCES: 'clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite'
# No need for --parallel flag, MTT is added in the compile script to let cmake/msbuild manage core count, # No need for --parallel flag, MTT is added in the compile script to let cmake/msbuild manage core count,
# project and process parallelism: https://devblogs.microsoft.com/cppblog/improved-parallelism-in-msbuild/ # project and process parallelism: https://devblogs.microsoft.com/cppblog/improved-parallelism-in-msbuild/
run: .ci/compile.sh --server --release --test --package run: .ci/compile.sh --server --release --test --package
- name: Upload artifact - name: Upload artifact
id: upload_artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: Windows${{matrix.target}}-installer name: Windows${{matrix.target}}-installer
@ -427,7 +471,8 @@ jobs:
if-no-files-found: error if-no-files-found: error
- name: Upload to release - name: Upload to release
if: matrix.package != 'skip' && needs.configure.outputs.tag != null id: upload_release
if: needs.configure.outputs.tag != null
shell: bash shell: bash
env: env:
GH_TOKEN: ${{github.token}} GH_TOKEN: ${{github.token}}
@ -435,3 +480,17 @@ jobs:
asset_path: ${{steps.build.outputs.path}} asset_path: ${{steps.build.outputs.path}}
asset_name: ${{steps.build.outputs.name}} asset_name: ${{steps.build.outputs.name}}
run: gh release upload "$tag_name" "$asset_path#$asset_name" run: gh release upload "$tag_name" "$asset_path#$asset_name"
- name: Attest binary provenance
id: attestation
if: steps.upload_release.outcome == 'success'
uses: actions/attest-build-provenance@v2
with:
subject-path: ${{steps.build.outputs.path}}
subject-name: ${{steps.build.outputs.name}}
subject-digest: sha256:${{ steps.upload_artifact.outputs.artifact-digest }}
- name: Verify binary attestation
if: steps.attestation.outcome == 'success'
shell: bash
run: gh attestation verify ${{steps.build.outputs.path}} -R Cockatrice/Cockatrice

View file

@ -51,7 +51,7 @@ jobs:
*This PR is automatically generated and updated by the workflow at `.github/workflows/translations-pull.yml`. Review [action runs][2].*<br> *This PR is automatically generated and updated by the workflow at `.github/workflows/translations-pull.yml`. Review [action runs][2].*<br>
*After merging, all new languages and translations are available in the next build.* *After merging, all new languages and translations are available in the next build.*
[1]: https://app.transifex.com/cockatrice/cockatrice/ [1]: https://explore.transifex.com/cockatrice/cockatrice/
[2]: https://github.com/Cockatrice/Cockatrice/actions/workflows/translations-pull.yml?query=branch%3Amaster [2]: https://github.com/Cockatrice/Cockatrice/actions/workflows/translations-pull.yml?query=branch%3Amaster
labels: | labels: |
CI CI

View file

@ -86,9 +86,9 @@ Cockatrice tries to use the [Google Developer Documentation Style Guide](https:/
<sub><i>Made with <a href="https://contrib.rocks">contrib.rocks</a></i></sub> <sub><i>Made with <a href="https://contrib.rocks">contrib.rocks</a></i></sub>
</details> </details>
### Translations [![Transifex Project](https://img.shields.io/badge/translate-on%20transifex-brightgreen)](https://transifex.com/cockatrice/cockatrice/) ### Translations [![Transifex Project](https://img.shields.io/badge/translate-on%20transifex-brightgreen)](https://explore.transifex.com/cockatrice/cockatrice/)
Cockatrice uses Transifex to manage translations. You can help us bring <kbd>Cockatrice</kbd>, <kbd>Oracle</kbd> and <kbd>Webatrice</kbd> to your language and just adjust single wordings right from within your browser by visiting our [Transifex project page](https://transifex.com/cockatrice/cockatrice/).<br> Cockatrice uses Transifex to manage translations. You can help us bring <kbd>Cockatrice</kbd>, <kbd>Oracle</kbd> and <kbd>Webatrice</kbd> to your language and just adjust single wordings right from within your browser by visiting our [Transifex project page](https://explore.transifex.com/cockatrice/cockatrice/).<br>
Check out our [Translator FAQ](https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ) for more information about getting invovled, and join a group of hundreds of others!<br> Check out our [Translator FAQ](https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ) for more information about getting invovled, and join a group of hundreds of others!<br>

View file

@ -67,7 +67,10 @@ set(cockatrice_SOURCES
src/client/ui/line_edit_completer.cpp src/client/ui/line_edit_completer.cpp
src/client/ui/phases_toolbar.cpp src/client/ui/phases_toolbar.cpp
src/client/ui/picture_loader/picture_loader.cpp src/client/ui/picture_loader/picture_loader.cpp
src/client/ui/picture_loader/picture_loader_request_status_display_widget.cpp
src/client/ui/picture_loader/picture_loader_status_bar.cpp
src/client/ui/picture_loader/picture_loader_worker.cpp src/client/ui/picture_loader/picture_loader_worker.cpp
src/client/ui/picture_loader/picture_loader_worker_work.cpp
src/client/ui/picture_loader/picture_to_load.cpp src/client/ui/picture_loader/picture_to_load.cpp
src/client/ui/pixel_map_generator.cpp src/client/ui/pixel_map_generator.cpp
src/client/ui/theme_manager.cpp src/client/ui/theme_manager.cpp
@ -190,6 +193,7 @@ set(cockatrice_SOURCES
src/game/cards/card_search_model.cpp src/game/cards/card_search_model.cpp
src/game/deckview/deck_view.cpp src/game/deckview/deck_view.cpp
src/game/deckview/deck_view_container.cpp src/game/deckview/deck_view_container.cpp
src/game/filters/deck_filter_string.cpp
src/game/filters/filter_builder.cpp src/game/filters/filter_builder.cpp
src/game/filters/filter_card.cpp src/game/filters/filter_card.cpp
src/game/filters/filter_string.cpp src/game/filters/filter_string.cpp

View file

@ -379,5 +379,6 @@
<file>resources/tips/tips_of_the_day.xml</file> <file>resources/tips/tips_of_the_day.xml</file>
<file>resources/help/search.md</file> <file>resources/help/search.md</file>
<file>resources/help/deck_search.md</file>
</qresource> </qresource>
</RCC> </RCC>

View file

@ -60,4 +60,6 @@
# pixel_map_generator = false # pixel_map_generator = false
# deck_filter_string = false
# filter_string = false # filter_string = false
# syntax_help = false

View file

@ -0,0 +1,40 @@
## Deck Search Syntax Help
-----
The search bar recognizes a set of special commands.<br>
In this list of examples below, each entry has an explanation and can be clicked to test the query. Note that all
searches are case insensitive.
<dl>
<dt>Display Name (The deck name, or the filename if the deck name isn't set):</dt>
<dd>[red deck wins](#red deck wins) <small>(Any deck with a display name containing the words red, deck, and wins)</small></dd>
<dd>["red deck wins"](#%22red deck wins%22) <small>(Any deck with a display name containing the exact phrase "red deck wins")</small></dd>
<dt>Deck <u>N</u>ame:</dt>
<dd>[n:aggro](#n:aggro) <small>(Any deck with a name containing the word aggro)</small></dd>
<dd>[n:red n:deck n:wins](#n:red n:deck n:wins) <small>(Any deck with a name containing the words red, deck, and wins)</small></dd>
<dd>[n:"red deck wins"](#n:%22red deck wins%22) <small>(Any deck with a name containing the exact phrase "red deck wins")</small></dd>
<dt><u>F</u>ile Name:</dt>
<dd>[f:aggro](#f:aggro) <small>(Any deck with a filename containing the word aggro)</small></dd>
<dd>[f:red f:deck f:wins](#f:red f:deck f:wins) <small>(Any deck with a filename containing the words red, deck, and wins)</small></dd>
<dd>[f:"red deck wins"](#f:%22red deck wins%22) <small>(Any deck with a filename containing the exact phrase "red deck wins")</small></dd>
<dt>Relative <u>P</u>ath (starting from the deck folder):</dt>
<dd>[p:aggro](#p:aggro) <small>(Any deck that has "aggro" somewhere in its relative path)</small></dd>
<dd>[p:edh/](#p:edh/) <small>(Any deck with "edh/" in its relative path, A.K.A. decks in the "edh" folder)</small></dd>
<dt>Deck Contents (Uses [card search expressions](#cardSearchSyntaxHelp)):</dt>
<dd><a href="#[[plains]]">[[plains]]</a> <small>(Any deck that contains at least one card with "plains" in its name)</small></dd>
<dd><a href="#[[t:legendary]]">[[t:legendary]]</a> <small>(Any deck that contains at least one legendary)</small></dd>
<dd><a href="#[[t:legendary]]>5">[[t:legendary]]>5</a> <small>(Any card that contains at least 5 legendaries)</small></dd>
<dd><a href="#[[]]:100">[[]]:100</a> <small>(Any deck that contains exactly 100 cards)</small></dd>
<dt>Negate:</dt>
<dd>[soldier -aggro](#soldier -aggro) <small>(Any deck filename that contains "soldier", but not "aggro")</small></dd>
<dt>Branching:</dt>
<dd>[t:aggro OR o:control](#t:aggro OR o:control) <small>(Any deck filename that contains either aggro or control)</small></dd>
<dt>Grouping:</dt>
<dd><a href="#red -([[]]:100 or aggro)">red -([[]]:100 or aggro)</a> <small>(Any deck that has red in its filename but is not 100 cards or has aggro in its filename)</small></dd>
</dl>

View file

@ -58,4 +58,9 @@ In this list of examples below, each entry has an explanation and can be clicked
<dt>Grouping:</dt> <dt>Grouping:</dt>
<dd><a href="#t:angel -(angel or c:w)">t:angel -(angel or c:w)</a> <small>(Any angel that doesn't have angel in its name and isn't white)</small></dd> <dd><a href="#t:angel -(angel or c:w)">t:angel -(angel or c:w)</a> <small>(Any angel that doesn't have angel in its name and isn't white)</small></dd>
<dt>Regular Expression:</dt>
<dd>[/^fell/](#/^fell/) <small>(Any card name that begins with "fell")</small></dd>
<dd>[o:/counter target .* spell/](#o:/counter target .* spell/) <small>(Any card text with "counter target *something* spell")</small></dd>
<dd>[o:/for each .* and\/or .*/](#o:/for each .* and\/or .*/) <small>(/'s can be escaped with a \)</small></dd>
</dl> </dl>

View file

@ -1538,7 +1538,7 @@ void TabGame::createPlayAreaWidget(bool bReplay)
scene = new GameScene(phasesToolbar, this); scene = new GameScene(phasesToolbar, this);
gameView = new GameView(scene); gameView = new GameView(scene);
gamePlayAreaVBox = new QVBoxLayout; auto gamePlayAreaVBox = new QVBoxLayout;
gamePlayAreaVBox->setContentsMargins(0, 0, 0, 0); gamePlayAreaVBox->setContentsMargins(0, 0, 0, 0);
gamePlayAreaVBox->addWidget(gameView); gamePlayAreaVBox->addWidget(gameView);
@ -1594,12 +1594,12 @@ void TabGame::createReplayDock()
connect(replayFastForwardButton, &QToolButton::toggled, this, &TabGame::replayFastForwardButtonToggled); connect(replayFastForwardButton, &QToolButton::toggled, this, &TabGame::replayFastForwardButtonToggled);
// putting everything together // putting everything together
replayControlLayout = new QHBoxLayout; auto replayControlLayout = new QHBoxLayout;
replayControlLayout->addWidget(timelineWidget, 10); replayControlLayout->addWidget(timelineWidget, 10);
replayControlLayout->addWidget(replayPlayButton); replayControlLayout->addWidget(replayPlayButton);
replayControlLayout->addWidget(replayFastForwardButton); replayControlLayout->addWidget(replayFastForwardButton);
replayControlWidget = new QWidget(); auto replayControlWidget = new QWidget();
replayControlWidget->setObjectName("replayControlWidget"); replayControlWidget->setObjectName("replayControlWidget");
replayControlWidget->setLayout(replayControlLayout); replayControlWidget->setLayout(replayControlLayout);
@ -1635,13 +1635,13 @@ void TabGame::createCardInfoDock(bool bReplay)
Q_UNUSED(bReplay); Q_UNUSED(bReplay);
cardInfoFrameWidget = new CardInfoFrameWidget(); cardInfoFrameWidget = new CardInfoFrameWidget();
cardHInfoLayout = new QHBoxLayout; auto cardHInfoLayout = new QHBoxLayout;
cardVInfoLayout = new QVBoxLayout; auto cardVInfoLayout = new QVBoxLayout;
cardVInfoLayout->setContentsMargins(0, 0, 0, 0); cardVInfoLayout->setContentsMargins(0, 0, 0, 0);
cardVInfoLayout->addWidget(cardInfoFrameWidget); cardVInfoLayout->addWidget(cardInfoFrameWidget);
cardVInfoLayout->addLayout(cardHInfoLayout); cardVInfoLayout->addLayout(cardHInfoLayout);
cardBoxLayoutWidget = new QWidget; auto cardBoxLayoutWidget = new QWidget;
cardBoxLayoutWidget->setLayout(cardVInfoLayout); cardBoxLayoutWidget->setLayout(cardVInfoLayout);
cardInfoDock = new QDockWidget(this); cardInfoDock = new QDockWidget(this);
@ -1679,6 +1679,22 @@ void TabGame::createPlayerListDock(bool bReplay)
void TabGame::createMessageDock(bool bReplay) void TabGame::createMessageDock(bool bReplay)
{ {
auto messageLogLayout = new QVBoxLayout;
messageLogLayout->setContentsMargins(0, 0, 0, 0);
// clock
if (!bReplay) {
timeElapsedLabel = new QLabel;
timeElapsedLabel->setAlignment(Qt::AlignCenter);
gameTimer = new QTimer(this);
gameTimer->setInterval(1000);
connect(gameTimer, &QTimer::timeout, this, &TabGame::incrementGameTime);
gameTimer->start();
messageLogLayout->addWidget(timeElapsedLabel);
}
// message log
messageLog = new MessageLogWidget(tabSupervisor, this); messageLog = new MessageLogWidget(tabSupervisor, this);
connect(messageLog, &MessageLogWidget::cardNameHovered, cardInfoFrameWidget, connect(messageLog, &MessageLogWidget::cardNameHovered, cardInfoFrameWidget,
qOverload<const QString &>(&CardInfoFrameWidget::setCard)); qOverload<const QString &>(&CardInfoFrameWidget::setCard));
@ -1690,14 +1706,12 @@ void TabGame::createMessageDock(bool bReplay)
connect(messageLog, &MessageLogWidget::addMentionTag, this, &TabGame::addMentionTag); connect(messageLog, &MessageLogWidget::addMentionTag, this, &TabGame::addMentionTag);
connect(&SettingsCache::instance(), &SettingsCache::chatMentionCompleterChanged, this, connect(&SettingsCache::instance(), &SettingsCache::chatMentionCompleterChanged, this,
&TabGame::actCompleterChanged); &TabGame::actCompleterChanged);
}
timeElapsedLabel = new QLabel; messageLogLayout->addWidget(messageLog);
timeElapsedLabel->setAlignment(Qt::AlignCenter);
gameTimer = new QTimer(this);
gameTimer->setInterval(1000);
connect(gameTimer, &QTimer::timeout, this, &TabGame::incrementGameTime);
gameTimer->start();
// chat entry
if (!bReplay) {
sayLabel = new QLabel; sayLabel = new QLabel;
sayEdit = new LineEditCompleter; sayEdit = new LineEditCompleter;
sayEdit->setMaxLength(MAX_TEXT_LENGTH); sayEdit->setMaxLength(MAX_TEXT_LENGTH);
@ -1726,20 +1740,15 @@ void TabGame::createMessageDock(bool bReplay)
connect(tabSupervisor, &TabSupervisor::adminLockChanged, this, &TabGame::adminLockChanged); connect(tabSupervisor, &TabSupervisor::adminLockChanged, this, &TabGame::adminLockChanged);
connect(sayEdit, &LineEditCompleter::returnPressed, this, &TabGame::actSay); connect(sayEdit, &LineEditCompleter::returnPressed, this, &TabGame::actSay);
sayHLayout = new QHBoxLayout; auto sayHLayout = new QHBoxLayout;
sayHLayout->addWidget(sayLabel); sayHLayout->addWidget(sayLabel);
sayHLayout->addWidget(sayEdit); sayHLayout->addWidget(sayEdit);
messageLogLayout->addLayout(sayHLayout);
} }
messageLogLayout = new QVBoxLayout; // dock
messageLogLayout->setContentsMargins(0, 0, 0, 0); auto messageLogLayoutWidget = new QWidget;
if (!bReplay)
messageLogLayout->addWidget(timeElapsedLabel);
messageLogLayout->addWidget(messageLog);
if (!bReplay)
messageLogLayout->addLayout(sayHLayout);
messageLogLayoutWidget = new QWidget;
messageLogLayoutWidget->setLayout(messageLogLayout); messageLogLayoutWidget->setLayout(messageLogLayout);
messageLayoutDock = new QDockWidget(this); messageLayoutDock = new QDockWidget(this);

View file

@ -111,10 +111,8 @@ private:
GameScene *scene; GameScene *scene;
GameView *gameView; GameView *gameView;
QMap<int, DeckViewContainer *> deckViewContainers; QMap<int, DeckViewContainer *> deckViewContainers;
QVBoxLayout *cardVInfoLayout, *messageLogLayout, *gamePlayAreaVBox, *deckViewContainerLayout; QVBoxLayout *deckViewContainerLayout;
QHBoxLayout *cardHInfoLayout, *sayHLayout, *mainHLayout, *replayControlLayout; QWidget *gamePlayAreaWidget, *deckViewContainerWidget;
QWidget *cardBoxLayoutWidget, *messageLogLayoutWidget, *gamePlayAreaWidget, *deckViewContainerWidget,
*replayControlWidget;
QDockWidget *cardInfoDock, *messageLayoutDock, *playerListDock, *replayDock; QDockWidget *cardInfoDock, *messageLayoutDock, *playerListDock, *replayDock;
QAction *playersSeparator; QAction *playersSeparator;
QMenu *gameMenu, *viewMenu, *cardInfoDockMenu, *messageLayoutDockMenu, *playerListDockMenu, *replayDockMenu; QMenu *gameMenu, *viewMenu, *cardInfoDockMenu, *messageLayoutDockMenu, *playerListDockMenu, *replayDockMenu;

View file

@ -7,12 +7,14 @@
#include <QDebug> #include <QDebug>
#include <QDirIterator> #include <QDirIterator>
#include <QFileInfo> #include <QFileInfo>
#include <QMainWindow>
#include <QMovie> #include <QMovie>
#include <QNetworkDiskCache> #include <QNetworkDiskCache>
#include <QNetworkRequest> #include <QNetworkRequest>
#include <QPainter> #include <QPainter>
#include <QPixmapCache> #include <QPixmapCache>
#include <QScreen> #include <QScreen>
#include <QStatusBar>
#include <QThread> #include <QThread>
#include <algorithm> #include <algorithm>
#include <utility> #include <utility>
@ -27,6 +29,16 @@ PictureLoader::PictureLoader() : QObject(nullptr)
connect(&SettingsCache::instance(), &SettingsCache::picDownloadChanged, this, &PictureLoader::picDownloadChanged); connect(&SettingsCache::instance(), &SettingsCache::picDownloadChanged, this, &PictureLoader::picDownloadChanged);
connect(worker, &PictureLoaderWorker::imageLoaded, this, &PictureLoader::imageLoaded); connect(worker, &PictureLoaderWorker::imageLoaded, this, &PictureLoader::imageLoaded);
statusBar = new PictureLoaderStatusBar(nullptr);
QMainWindow *mainWindow = qobject_cast<QMainWindow *>(QApplication::activeWindow());
if (mainWindow) {
mainWindow->statusBar()->addPermanentWidget(statusBar);
}
connect(worker, &PictureLoaderWorker::imageLoadQueued, statusBar, &PictureLoaderStatusBar::addQueuedImageLoad);
connect(worker, &PictureLoaderWorker::imageLoadSuccessful, statusBar,
&PictureLoaderStatusBar::addSuccessfulImageLoad);
} }
PictureLoader::~PictureLoader() PictureLoader::~PictureLoader()

View file

@ -2,6 +2,7 @@
#define PICTURELOADER_H #define PICTURELOADER_H
#include "../../../game/cards/card_info.h" #include "../../../game/cards/card_info.h"
#include "picture_loader_status_bar.h"
#include "picture_loader_worker.h" #include "picture_loader_worker.h"
#include <QLoggingCategory> #include <QLoggingCategory>
@ -27,6 +28,7 @@ private:
void operator=(PictureLoader const &); void operator=(PictureLoader const &);
PictureLoaderWorker *worker; PictureLoaderWorker *worker;
PictureLoaderStatusBar *statusBar;
public: public:
static void getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size); static void getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size);

View file

@ -0,0 +1,32 @@
#include "picture_loader_request_status_display_widget.h"
PictureLoaderRequestStatusDisplayWidget::PictureLoaderRequestStatusDisplayWidget(QWidget *parent,
const QUrl &_url,
PictureLoaderWorkerWork *worker)
: QWidget(parent)
{
layout = new QHBoxLayout(this);
if (worker->cardToDownload.getCard()) {
name = new QLabel(this);
name->setText(worker->cardToDownload.getCard()->getName());
setShortname = new QLabel(this);
setShortname->setText(worker->cardToDownload.getSetName());
providerId = new QLabel(this);
providerId->setText(worker->cardToDownload.getCard()->getProperty("uuid"));
layout->addWidget(name);
layout->addWidget(setShortname);
layout->addWidget(providerId);
}
startTime = new QLabel(QDateTime::currentDateTime().toString(), this);
elapsedTime = new QLabel("0", this);
finished = new QLabel("False", this);
url = new QLabel(_url.toString(), this);
layout->addWidget(startTime);
layout->addWidget(elapsedTime);
layout->addWidget(finished);
layout->addWidget(url);
}

View file

@ -0,0 +1,66 @@
#ifndef PICTURE_LOADER_REQUEST_STATUS_DISPLAY_WIDGET_H
#define PICTURE_LOADER_REQUEST_STATUS_DISPLAY_WIDGET_H
#include "picture_loader_worker_work.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QWidget>
class PictureLoaderRequestStatusDisplayWidget : public QWidget
{
Q_OBJECT
public:
PictureLoaderRequestStatusDisplayWidget(QWidget *parent, const QUrl &url, PictureLoaderWorkerWork *worker);
PictureLoaderWorkerWork *worker;
void setFinished()
{
finished->setText("True");
update();
repaint();
}
bool getFinished() const
{
return finished->text() == "True";
}
void setElapsedTime(const QString &_elapsedTime) const
{
elapsedTime->setText(_elapsedTime);
}
int queryElapsedSeconds()
{
if (!finished) {
int elapsedSeconds = QDateTime::fromString(startTime->text()).secsTo(QDateTime::currentDateTime());
elapsedTime->setText(QString::number(elapsedSeconds));
update();
repaint();
return elapsedSeconds;
}
return elapsedTime->text().toInt();
}
QString getStartTime() const
{
return startTime->text();
}
QString getUrl() const
{
return url->text();
}
private:
QHBoxLayout *layout;
QLabel *name;
QLabel *setShortname;
QLabel *providerId;
QLabel *startTime;
QLabel *elapsedTime;
QLabel *finished;
QLabel *url;
};
#endif // PICTURE_LOADER_REQUEST_STATUS_DISPLAY_WIDGET_H

View file

@ -0,0 +1,58 @@
#include "picture_loader_status_bar.h"
#include "picture_loader_request_status_display_widget.h"
PictureLoaderStatusBar::PictureLoaderStatusBar(QWidget *parent) : QWidget(parent)
{
layout = new QHBoxLayout(this);
progressBar = new QProgressBar(this);
progressBar->setMaximum(0);
progressBar->setFormat("%v/%m");
layout->addWidget(progressBar);
loadLog = new SettingsButtonWidget(this);
layout->addWidget(loadLog);
cleaner = new QTimer(this);
cleaner->setInterval(1000);
connect(cleaner, &QTimer::timeout, this, &PictureLoaderStatusBar::cleanOldEntries);
cleaner->start();
setLayout(layout);
}
void PictureLoaderStatusBar::cleanOldEntries()
{
if (!loadLog || !loadLog->popup) {
return;
}
for (PictureLoaderRequestStatusDisplayWidget *statusDisplayWidget :
loadLog->popup->findChildren<PictureLoaderRequestStatusDisplayWidget *>()) {
statusDisplayWidget->queryElapsedSeconds();
if (statusDisplayWidget->getFinished() &&
QDateTime::fromString(statusDisplayWidget->getStartTime()).secsTo(QDateTime::currentDateTime()) > 10) {
loadLog->removeSettingsWidget(statusDisplayWidget);
progressBar->setMaximum(progressBar->maximum() - 1);
progressBar->setValue(progressBar->value() - 1);
}
}
}
void PictureLoaderStatusBar::addQueuedImageLoad(const QUrl &url, PictureLoaderWorkerWork *worker)
{
loadLog->addSettingsWidget(new PictureLoaderRequestStatusDisplayWidget(loadLog, url, worker));
progressBar->setMaximum(progressBar->maximum() + 1);
}
void PictureLoaderStatusBar::addSuccessfulImageLoad(const QUrl &url, PictureLoaderWorkerWork *worker)
{
Q_UNUSED(worker)
progressBar->setValue(progressBar->value() + 1);
for (PictureLoaderRequestStatusDisplayWidget *statusDisplayWidget :
loadLog->popup->findChildren<PictureLoaderRequestStatusDisplayWidget *>()) {
if (statusDisplayWidget->getUrl() == url.toString()) {
statusDisplayWidget->queryElapsedSeconds();
statusDisplayWidget->setFinished();
}
}
}

View file

@ -0,0 +1,29 @@
#ifndef PICTURE_LOADER_STATUS_BAR_H
#define PICTURE_LOADER_STATUS_BAR_H
#include "../widgets/quick_settings/settings_button_widget.h"
#include "picture_loader_worker_work.h"
#include <QHBoxLayout>
#include <QProgressBar>
#include <QWidget>
class PictureLoaderStatusBar : public QWidget
{
Q_OBJECT
public:
explicit PictureLoaderStatusBar(QWidget *parent);
public slots:
void addQueuedImageLoad(const QUrl &url, PictureLoaderWorkerWork *worker);
void addSuccessfulImageLoad(const QUrl &url, PictureLoaderWorkerWork *worker);
void cleanOldEntries();
private:
QHBoxLayout *layout;
QProgressBar *progressBar;
SettingsButtonWidget *loadLog;
QTimer *cleaner;
};
#endif // PICTURE_LOADER_STATUS_BAR_H

View file

@ -2,13 +2,15 @@
#include "../../../game/cards/card_database_manager.h" #include "../../../game/cards/card_database_manager.h"
#include "../../../settings/cache_settings.h" #include "../../../settings/cache_settings.h"
#include "picture_loader_worker_work.h"
#include <QBuffer>
#include <QDirIterator> #include <QDirIterator>
#include <QJsonDocument>
#include <QMovie> #include <QMovie>
#include <QNetworkDiskCache> #include <QNetworkDiskCache>
#include <QNetworkReply> #include <QNetworkReply>
#include <QThread> #include <QThread>
#include <utility>
// Card back returned by gatherer when card is not found // Card back returned by gatherer when card is not found
QStringList PictureLoaderWorker::md5Blacklist = QStringList() << "db0c48db407a907c16ade38de048a441"; QStringList PictureLoaderWorker::md5Blacklist = QStringList() << "db0c48db407a907c16ade38de048a441";
@ -19,11 +21,8 @@ PictureLoaderWorker::PictureLoaderWorker()
picDownload(SettingsCache::instance().getPicDownload()), downloadRunning(false), loadQueueRunning(false), picDownload(SettingsCache::instance().getPicDownload()), downloadRunning(false), loadQueueRunning(false),
overrideAllCardArtWithPersonalPreference(SettingsCache::instance().getOverrideAllCardArtWithPersonalPreference()) overrideAllCardArtWithPersonalPreference(SettingsCache::instance().getOverrideAllCardArtWithPersonalPreference())
{ {
connect(this, &PictureLoaderWorker::startLoadQueue, this, &PictureLoaderWorker::processLoadQueue, connect(&SettingsCache::instance(), SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged()));
Qt::QueuedConnection); connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
connect(&SettingsCache::instance(), &SettingsCache::picsPathChanged, this, &PictureLoaderWorker::picsPathChanged);
connect(&SettingsCache::instance(), &SettingsCache::picDownloadChanged, this,
&PictureLoaderWorker::picDownloadChanged);
connect(&SettingsCache::instance(), &SettingsCache::overrideAllCardArtWithPersonalPreferenceChanged, this, connect(&SettingsCache::instance(), &SettingsCache::overrideAllCardArtWithPersonalPreferenceChanged, this,
&PictureLoaderWorker::setOverrideAllCardArtWithPersonalPreference); &PictureLoaderWorker::setOverrideAllCardArtWithPersonalPreference);
@ -34,18 +33,17 @@ PictureLoaderWorker::PictureLoaderWorker()
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
networkManager->setTransferTimeout(); networkManager->setTransferTimeout();
#endif #endif
auto cache = new QNetworkDiskCache(this); cache = new QNetworkDiskCache(this);
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath()); cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
cache->setMaximumCacheSize(1024L * 1024L * cache->setMaximumCacheSize(1024L * 1024L *
static_cast<qint64>(SettingsCache::instance().getNetworkCacheSizeInMB())); static_cast<qint64>(SettingsCache::instance().getNetworkCacheSizeInMB()));
// Note: the settings is in MB, but QNetworkDiskCache uses bytes // Note: the settings is in MB, but QNetworkDiskCache uses bytes
connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, cache, connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, this,
[cache](int newSizeInMB) { cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB)); }); [this](int newSizeInMB) { cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB)); });
networkManager->setCache(cache); networkManager->setCache(cache);
// Use a ManualRedirectPolicy since we keep track of redirects in picDownloadFinished // Use a ManualRedirectPolicy since we keep track of redirects in picDownloadFinished
// We can't use NoLessSafeRedirectPolicy because it is not applied with AlwaysCache // We can't use NoLessSafeRedirectPolicy because it is not applied with AlwaysCache
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy); networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
connect(networkManager, &QNetworkAccessManager::finished, this, &PictureLoaderWorker::picDownloadFinished);
cacheFilePath = SettingsCache::instance().getRedirectCachePath() + REDIRECT_CACHE_FILENAME; cacheFilePath = SettingsCache::instance().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
loadRedirectCache(); loadRedirectCache();
@ -57,244 +55,104 @@ PictureLoaderWorker::PictureLoaderWorker()
pictureLoaderThread = new QThread; pictureLoaderThread = new QThread;
pictureLoaderThread->start(QThread::LowPriority); pictureLoaderThread->start(QThread::LowPriority);
moveToThread(pictureLoaderThread); moveToThread(pictureLoaderThread);
connect(&requestTimer, &QTimer::timeout, this, &PictureLoaderWorker::processQueuedRequests);
requestTimer.setInterval(1000);
requestTimer.start();
} }
PictureLoaderWorker::~PictureLoaderWorker() PictureLoaderWorker::~PictureLoaderWorker()
{ {
saveRedirectCache();
pictureLoaderThread->deleteLater(); pictureLoaderThread->deleteLater();
} }
void PictureLoaderWorker::processLoadQueue() void PictureLoaderWorker::queueRequest(const QUrl &url, PictureLoaderWorkerWork *worker)
{ {
if (loadQueueRunning) {
return;
}
loadQueueRunning = true;
while (true) {
mutex.lock();
if (loadQueue.isEmpty()) {
mutex.unlock();
loadQueueRunning = false;
return;
}
cardBeingLoaded = loadQueue.takeFirst();
mutex.unlock();
QString setName = cardBeingLoaded.getSetName();
QString cardName = cardBeingLoaded.getCard()->getName();
QString correctedCardName = cardBeingLoaded.getCard()->getCorrectedName();
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardName << " set: " << setName << "]: Trying to load picture";
// FIXME: This is a hack so that to keep old Cockatrice behavior
// (ignoring provider ID) when the "override all card art with personal
// preference" is set.
//
// Figure out a proper way to integrate the two systems at some point.
//
// Note: need to go through a member for
// overrideAllCardArtWithPersonalPreference as reading from the
// SettingsCache instance from the PictureLoaderWorker thread could
// cause race conditions.
//
// XXX: Reading from the CardDatabaseManager instance from the
// PictureLoaderWorker thread might not be safe either
bool searchCustomPics = overrideAllCardArtWithPersonalPreference ||
CardDatabaseManager::getInstance()->isProviderIdForPreferredPrinting(
cardName, cardBeingLoaded.getCard()->getPixmapCacheKey());
if (searchCustomPics && cardImageExistsOnDisk(setName, correctedCardName, searchCustomPics)) {
continue;
}
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardName << " set: " << setName << "]: No custom picture, trying to download";
cardsToDownload.append(cardBeingLoaded);
cardBeingLoaded.clear();
if (!downloadRunning) {
startNextPicDownload();
}
}
}
bool PictureLoaderWorker::cardImageExistsOnDisk(QString &setName, QString &correctedCardname, bool searchCustomPics)
{
QImage image;
QImageReader imgReader;
imgReader.setDecideFormatFromContent(true);
QList<QString> picsPaths = QList<QString>();
if (searchCustomPics) {
QDirIterator it(customPicsPath, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
// Recursively check all subdirectories of the CUSTOM folder
while (it.hasNext()) {
QString thisPath(it.next());
QFileInfo thisFileInfo(thisPath);
if (thisFileInfo.isFile() &&
(thisFileInfo.fileName() == correctedCardname || thisFileInfo.completeBaseName() == correctedCardname ||
thisFileInfo.baseName() == correctedCardname)) {
picsPaths << thisPath; // Card found in the CUSTOM directory, somewhere
}
}
}
if (!setName.isEmpty()) {
picsPaths << picsPath + "/" + setName + "/" + correctedCardname
// We no longer store downloaded images there, but don't just ignore
// stuff that old versions have put there.
<< picsPath + "/downloadedPics/" + setName + "/" + correctedCardname;
}
// Iterates through the list of paths, searching for images with the desired
// name with any QImageReader-supported
// extension
for (const auto &_picsPath : picsPaths) {
imgReader.setFileName(_picsPath);
if (imgReader.read(&image)) {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << correctedCardname << " set: " << setName << "]: Picture found on disk.";
imageLoaded(cardBeingLoaded.getCard(), image);
return true;
}
imgReader.setFileName(_picsPath + ".full");
if (imgReader.read(&image)) {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << correctedCardname << " set: " << setName << "]: Picture.full found on disk.";
imageLoaded(cardBeingLoaded.getCard(), image);
return true;
}
imgReader.setFileName(_picsPath + ".xlhq");
if (imgReader.read(&image)) {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << correctedCardname << " set: " << setName << "]: Picture.xlhq found on disk.";
imageLoaded(cardBeingLoaded.getCard(), image);
return true;
}
}
return false;
}
void PictureLoaderWorker::startNextPicDownload()
{
if (cardsToDownload.isEmpty()) {
cardBeingDownloaded.clear();
downloadRunning = false;
return;
}
downloadRunning = true;
cardBeingDownloaded = cardsToDownload.takeFirst();
QString picUrl = cardBeingDownloaded.getCurrentUrl();
if (picUrl.isEmpty()) {
downloadRunning = false;
picDownloadFailed();
} else {
QUrl url(picUrl);
qCDebug(PictureLoaderWorkerLog).nospace() << "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
<< " set: " << cardBeingDownloaded.getSetName()
<< "]: Trying to fetch picture from url " << url.toDisplayString();
makeRequest(url);
}
}
void PictureLoaderWorker::picDownloadFailed()
{
/* Take advantage of short circuiting here to call the nextUrl until one
is not available. Only once nextUrl evaluates to false will this move
on to nextSet. If the Urls for a particular card are empty, this will
effectively go through the sets for that card. */
if (cardBeingDownloaded.nextUrl() || cardBeingDownloaded.nextSet()) {
mutex.lock();
loadQueue.prepend(cardBeingDownloaded);
mutex.unlock();
} else {
qCWarning(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
<< " set: " << cardBeingDownloaded.getSetName() << "]: Picture NOT found, "
<< (picDownload ? "download failed" : "downloads disabled")
<< ", no more url combinations to try: BAILING OUT";
imageLoaded(cardBeingDownloaded.getCard(), QImage());
cardBeingDownloaded.clear();
}
emit startLoadQueue();
}
bool PictureLoaderWorker::imageIsBlackListed(const QByteArray &picData)
{
QString md5sum = QCryptographicHash::hash(picData, QCryptographicHash::Md5).toHex();
return md5Blacklist.contains(md5sum);
}
QNetworkReply *PictureLoaderWorker::makeRequest(const QUrl &url)
{
// Check if the redirect is cached
QUrl cachedRedirect = getCachedRedirect(url); QUrl cachedRedirect = getCachedRedirect(url);
if (!cachedRedirect.isEmpty()) { if (!cachedRedirect.isEmpty()) {
qCDebug(PictureLoaderWorkerLog).nospace() queueRequest(cachedRedirect, worker);
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName() }
<< " set: " << cardBeingDownloaded.getSetName() << "]: Using cached redirect for " << url.toDisplayString() if (cache->metaData(url).isValid()) {
<< " to " << cachedRedirect.toDisplayString(); makeRequest(url, worker);
return makeRequest(cachedRedirect); // Use the cached redirect } else {
requestLoadQueue.append(qMakePair(url, worker));
emit imageLoadQueued(url, worker);
}
}
QNetworkReply *PictureLoaderWorker::makeRequest(const QUrl &url, PictureLoaderWorkerWork *worker)
{
// Check for cached redirects
QUrl cachedRedirect = getCachedRedirect(url);
if (!cachedRedirect.isEmpty()) {
emit imageLoadSuccessful(url, worker);
return makeRequest(cachedRedirect, worker);
} }
QNetworkRequest req(url); QNetworkRequest req(url);
// QNetworkDiskCache leaks file descriptors when downloading compressed
// files, see https://bugreports.qt.io/browse/QTBUG-135641
//
// We can set the Accept-Encoding header manually to disable Qt's automatic
// response decompression, but then we would have to deal with decompression
// ourselves.
//
// Since we are dowloading images that are usually stored in a
// compressed format (e.g. jpeg or webp), it's not clear compression at the
// HTTP level helps; in fact, images are typically returned without
// compression. Redirects, on the other hand, are compressed and cause file
// descriptor leaks -- but since redirects have no payload, we don't really
// care either.
//
// In the end, just do the simple thing and disable HTTP compression.
req.setRawHeader("accept-encoding", "identity");
if (!picDownload) { if (!picDownload) {
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache); req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache);
} }
QNetworkReply *reply = networkManager->get(req); QNetworkReply *reply = networkManager->get(req);
connect(reply, &QNetworkReply::finished, this, [this, reply, url]() { // Connect reply handling
connect(reply, &QNetworkReply::finished, this, [this, reply, url, worker]() {
QVariant redirectTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute); QVariant redirectTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
if (redirectTarget.isValid()) { if (redirectTarget.isValid()) {
QUrl redirectUrl = redirectTarget.toUrl(); QUrl redirectUrl = redirectTarget.toUrl();
if (redirectUrl.isRelative()) { if (redirectUrl.isRelative()) {
redirectUrl = url.resolved(redirectUrl); redirectUrl = url.resolved(redirectUrl);
} }
cacheRedirect(url, redirectUrl); cacheRedirect(url, redirectUrl);
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
<< " set: " << cardBeingDownloaded.getSetName() << "]: Caching redirect from " << url.toDisplayString()
<< " to " << redirectUrl.toDisplayString();
} }
if (reply->error() == QNetworkReply::NoError) {
worker->picDownloadFinished(reply);
emit imageLoadSuccessful(url, worker);
// If we hit a cached image, we get to make another request for free.
if (reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool()) {
if (!requestLoadQueue.isEmpty()) {
auto request = requestLoadQueue.takeFirst();
makeRequest(request.first, request.second);
}
}
} else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 429) {
qInfo() << "Too many requests.";
}
reply->deleteLater(); reply->deleteLater();
}); });
return reply; return reply;
} }
void PictureLoaderWorker::processQueuedRequests()
{
for (int i = 0; i < 10; i++) {
if (!requestLoadQueue.isEmpty()) {
auto request = requestLoadQueue.takeFirst();
makeRequest(request.first, request.second);
}
}
}
void PictureLoaderWorker::enqueueImageLoad(const CardInfoPtr &card)
{
auto worker = new PictureLoaderWorkerWork(this, card);
Q_UNUSED(worker);
}
void PictureLoaderWorker::imageLoadedSuccessfully(CardInfoPtr card, const QImage &image)
{
emit imageLoaded(std::move(card), image);
}
void PictureLoaderWorker::cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl) void PictureLoaderWorker::cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl)
{ {
redirectCache[originalUrl] = qMakePair(redirectUrl, QDateTime::currentDateTimeUtc()); redirectCache[originalUrl] = qMakePair(redirectUrl, QDateTime::currentDateTimeUtc());
saveRedirectCache(); // saveRedirectCache();
} }
QUrl PictureLoaderWorker::getCachedRedirect(const QUrl &originalUrl) const QUrl PictureLoaderWorker::getCachedRedirect(const QUrl &originalUrl) const
@ -353,129 +211,6 @@ void PictureLoaderWorker::cleanStaleEntries()
} }
} }
void PictureLoaderWorker::picDownloadFinished(QNetworkReply *reply)
{
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
if (reply->error()) {
if (isFromCache) {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
<< "]: Removing corrupted cache file for url " << reply->url().toDisplayString() << " and retrying ("
<< reply->errorString() << ")";
networkManager->cache()->remove(reply->url());
makeRequest(reply->url());
} else {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
<< "]: " << (picDownload ? "Download" : "Cache search") << " failed for url "
<< reply->url().toDisplayString() << " (" << reply->errorString() << ")";
picDownloadFailed();
startNextPicDownload();
}
reply->deleteLater();
return;
}
// List of status codes from https://doc.qt.io/qt-6/qnetworkreply.html#redirected
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 305 || statusCode == 307 ||
statusCode == 308) {
QUrl redirectUrl = reply->header(QNetworkRequest::LocationHeader).toUrl();
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
<< "]: following " << (isFromCache ? "cached redirect" : "redirect") << " to "
<< redirectUrl.toDisplayString();
makeRequest(redirectUrl);
reply->deleteLater();
return;
}
// peek is used to keep the data in the buffer for use by QImageReader
const QByteArray &picData = reply->peek(reply->size());
if (imageIsBlackListed(picData)) {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
<< "]: Picture found, but blacklisted, will consider it as not found";
picDownloadFailed();
reply->deleteLater();
startNextPicDownload();
return;
}
QImage testImage;
QImageReader imgReader;
imgReader.setDecideFormatFromContent(true);
imgReader.setDevice(reply);
bool logSuccessMessage = false;
static const int riffHeaderSize = 12; // RIFF_HEADER_SIZE from webp/format_constants.h
auto replyHeader = reply->peek(riffHeaderSize);
if (replyHeader.startsWith("RIFF") && replyHeader.endsWith("WEBP")) {
auto imgBuf = QBuffer(this);
imgBuf.setData(reply->readAll());
auto movie = QMovie(&imgBuf);
movie.start();
movie.stop();
imageLoaded(cardBeingDownloaded.getCard(), movie.currentImage());
logSuccessMessage = true;
} else if (imgReader.read(&testImage)) {
imageLoaded(cardBeingDownloaded.getCard(), testImage);
logSuccessMessage = true;
} else {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
<< "]: Possible " << (isFromCache ? "cached" : "downloaded") << " picture at "
<< reply->url().toDisplayString() << " could not be loaded: " << reply->errorString();
picDownloadFailed();
}
if (logSuccessMessage) {
qCInfo(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
<< "]: Image successfully " << (isFromCache ? "loaded from cached" : "downloaded from") << " url "
<< reply->url().toDisplayString();
}
reply->deleteLater();
startNextPicDownload();
}
void PictureLoaderWorker::enqueueImageLoad(CardInfoPtr card)
{
QMutexLocker locker(&mutex);
// avoid queueing the same card more than once
if (!card || card == cardBeingLoaded.getCard() || card == cardBeingDownloaded.getCard()) {
return;
}
for (const PictureToLoad &pic : loadQueue) {
if (pic.getCard() == card)
return;
}
for (const PictureToLoad &pic : cardsToDownload) {
if (pic.getCard() == card)
return;
}
loadQueue.append(PictureToLoad(card));
emit startLoadQueue();
}
void PictureLoaderWorker::picDownloadChanged() void PictureLoaderWorker::picDownloadChanged()
{ {
QMutexLocker locker(&mutex); QMutexLocker locker(&mutex);

View file

@ -1,13 +1,18 @@
#ifndef PICTURE_LOADER_WORKER_H #ifndef PICTURE_LOADER_WORKER_H
#define PICTURE_LOADER_WORKER_H #define PICTURE_LOADER_WORKER_H
#include "../../../game/cards/card_database.h"
#include "../../../game/cards/card_info.h" #include "../../../game/cards/card_info.h"
#include "picture_loader_worker_work.h"
#include "picture_to_load.h" #include "picture_to_load.h"
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QMutex> #include <QMutex>
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QNetworkDiskCache>
#include <QObject> #include <QObject>
#include <QQueue>
#include <QTimer>
#define REDIRECT_HEADER_NAME "redirects" #define REDIRECT_HEADER_NAME "redirects"
#define REDIRECT_ORIGINAL_URL "original" #define REDIRECT_ORIGINAL_URL "original"
@ -17,16 +22,23 @@
inline Q_LOGGING_CATEGORY(PictureLoaderWorkerLog, "picture_loader.worker"); inline Q_LOGGING_CATEGORY(PictureLoaderWorkerLog, "picture_loader.worker");
class PictureLoaderWorkerWork;
class PictureLoaderWorker : public QObject class PictureLoaderWorker : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit PictureLoaderWorker(); explicit PictureLoaderWorker();
~PictureLoaderWorker() override; ~PictureLoaderWorker() override;
void queueRequest(const QUrl &url, PictureLoaderWorkerWork *worker);
void enqueueImageLoad(CardInfoPtr card); void enqueueImageLoad(const CardInfoPtr &card);
void clearNetworkCache(); void clearNetworkCache();
public slots:
QNetworkReply *makeRequest(const QUrl &url, PictureLoaderWorkerWork *workThread);
void processQueuedRequests();
void imageLoadedSuccessfully(CardInfoPtr card, const QImage &image);
private: private:
static QStringList md5Blacklist; static QStringList md5Blacklist;
@ -35,6 +47,7 @@ private:
QList<PictureToLoad> loadQueue; QList<PictureToLoad> loadQueue;
QMutex mutex; QMutex mutex;
QNetworkAccessManager *networkManager; QNetworkAccessManager *networkManager;
QNetworkDiskCache *cache;
QHash<QUrl, QPair<QUrl, QDateTime>> redirectCache; // Stores redirect and timestamp QHash<QUrl, QPair<QUrl, QDateTime>> redirectCache; // Stores redirect and timestamp
QString cacheFilePath; // Path to persistent storage QString cacheFilePath; // Path to persistent storage
static constexpr int CacheTTLInDays = 30; // TODO: Make user configurable static constexpr int CacheTTLInDays = 30; // TODO: Make user configurable
@ -42,18 +55,11 @@ private:
PictureToLoad cardBeingLoaded; PictureToLoad cardBeingLoaded;
PictureToLoad cardBeingDownloaded; PictureToLoad cardBeingDownloaded;
bool picDownload, downloadRunning, loadQueueRunning; bool picDownload, downloadRunning, loadQueueRunning;
QQueue<QPair<QUrl, PictureLoaderWorkerWork *>> requestLoadQueue;
QList<QPair<QUrl, PictureLoaderWorkerWork *>> requestQueue;
QTimer requestTimer; // Timer for processing delayed requests
bool overrideAllCardArtWithPersonalPreference; bool overrideAllCardArtWithPersonalPreference;
void startNextPicDownload();
/** Emit the `imageLoaded` signal and return `true` if a picture is found on
disk, return `false` otherwise.
If `searchCustomPics` is `true`, the CUSTOM folder is searched for a
matching image first; otherwise, only the set-based folders are used. */
bool cardImageExistsOnDisk(QString &setName, QString &correctedCardName, bool searchCustomPics);
bool imageIsBlackListed(const QByteArray &);
QNetworkReply *makeRequest(const QUrl &url);
void cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl); void cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl);
QUrl getCachedRedirect(const QUrl &originalUrl) const; QUrl getCachedRedirect(const QUrl &originalUrl) const;
void loadRedirectCache(); void loadRedirectCache();
@ -61,18 +67,15 @@ private:
void cleanStaleEntries(); void cleanStaleEntries();
private slots: private slots:
void picDownloadFinished(QNetworkReply *reply);
void picDownloadFailed();
void picDownloadChanged(); void picDownloadChanged();
void picsPathChanged(); void picsPathChanged();
void setOverrideAllCardArtWithPersonalPreference(bool _overrideAllCardArtWithPersonalPreference); void setOverrideAllCardArtWithPersonalPreference(bool _overrideAllCardArtWithPersonalPreference);
public slots:
void processLoadQueue();
signals: signals:
void startLoadQueue(); void startLoadQueue();
void imageLoaded(CardInfoPtr card, const QImage &image); void imageLoaded(CardInfoPtr card, const QImage &image);
void imageLoadQueued(const QUrl &url, PictureLoaderWorkerWork *worker);
void imageLoadSuccessful(const QUrl &url, PictureLoaderWorkerWork *worker);
}; };
#endif // PICTURE_LOADER_WORKER_H #endif // PICTURE_LOADER_WORKER_H

View file

@ -0,0 +1,236 @@
#include "picture_loader_worker_work.h"
#include "../../../game/cards/card_database_manager.h"
#include "../../../settings/cache_settings.h"
#include "picture_loader_worker.h"
#include <QBuffer>
#include <QDirIterator>
#include <QLoggingCategory>
#include <QMovie>
#include <QNetworkDiskCache>
#include <QNetworkReply>
#include <QThread>
// Card back returned by gatherer when card is not found
QStringList PictureLoaderWorkerWork::md5Blacklist = QStringList() << "db0c48db407a907c16ade38de048a441";
PictureLoaderWorkerWork::PictureLoaderWorkerWork(PictureLoaderWorker *_worker, const CardInfoPtr &toLoad)
: QThread(nullptr), worker(_worker), cardToDownload(toLoad)
{
connect(this, &PictureLoaderWorkerWork::requestImageDownload, worker, &PictureLoaderWorker::queueRequest,
Qt::QueuedConnection);
connect(this, &PictureLoaderWorkerWork::imageLoaded, worker, &PictureLoaderWorker::imageLoadedSuccessfully,
Qt::QueuedConnection);
pictureLoaderThread = new QThread;
pictureLoaderThread->start(QThread::LowPriority);
moveToThread(pictureLoaderThread);
startNextPicDownload();
}
PictureLoaderWorkerWork::~PictureLoaderWorkerWork()
{
pictureLoaderThread->deleteLater();
}
bool PictureLoaderWorkerWork::cardImageExistsOnDisk(QString &setName, QString &correctedCardname)
{
QImage image;
QImageReader imgReader;
imgReader.setDecideFormatFromContent(true);
QList<QString> picsPaths = QList<QString>();
QDirIterator it(SettingsCache::instance().getCustomPicsPath(),
QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
// Recursively check all subdirectories of the CUSTOM folder
while (it.hasNext()) {
QString thisPath(it.next());
QFileInfo thisFileInfo(thisPath);
if (thisFileInfo.isFile() &&
(thisFileInfo.fileName() == correctedCardname || thisFileInfo.completeBaseName() == correctedCardname ||
thisFileInfo.baseName() == correctedCardname)) {
picsPaths << thisPath; // Card found in the CUSTOM directory, somewhere
}
}
if (!setName.isEmpty()) {
picsPaths << SettingsCache::instance().getPicsPath() + "/" + setName + "/" + correctedCardname
// We no longer store downloaded images there, but don't just ignore
// stuff that old versions have put there.
<< SettingsCache::instance().getPicsPath() + "/downloadedPics/" + setName + "/" + correctedCardname;
}
// Iterates through the list of paths, searching for images with the desired
// name with any QImageReader-supported
// extension
for (const auto &_picsPath : picsPaths) {
imgReader.setFileName(_picsPath);
if (imgReader.read(&image)) {
qCDebug(PictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << correctedCardname << " set: " << setName << "]: Picture found on disk.";
imageLoaded(cardToDownload.getCard(), image);
return true;
}
imgReader.setFileName(_picsPath + ".full");
if (imgReader.read(&image)) {
qCDebug(PictureLoaderWorkerWorkLog).nospace() << "PictureLoader: [card: " << correctedCardname
<< " set: " << setName << "]: Picture.full found on disk.";
imageLoaded(cardToDownload.getCard(), image);
return true;
}
imgReader.setFileName(_picsPath + ".xlhq");
if (imgReader.read(&image)) {
qCDebug(PictureLoaderWorkerWorkLog).nospace() << "PictureLoader: [card: " << correctedCardname
<< " set: " << setName << "]: Picture.xlhq found on disk.";
imageLoaded(cardToDownload.getCard(), image);
return true;
}
}
return false;
}
void PictureLoaderWorkerWork::startNextPicDownload()
{
QString picUrl = cardToDownload.getCurrentUrl();
if (picUrl.isEmpty()) {
downloadRunning = false;
picDownloadFailed();
} else {
QUrl url(picUrl);
qCDebug(PictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard()->getCorrectedName()
<< " set: " << cardToDownload.getSetName() << "]: Trying to fetch picture from url "
<< url.toDisplayString();
emit requestImageDownload(url, this);
}
}
void PictureLoaderWorkerWork::picDownloadFailed()
{
/* Take advantage of short-circuiting here to call the nextUrl until one
is not available. Only once nextUrl evaluates to false will this move
on to nextSet. If the Urls for a particular card are empty, this will
effectively go through the sets for that card. */
if (cardToDownload.nextUrl() || cardToDownload.nextSet()) {
startNextPicDownload();
} else {
qCDebug(PictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard()->getCorrectedName()
<< " set: " << cardToDownload.getSetName() << "]: Picture NOT found, "
<< (picDownload ? "download failed" : "downloads disabled")
<< ", no more url combinations to try: BAILING OUT";
imageLoaded(cardToDownload.getCard(), QImage());
}
emit startLoadQueue();
}
void PictureLoaderWorkerWork::picDownloadFinished(QNetworkReply *reply)
{
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
if (reply->error()) {
if (isFromCache) {
qCDebug(PictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
<< " set: " << cardToDownload.getSetName() << "]: Removing corrupted cache file for url "
<< reply->url().toDisplayString() << " and retrying (" << reply->errorString() << ")";
networkManager->cache()->remove(reply->url());
requestImageDownload(reply->url(), this);
} else {
qCDebug(PictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
<< " set: " << cardToDownload.getSetName() << "]: " << (picDownload ? "Download" : "Cache search")
<< " failed for url " << reply->url().toDisplayString() << " (" << reply->errorString() << ")";
picDownloadFailed();
startNextPicDownload();
}
reply->deleteLater();
return;
}
// List of status codes from https://doc.qt.io/qt-6/qnetworkreply.html#redirected
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 305 || statusCode == 307 ||
statusCode == 308) {
QUrl redirectUrl = reply->header(QNetworkRequest::LocationHeader).toUrl();
qCDebug(PictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
<< " set: " << cardToDownload.getSetName() << "]: following "
<< (isFromCache ? "cached redirect" : "redirect") << " to " << redirectUrl.toDisplayString();
requestImageDownload(redirectUrl, this);
reply->deleteLater();
return;
}
// peek is used to keep the data in the buffer for use by QImageReader
const QByteArray &picData = reply->peek(reply->size());
if (imageIsBlackListed(picData)) {
qCDebug(PictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
<< " set: " << cardToDownload.getSetName()
<< "]: Picture found, but blacklisted, will consider it as not found";
picDownloadFailed();
reply->deleteLater();
startNextPicDownload();
return;
}
QImage testImage;
QImageReader imgReader;
imgReader.setDecideFormatFromContent(true);
imgReader.setDevice(reply);
bool logSuccessMessage = false;
static const int riffHeaderSize = 12; // RIFF_HEADER_SIZE from webp/format_constants.h
auto replyHeader = reply->peek(riffHeaderSize);
if (replyHeader.startsWith("RIFF") && replyHeader.endsWith("WEBP")) {
auto imgBuf = QBuffer(this);
imgBuf.setData(reply->readAll());
auto movie = QMovie(&imgBuf);
movie.start();
movie.stop();
imageLoaded(cardToDownload.getCard(), movie.currentImage());
logSuccessMessage = true;
} else if (imgReader.read(&testImage)) {
imageLoaded(cardToDownload.getCard(), testImage);
logSuccessMessage = true;
} else {
qCDebug(PictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
<< " set: " << cardToDownload.getSetName() << "]: Possible " << (isFromCache ? "cached" : "downloaded")
<< " picture at " << reply->url().toDisplayString() << " could not be loaded: " << reply->errorString();
picDownloadFailed();
}
if (logSuccessMessage) {
qCDebug(PictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard()->getName()
<< " set: " << cardToDownload.getSetName() << "]: Image successfully "
<< (isFromCache ? "loaded from cached" : "downloaded from") << " url " << reply->url().toDisplayString();
} else {
startNextPicDownload();
}
reply->deleteLater();
}
bool PictureLoaderWorkerWork::imageIsBlackListed(const QByteArray &picData)
{
QString md5sum = QCryptographicHash::hash(picData, QCryptographicHash::Md5).toHex();
return md5Blacklist.contains(md5sum);
}

View file

@ -0,0 +1,50 @@
#ifndef PICTURE_LOADER_WORKER_WORK_H
#define PICTURE_LOADER_WORKER_WORK_H
#include "../../../game/cards/card_database.h"
#include "picture_loader_worker.h"
#include "picture_to_load.h"
#include <QLoggingCategory>
#include <QMutex>
#include <QNetworkAccessManager>
#include <QObject>
#include <QThread>
#define REDIRECT_HEADER_NAME "redirects"
#define REDIRECT_ORIGINAL_URL "original"
#define REDIRECT_URL "redirect"
#define REDIRECT_TIMESTAMP "timestamp"
#define REDIRECT_CACHE_FILENAME "cache.ini"
inline Q_LOGGING_CATEGORY(PictureLoaderWorkerWorkLog, "picture_loader.worker");
class PictureLoaderWorker;
class PictureLoaderWorkerWork : public QThread
{
Q_OBJECT
public:
explicit PictureLoaderWorkerWork(PictureLoaderWorker *worker, const CardInfoPtr &toLoad);
~PictureLoaderWorkerWork() override;
PictureLoaderWorker *worker;
PictureToLoad cardToDownload;
public slots:
void picDownloadFinished(QNetworkReply *reply);
void picDownloadFailed();
private:
static QStringList md5Blacklist;
QThread *pictureLoaderThread;
QNetworkAccessManager *networkManager;
bool picDownload, downloadRunning, loadQueueRunning;
void startNextPicDownload();
bool cardImageExistsOnDisk(QString &setName, QString &correctedCardName);
bool imageIsBlackListed(const QByteArray &);
signals:
void startLoadQueue();
void imageLoaded(CardInfoPtr card, const QImage &image);
void requestImageDownload(const QUrl &url, PictureLoaderWorkerWork *instance);
};
#endif // PICTURE_LOADER_WORKER_WORK_H

View file

@ -12,12 +12,13 @@ DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent,
QString _zoneName, QString _zoneName,
QString _activeGroupCriteria, QString _activeGroupCriteria,
QStringList _activeSortCriteria, QStringList _activeSortCriteria,
DisplayType _displayType,
int bannerOpacity, int bannerOpacity,
int subBannerOpacity, int subBannerOpacity,
CardSizeWidget *_cardSizeWidget) CardSizeWidget *_cardSizeWidget)
: QWidget(parent), deckListModel(_deckListModel), zoneName(_zoneName), activeGroupCriteria(_activeGroupCriteria), : QWidget(parent), deckListModel(_deckListModel), zoneName(_zoneName), activeGroupCriteria(_activeGroupCriteria),
activeSortCriteria(_activeSortCriteria), bannerOpacity(bannerOpacity), subBannerOpacity(subBannerOpacity), activeSortCriteria(_activeSortCriteria), displayType(_displayType), bannerOpacity(bannerOpacity),
cardSizeWidget(_cardSizeWidget) subBannerOpacity(subBannerOpacity), cardSizeWidget(_cardSizeWidget)
{ {
layout = new QVBoxLayout(this); layout = new QVBoxLayout(this);
setLayout(layout); setLayout(layout);
@ -60,7 +61,7 @@ void DeckCardZoneDisplayWidget::displayCards()
deleteCardGroupIfItDoesNotExist(); deleteCardGroupIfItDoesNotExist();
} }
void DeckCardZoneDisplayWidget::refreshDisplayType(const QString &_displayType) void DeckCardZoneDisplayWidget::refreshDisplayType(const DisplayType &_displayType)
{ {
displayType = _displayType; displayType = _displayType;
QLayoutItem *item; QLayoutItem *item;
@ -102,7 +103,7 @@ void DeckCardZoneDisplayWidget::addCardGroupIfItDoesNotExist()
continue; continue;
} }
if (displayType == "overlap") { if (displayType == DisplayType::Overlap) {
auto *display_widget = new OverlappedCardGroupDisplayWidget( auto *display_widget = new OverlappedCardGroupDisplayWidget(
cardGroupContainer, deckListModel, zoneName, cardGroup, activeGroupCriteria, activeSortCriteria, cardGroupContainer, deckListModel, zoneName, cardGroup, activeGroupCriteria, activeSortCriteria,
subBannerOpacity, cardSizeWidget); subBannerOpacity, cardSizeWidget);
@ -112,7 +113,7 @@ void DeckCardZoneDisplayWidget::addCardGroupIfItDoesNotExist()
connect(this, &DeckCardZoneDisplayWidget::activeSortCriteriaChanged, display_widget, connect(this, &DeckCardZoneDisplayWidget::activeSortCriteriaChanged, display_widget,
&CardGroupDisplayWidget::onActiveSortCriteriaChanged); &CardGroupDisplayWidget::onActiveSortCriteriaChanged);
cardGroupLayout->addWidget(display_widget); cardGroupLayout->addWidget(display_widget);
} else if (displayType == "flat") { } else if (displayType == DisplayType::Flat) {
auto *display_widget = new FlatCardGroupDisplayWidget(cardGroupContainer, deckListModel, zoneName, auto *display_widget = new FlatCardGroupDisplayWidget(cardGroupContainer, deckListModel, zoneName,
cardGroup, activeGroupCriteria, activeSortCriteria, cardGroup, activeGroupCriteria, activeSortCriteria,
subBannerOpacity, cardSizeWidget); subBannerOpacity, cardSizeWidget);

View file

@ -5,6 +5,7 @@
#include "../../../../game/cards/card_info.h" #include "../../../../game/cards/card_info.h"
#include "../general/display/banner_widget.h" #include "../general/display/banner_widget.h"
#include "../general/layout_containers/overlap_widget.h" #include "../general/layout_containers/overlap_widget.h"
#include "../visual_deck_editor/visual_deck_editor_widget.h"
#include "card_info_picture_with_text_overlay_widget.h" #include "card_info_picture_with_text_overlay_widget.h"
#include "card_size_widget.h" #include "card_size_widget.h"
@ -21,6 +22,7 @@ public:
QString zoneName, QString zoneName,
QString activeGroupCriteria, QString activeGroupCriteria,
QStringList activeSortCriteria, QStringList activeSortCriteria,
DisplayType displayType,
int bannerOpacity, int bannerOpacity,
int subBannerOpacity, int subBannerOpacity,
CardSizeWidget *_cardSizeWidget); CardSizeWidget *_cardSizeWidget);
@ -33,7 +35,7 @@ public slots:
void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card); void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card);
void onHover(CardInfoPtr card); void onHover(CardInfoPtr card);
void displayCards(); void displayCards();
void refreshDisplayType(const QString &displayType); void refreshDisplayType(const DisplayType &displayType);
void addCardGroupIfItDoesNotExist(); void addCardGroupIfItDoesNotExist();
void deleteCardGroupIfItDoesNotExist(); void deleteCardGroupIfItDoesNotExist();
void onActiveGroupCriteriaChanged(QString activeGroupCriteria); void onActiveGroupCriteriaChanged(QString activeGroupCriteria);
@ -48,6 +50,7 @@ signals:
private: private:
QString activeGroupCriteria; QString activeGroupCriteria;
QStringList activeSortCriteria; QStringList activeSortCriteria;
DisplayType displayType = DisplayType::Overlap;
int bannerOpacity = 20; int bannerOpacity = 20;
int subBannerOpacity = 10; int subBannerOpacity = 10;
CardSizeWidget *cardSizeWidget; CardSizeWidget *cardSizeWidget;
@ -55,7 +58,6 @@ private:
BannerWidget *banner; BannerWidget *banner;
QWidget *cardGroupContainer; QWidget *cardGroupContainer;
QVBoxLayout *cardGroupLayout; QVBoxLayout *cardGroupLayout;
QString displayType = "flat";
OverlapWidget *overlapWidget; OverlapWidget *overlapWidget;
}; };

View file

@ -104,6 +104,20 @@ void DeckEditorDeckDockWidget::createDeckDock()
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckModel->getDeckList()); deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckModel->getDeckList());
deckTagsDisplayWidget->setHidden(!SettingsCache::instance().getDeckEditorTagsWidgetVisible()); deckTagsDisplayWidget->setHidden(!SettingsCache::instance().getDeckEditorTagsWidgetVisible());
activeGroupCriteriaLabel = new QLabel(this);
activeGroupCriteriaComboBox = new QComboBox(this);
activeGroupCriteriaComboBox->addItem(tr("Main Type"), DeckListModelGroupCriteria::MAIN_TYPE);
activeGroupCriteriaComboBox->addItem(tr("Mana Cost"), DeckListModelGroupCriteria::MANA_COST);
activeGroupCriteriaComboBox->addItem(tr("Colors"), DeckListModelGroupCriteria::COLOR);
connect(activeGroupCriteriaComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), [this]() {
deckModel->setActiveGroupCriteria(
static_cast<DeckListModelGroupCriteria>(activeGroupCriteriaComboBox->currentData(Qt::UserRole).toInt()));
deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder());
deckView->expandAll();
deckView->expandAll();
});
aIncrement = new QAction(QString(), this); aIncrement = new QAction(QString(), this);
aIncrement->setIcon(QPixmap("theme:icons/increment")); aIncrement->setIcon(QPixmap("theme:icons/increment"));
connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrement); connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrement);
@ -144,6 +158,9 @@ void DeckEditorDeckDockWidget::createDeckDock()
upperLayout->addWidget(deckTagsDisplayWidget, 3, 1); upperLayout->addWidget(deckTagsDisplayWidget, 3, 1);
upperLayout->addWidget(activeGroupCriteriaLabel, 4, 0);
upperLayout->addWidget(activeGroupCriteriaComboBox, 4, 1);
hashLabel1 = new QLabel(); hashLabel1 = new QLabel();
hashLabel1->setObjectName("hashLabel1"); hashLabel1->setObjectName("hashLabel1");
auto *hashSizePolicy = new QSizePolicy(); auto *hashSizePolicy = new QSizePolicy();
@ -286,11 +303,7 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox()
}); });
for (const auto &pair : pairList) { for (const auto &pair : pairList) {
QVariantMap dataMap; bannerCardComboBox->addItem(pair.first, QVariant::fromValue(pair));
dataMap["name"] = pair.first;
dataMap["uuid"] = pair.second;
bannerCardComboBox->addItem(pair.first, dataMap);
} }
// Try to restore the previous selection by finding the currentText // Try to restore the previous selection by finding the currentText
@ -298,7 +311,7 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox()
if (restoredIndex != -1) { if (restoredIndex != -1) {
bannerCardComboBox->setCurrentIndex(restoredIndex); bannerCardComboBox->setCurrentIndex(restoredIndex);
if (deckModel->getDeckList()->getBannerCard().second != if (deckModel->getDeckList()->getBannerCard().second !=
bannerCardComboBox->itemData(bannerCardComboBox->currentIndex()).toMap()["uuid"].toString()) { bannerCardComboBox->currentData().value<QPair<QString, QString>>().second) {
setBannerCard(restoredIndex); setBannerCard(restoredIndex);
} }
} else { } else {
@ -318,9 +331,8 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox()
void DeckEditorDeckDockWidget::setBannerCard(int /* changedIndex */) void DeckEditorDeckDockWidget::setBannerCard(int /* changedIndex */)
{ {
QVariantMap itemData = bannerCardComboBox->itemData(bannerCardComboBox->currentIndex()).toMap(); auto cardAndId = bannerCardComboBox->currentData().value<QPair<QString, QString>>();
deckModel->getDeckList()->setBannerCard( deckModel->getDeckList()->setBannerCard(cardAndId);
QPair<QString, QString>(itemData["name"].toString(), itemData["uuid"].toString()));
emit deckModified(); emit deckModified();
} }
@ -578,6 +590,7 @@ void DeckEditorDeckDockWidget::retranslateUi()
showBannerCardCheckBox->setText(tr("Show banner card selection menu")); showBannerCardCheckBox->setText(tr("Show banner card selection menu"));
showTagsWidgetCheckBox->setText(tr("Show tags selection menu")); showTagsWidgetCheckBox->setText(tr("Show tags selection menu"));
commentsLabel->setText(tr("&Comments:")); commentsLabel->setText(tr("&Comments:"));
activeGroupCriteriaLabel->setText(tr("Group by:"));
hashLabel1->setText(tr("Hash:")); hashLabel1->setText(tr("Hash:"));
aIncrement->setText(tr("&Increment number")); aIncrement->setText(tr("&Increment number"));

View file

@ -70,6 +70,8 @@ private:
DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget; DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget;
QLabel *hashLabel1; QLabel *hashLabel1;
LineEditUnfocusable *hashLabel; LineEditUnfocusable *hashLabel;
QLabel *activeGroupCriteriaLabel;
QComboBox *activeGroupCriteriaComboBox;
QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard; QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;

View file

@ -26,6 +26,11 @@ void SettingsButtonWidget::addSettingsWidget(QWidget *toAdd) const
popup->addSettingsWidget(toAdd); popup->addSettingsWidget(toAdd);
} }
void SettingsButtonWidget::removeSettingsWidget(QWidget *toRemove) const
{
popup->removeSettingsWidget(toRemove);
}
void SettingsButtonWidget::setButtonIcon(QPixmap iconMap) void SettingsButtonWidget::setButtonIcon(QPixmap iconMap)
{ {
button->setIcon(iconMap); button->setIcon(iconMap);

View file

@ -13,6 +13,7 @@ class SettingsButtonWidget : public QWidget
public: public:
explicit SettingsButtonWidget(QWidget *parent = nullptr); explicit SettingsButtonWidget(QWidget *parent = nullptr);
void addSettingsWidget(QWidget *toAdd) const; void addSettingsWidget(QWidget *toAdd) const;
void removeSettingsWidget(QWidget *toRemove) const;
void setButtonIcon(QPixmap iconMap); void setButtonIcon(QPixmap iconMap);
protected: protected:
@ -25,6 +26,8 @@ private slots:
private: private:
QHBoxLayout *layout; QHBoxLayout *layout;
QToolButton *button; QToolButton *button;
public:
SettingsPopupWidget *popup; SettingsPopupWidget *popup;
}; };

View file

@ -29,6 +29,12 @@ void SettingsPopupWidget::addSettingsWidget(QWidget *toAdd) const
containerLayout->addWidget(toAdd); // Add to containerWidget's layout containerLayout->addWidget(toAdd); // Add to containerWidget's layout
} }
void SettingsPopupWidget::removeSettingsWidget(QWidget *toRemove) const
{
containerLayout->removeWidget(toRemove);
toRemove->deleteLater();
}
void SettingsPopupWidget::adjustSizeToFitScreen() void SettingsPopupWidget::adjustSizeToFitScreen()
{ {
QScreen *screen = QApplication::screenAt(this->pos()); QScreen *screen = QApplication::screenAt(this->pos());

View file

@ -13,6 +13,7 @@ class SettingsPopupWidget : public QWidget
public: public:
explicit SettingsPopupWidget(QWidget *parent = nullptr); explicit SettingsPopupWidget(QWidget *parent = nullptr);
void addSettingsWidget(QWidget *toAdd) const; void addSettingsWidget(QWidget *toAdd) const;
void removeSettingsWidget(QWidget *toRemove) const;
void adjustSizeToFitScreen(); void adjustSizeToFitScreen();
signals: signals:

View file

@ -125,7 +125,7 @@ void VisualDatabaseDisplayFilterSaveLoadWidget::refreshFilterList()
// Refresh the filter file list // Refresh the filter file list
QDir dir(SettingsCache::instance().getFiltersPath()); QDir dir(SettingsCache::instance().getFiltersPath());
QStringList filterFiles = dir.entryList(QStringList() << "*.json", QDir::Files, QDir::Time); QStringList filterFiles = dir.entryList(QStringList() << "*.json", QDir::Files, QDir::Name);
// Loop through the filter files and create widgets for them // Loop through the filter files and create widgets for them
for (const QString &filename : filterFiles) { for (const QString &filename : filterFiles) {

View file

@ -1,5 +1,6 @@
#include "visual_database_display_name_filter_widget.h" #include "visual_database_display_name_filter_widget.h"
#include "../../../../dialogs/dlg_load_deck_from_clipboard.h"
#include "../../../tabs/abstract_tab_deck_editor.h" #include "../../../tabs/abstract_tab_deck_editor.h"
#include <QHBoxLayout> #include <QHBoxLayout>
@ -35,6 +36,13 @@ VisualDatabaseDisplayNameFilterWidget::VisualDatabaseDisplayNameFilterWidget(QWi
layout->addWidget(loadFromDeckButton); layout->addWidget(loadFromDeckButton);
connect(loadFromDeckButton, &QPushButton::clicked, this, &VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck); connect(loadFromDeckButton, &QPushButton::clicked, this, &VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck);
loadFromClipboardButton = new QPushButton(this);
layout->addWidget(loadFromClipboardButton);
connect(loadFromClipboardButton, &QPushButton::clicked, this,
&VisualDatabaseDisplayNameFilterWidget::actLoadFromClipboard);
connect(filterModel, &FilterTreeModel::layoutChanged, this, connect(filterModel, &FilterTreeModel::layoutChanged, this,
[this]() { QTimer::singleShot(100, this, &VisualDatabaseDisplayNameFilterWidget::syncWithFilterModel); }); [this]() { QTimer::singleShot(100, this, &VisualDatabaseDisplayNameFilterWidget::syncWithFilterModel); });
@ -46,6 +54,8 @@ void VisualDatabaseDisplayNameFilterWidget::retranslateUi()
searchBox->setPlaceholderText(tr("Filter by name...")); searchBox->setPlaceholderText(tr("Filter by name..."));
loadFromDeckButton->setText(tr("Load from Deck")); loadFromDeckButton->setText(tr("Load from Deck"));
loadFromDeckButton->setToolTip(tr("Apply all card names in currently loaded deck as exact match name filters")); loadFromDeckButton->setToolTip(tr("Apply all card names in currently loaded deck as exact match name filters"));
loadFromClipboardButton->setText(tr("Load from Clipboard"));
loadFromClipboardButton->setToolTip(tr("Apply all card names in clipboard as exact match name filters"));
} }
void VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck() void VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck()
@ -75,6 +85,20 @@ void VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck()
updateFilterModel(); updateFilterModel();
} }
void VisualDatabaseDisplayNameFilterWidget::actLoadFromClipboard()
{
DlgLoadDeckFromClipboard dlg(this);
if (!dlg.exec())
return;
QStringList cardsInClipboard = dlg.getDeckList()->getCardList();
for (QString cardName : cardsInClipboard) {
createNameFilter(cardName);
}
updateFilterModel();
}
void VisualDatabaseDisplayNameFilterWidget::createNameFilter(const QString &name) void VisualDatabaseDisplayNameFilterWidget::createNameFilter(const QString &name)
{ {
if (activeFilters.contains(name)) if (activeFilters.contains(name))
@ -85,7 +109,11 @@ void VisualDatabaseDisplayNameFilterWidget::createNameFilter(const QString &name
button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }" button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }"
"QPushButton:hover { background-color: red; color: white; }"); "QPushButton:hover { background-color: red; color: white; }");
connect(button, &QPushButton::clicked, this, [this, name]() { removeNameFilter(name); }); connect(button, &QPushButton::clicked, this, [this, name]() {
removeNameFilter(name);
QTimer::singleShot(0, this,
&VisualDatabaseDisplayNameFilterWidget::updateFilterModel); // Avoid concurrent modification
});
flowWidget->addWidget(button); flowWidget->addWidget(button);
activeFilters[name] = button; activeFilters[name] = button;
@ -96,9 +124,6 @@ void VisualDatabaseDisplayNameFilterWidget::removeNameFilter(const QString &name
if (activeFilters.contains(name)) { if (activeFilters.contains(name)) {
activeFilters[name]->deleteLater(); // Safe deletion activeFilters[name]->deleteLater(); // Safe deletion
activeFilters.remove(name); activeFilters.remove(name);
QTimer::singleShot(0, this,
&VisualDatabaseDisplayNameFilterWidget::updateFilterModel); // Avoid concurrent modification
} }
} }

View file

@ -35,11 +35,13 @@ private:
QLineEdit *searchBox; QLineEdit *searchBox;
FlowWidget *flowWidget; FlowWidget *flowWidget;
QPushButton *loadFromDeckButton; QPushButton *loadFromDeckButton;
QPushButton *loadFromClipboardButton;
QMap<QString, QPushButton *> activeFilters; // Store active name filter buttons QMap<QString, QPushButton *> activeFilters; // Store active name filter buttons
private slots: private slots:
void actLoadFromDeck(); void actLoadFromDeck();
void actLoadFromClipboard();
}; };
#endif // VISUAL_DATABASE_DISPLAY_NAME_FILTER_WIDGET_H #endif // VISUAL_DATABASE_DISPLAY_NAME_FILTER_WIDGET_H

View file

@ -95,7 +95,15 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
clearFilterWidget = new QToolButton(); clearFilterWidget = new QToolButton();
clearFilterWidget->setFixedSize(32, 32); clearFilterWidget->setFixedSize(32, 32);
clearFilterWidget->setIcon(QPixmap("theme:icons/delete")); clearFilterWidget->setIcon(QPixmap("theme:icons/delete"));
connect(clearFilterWidget, &QToolButton::clicked, this, [this] { filterModel->clear(); }); connect(clearFilterWidget, &QToolButton::clicked, this, [this] {
filterModel->blockSignals(true);
filterModel->filterTree()->blockSignals(true);
filterModel->clear();
filterModel->blockSignals(false);
filterModel->filterTree()->blockSignals(false);
emit filterModel->filterTree()->changed();
emit filterModel->layoutChanged();
});
quickFilterSaveLoadWidget = new SettingsButtonWidget(this); quickFilterSaveLoadWidget = new SettingsButtonWidget(this);
quickFilterSaveLoadWidget->setButtonIcon(QPixmap("theme:icons/lock")); quickFilterSaveLoadWidget->setButtonIcon(QPixmap("theme:icons/lock"));

View file

@ -193,7 +193,7 @@ void VisualDeckEditorWidget::retranslateUi()
searchPushButton->setToolTip(tr("Search for closest match in the database (with auto-suggestions) and add " searchPushButton->setToolTip(tr("Search for closest match in the database (with auto-suggestions) and add "
"preferred printing to the deck on pressing enter")); "preferred printing to the deck on pressing enter"));
sortCriteriaButton->setToolTip(tr("Configure how cards are sorted within their groups")); sortCriteriaButton->setToolTip(tr("Configure how cards are sorted within their groups"));
displayTypeButton->setText(tr("Flat Layout")); displayTypeButton->setText(tr("Overlap Layout"));
displayTypeButton->setToolTip( displayTypeButton->setToolTip(
tr("Change how cards are displayed within zones (i.e. overlapped or fully visible.)")); tr("Change how cards are displayed within zones (i.e. overlapped or fully visible.)"));
} }
@ -212,14 +212,13 @@ void VisualDeckEditorWidget::updateDisplayType()
// Update UI and emit signal // Update UI and emit signal
switch (currentDisplayType) { switch (currentDisplayType) {
case DisplayType::Flat: case DisplayType::Flat:
emit displayTypeChanged("flat");
displayTypeButton->setText(tr("Flat Layout")); displayTypeButton->setText(tr("Flat Layout"));
break; break;
case DisplayType::Overlap: case DisplayType::Overlap:
emit displayTypeChanged("overlap");
displayTypeButton->setText(tr("Overlap Layout")); displayTypeButton->setText(tr("Overlap Layout"));
break; break;
} }
emit displayTypeChanged(currentDisplayType);
} }
void VisualDeckEditorWidget::addZoneIfDoesNotExist() void VisualDeckEditorWidget::addZoneIfDoesNotExist()
@ -239,8 +238,9 @@ void VisualDeckEditorWidget::addZoneIfDoesNotExist()
if (found) { if (found) {
continue; continue;
} }
DeckCardZoneDisplayWidget *zoneDisplayWidget = new DeckCardZoneDisplayWidget( DeckCardZoneDisplayWidget *zoneDisplayWidget =
zoneContainer, deckListModel, zone, activeGroupCriteria, activeSortCriteria, 20, 10, cardSizeWidget); new DeckCardZoneDisplayWidget(zoneContainer, deckListModel, zone, activeGroupCriteria, activeSortCriteria,
currentDisplayType, 20, 10, cardSizeWidget);
connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardHovered, this, &VisualDeckEditorWidget::onHover); connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardHovered, this, &VisualDeckEditorWidget::onHover);
connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardClicked, this, &VisualDeckEditorWidget::onCardClick); connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardClicked, this, &VisualDeckEditorWidget::onCardClick);
connect(this, &VisualDeckEditorWidget::activeSortCriteriaChanged, zoneDisplayWidget, connect(this, &VisualDeckEditorWidget::activeSortCriteriaChanged, zoneDisplayWidget,

View file

@ -50,7 +50,7 @@ signals:
void activeSortCriteriaChanged(QStringList activeSortCriteria); void activeSortCriteriaChanged(QStringList activeSortCriteria);
void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName); void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName);
void cardAdditionRequested(CardInfoPtr card); void cardAdditionRequested(CardInfoPtr card);
void displayTypeChanged(QString displayType); void displayTypeChanged(DisplayType displayType);
protected slots: protected slots:
void onHover(CardInfoPtr hoveredCard); void onHover(CardInfoPtr hoveredCard);

View file

@ -12,6 +12,7 @@
#include <QDirIterator> #include <QDirIterator>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
#include <QMessageBox>
DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckList *_deckList) DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckList *_deckList)
: QWidget(_parent), deckList(nullptr) : QWidget(_parent), deckList(nullptr)
@ -77,6 +78,21 @@ static QStringList getAllFiles(const QString &filePath)
return allFiles; return allFiles;
} }
bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
{
QFileInfo fileInfo(filePath);
QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod");
if (QFile::exists(newFileName)) {
QMessageBox::StandardButton reply =
QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"),
QObject::tr("A .cod version of this deck already exists. Overwrite it?"),
QMessageBox::Yes | QMessageBox::No);
return reply == QMessageBox::Yes;
}
return true; // Safe to proceed
}
void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg() void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
{ {
if (qobject_cast<DeckPreviewWidget *>(parentWidget())) { if (qobject_cast<DeckPreviewWidget *>(parentWidget())) {
@ -91,6 +107,10 @@ void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
// Retrieve saved preference if the prompt is disabled // Retrieve saved preference if the prompt is disabled
if (!SettingsCache::instance().getVisualDeckStoragePromptForConversion()) { if (!SettingsCache::instance().getVisualDeckStoragePromptForConversion()) {
if (SettingsCache::instance().getVisualDeckStorageAlwaysConvert()) { if (SettingsCache::instance().getVisualDeckStorageAlwaysConvert()) {
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath))
return;
deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath); deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath);
deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastFileName(); deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastFileName();
deckPreviewWidget->refreshBannerCardText(); deckPreviewWidget->refreshBannerCardText();
@ -100,6 +120,10 @@ void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
// Show the dialog to the user // Show the dialog to the user
DialogConvertDeckToCodFormat conversionDialog(parentWidget()); DialogConvertDeckToCodFormat conversionDialog(parentWidget());
if (conversionDialog.exec() == QDialog::Accepted) { if (conversionDialog.exec() == QDialog::Accepted) {
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath))
return;
deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath); deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath);
deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastFileName(); deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastFileName();
deckPreviewWidget->refreshBannerCardText(); deckPreviewWidget->refreshBannerCardText();

View file

@ -50,10 +50,10 @@ void DeckPreviewTagDisplayWidget::mousePressEvent(QMouseEvent *event)
{ {
switch (event->button()) { switch (event->button()) {
case Qt::LeftButton: case Qt::LeftButton:
setState(TagState::Selected); setState(state != TagState::Selected ? TagState::Selected : TagState::NotSelected);
break; break;
case Qt::RightButton: case Qt::RightButton:
setState(TagState::Excluded); setState(state != TagState::Excluded ? TagState::Excluded : TagState::NotSelected);
break; break;
case Qt::MiddleButton: case Qt::MiddleButton:
setState(TagState::NotSelected); setState(TagState::NotSelected);

View file

@ -46,6 +46,8 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent,
&DeckPreviewWidget::updateTagsVisibility); &DeckPreviewWidget::updateTagsVisibility);
connect(&SettingsCache::instance(), &SettingsCache::visualDeckStorageShowBannerCardComboBoxChanged, this, connect(&SettingsCache::instance(), &SettingsCache::visualDeckStorageShowBannerCardComboBoxChanged, this,
&DeckPreviewWidget::updateBannerCardComboBoxVisibility); &DeckPreviewWidget::updateBannerCardComboBoxVisibility);
connect(visualDeckStorageWidget->settings(), &VisualDeckStorageQuickSettingsWidget::deckPreviewTooltipChanged, this,
&DeckPreviewWidget::refreshBannerCardToolTip);
layout->addWidget(bannerCardDisplayWidget); layout->addWidget(bannerCardDisplayWidget);
} }
@ -79,7 +81,6 @@ void DeckPreviewWidget::initializeUi(const bool deckLoadSuccess)
bannerCardDisplayWidget->setCard(bannerCard); bannerCardDisplayWidget->setCard(bannerCard);
bannerCardDisplayWidget->setFontSize(24); bannerCardDisplayWidget->setFontSize(24);
refreshBannerCardText();
setFilePath(deckLoader->getLastFileName()); setFilePath(deckLoader->getLastFileName());
colorIdentityWidget = new ColorIdentityWidget(this, getColorIdentity()); colorIdentityWidget = new ColorIdentityWidget(this, getColorIdentity());
@ -104,6 +105,8 @@ void DeckPreviewWidget::initializeUi(const bool deckLoadSuccess)
layout->addWidget(bannerCardLabel); layout->addWidget(bannerCardLabel);
layout->addWidget(bannerCardComboBox); layout->addWidget(bannerCardComboBox);
refreshBannerCardText();
retranslateUi(); retranslateUi();
} }
@ -179,15 +182,42 @@ QString DeckPreviewWidget::getColorIdentity()
return colorIdentity; return colorIdentity;
} }
/**
* The display name is given by the deck name, or the filename if the deck name is not set.
*/
QString DeckPreviewWidget::getDisplayName() const
{
return deckLoader->getName().isEmpty() ? QFileInfo(deckLoader->getLastFileName()).fileName()
: deckLoader->getName();
}
void DeckPreviewWidget::setFilePath(const QString &_filePath) void DeckPreviewWidget::setFilePath(const QString &_filePath)
{ {
filePath = _filePath; filePath = _filePath;
} }
/**
* Refreshes the banner card text.
* This also calls `refreshBannerCardToolTip`, since those two often need to be updated together.
*/
void DeckPreviewWidget::refreshBannerCardText() void DeckPreviewWidget::refreshBannerCardText()
{ {
bannerCardDisplayWidget->setOverlayText( bannerCardDisplayWidget->setOverlayText(getDisplayName());
deckLoader->getName().isEmpty() ? QFileInfo(deckLoader->getLastFileName()).fileName() : deckLoader->getName());
refreshBannerCardToolTip();
}
void DeckPreviewWidget::refreshBannerCardToolTip()
{
auto type = visualDeckStorageWidget->settings()->getDeckPreviewTooltip();
switch (type) {
case VisualDeckStorageQuickSettingsWidget::TooltipType::None:
bannerCardDisplayWidget->setToolTip("");
break;
case VisualDeckStorageQuickSettingsWidget::TooltipType::Filepath:
bannerCardDisplayWidget->setToolTip(filePath);
break;
}
} }
void DeckPreviewWidget::updateBannerCardComboBox() void DeckPreviewWidget::updateBannerCardComboBox()
@ -260,11 +290,11 @@ void DeckPreviewWidget::updateBannerCardComboBox()
void DeckPreviewWidget::setBannerCard(int /* changedIndex */) void DeckPreviewWidget::setBannerCard(int /* changedIndex */)
{ {
QVariant itemData = bannerCardComboBox->itemData(bannerCardComboBox->currentIndex()); auto nameAndId = bannerCardComboBox->currentData().value<QPair<QString, QString>>();
deckLoader->setBannerCard(QPair<QString, QString>(bannerCardComboBox->currentText(), itemData.toString())); deckLoader->setBannerCard(nameAndId);
deckLoader->saveToFile(filePath, DeckLoader::getFormatFromName(filePath)); deckLoader->saveToFile(filePath, DeckLoader::getFormatFromName(filePath));
bannerCardDisplayWidget->setCard(CardDatabaseManager::getInstance()->getCardByNameAndProviderId( bannerCardDisplayWidget->setCard(
bannerCardComboBox->currentText(), itemData.toString())); CardDatabaseManager::getInstance()->getCardByNameAndProviderId(nameAndId.first, nameAndId.second));
} }
void DeckPreviewWidget::imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance) void DeckPreviewWidget::imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance)

View file

@ -27,6 +27,7 @@ public:
const QString &_filePath); const QString &_filePath);
void retranslateUi(); void retranslateUi();
QString getColorIdentity(); QString getColorIdentity();
QString getDisplayName() const;
VisualDeckStorageWidget *visualDeckStorageWidget; VisualDeckStorageWidget *visualDeckStorageWidget;
QVBoxLayout *layout; QVBoxLayout *layout;
@ -49,6 +50,7 @@ signals:
public slots: public slots:
void setFilePath(const QString &filePath); void setFilePath(const QString &filePath);
void refreshBannerCardText(); void refreshBannerCardText();
void refreshBannerCardToolTip();
void updateBannerCardComboBox(); void updateBannerCardComboBox();
void setBannerCard(int); void setBannerCard(int);
void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);

View file

@ -4,6 +4,7 @@
#include "visual_deck_storage_widget.h" #include "visual_deck_storage_widget.h"
#include <QCheckBox> #include <QCheckBox>
#include <QComboBox>
#include <QSpinBox> #include <QSpinBox>
VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidget *parent) VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidget *parent)
@ -41,14 +42,6 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg
connect(showBannerCardComboBoxCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), connect(showBannerCardComboBoxCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
&SettingsCache::setVisualDeckStorageShowBannerCardComboBox); &SettingsCache::setVisualDeckStorageShowBannerCardComboBox);
// search folder names checkbox
searchFolderNamesCheckBox = new QCheckBox(this);
searchFolderNamesCheckBox->setChecked(SettingsCache::instance().getVisualDeckStorageSearchFolderNames());
connect(searchFolderNamesCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
&VisualDeckStorageQuickSettingsWidget::searchFolderNamesChanged);
connect(searchFolderNamesCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
&SettingsCache::setVisualDeckStorageSearchFolderNames);
// draw unused color identities checkbox // draw unused color identities checkbox
drawUnusedColorIdentitiesCheckBox = new QCheckBox(this); drawUnusedColorIdentitiesCheckBox = new QCheckBox(this);
drawUnusedColorIdentitiesCheckBox->setChecked( drawUnusedColorIdentitiesCheckBox->setChecked(
@ -80,6 +73,26 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg
unusedColorIdentityOpacityLayout->addWidget(unusedColorIdentitiesOpacityLabel); unusedColorIdentityOpacityLayout->addWidget(unusedColorIdentitiesOpacityLabel);
unusedColorIdentityOpacityLayout->addWidget(unusedColorIdentitiesOpacitySpinBox); unusedColorIdentityOpacityLayout->addWidget(unusedColorIdentitiesOpacitySpinBox);
// tooltip selector
auto deckPreviewTooltipWidget = new QWidget(this);
deckPreviewTooltipLabel = new QLabel(deckPreviewTooltipWidget);
deckPreviewTooltipComboBox = new QComboBox(deckPreviewTooltipWidget);
deckPreviewTooltipComboBox->setFocusPolicy(Qt::StrongFocus);
deckPreviewTooltipComboBox->addItem("", TooltipType::None);
deckPreviewTooltipComboBox->addItem("", TooltipType::Filepath);
deckPreviewTooltipComboBox->setCurrentIndex(SettingsCache::instance().getVisualDeckStorageTooltipType());
connect(deckPreviewTooltipComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
[this] { emit deckPreviewTooltipChanged(getDeckPreviewTooltip()); });
connect(deckPreviewTooltipComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), &SettingsCache::instance(),
&SettingsCache::setVisualDeckStorageTooltipType);
auto deckPreviewTooltipLayout = new QHBoxLayout(deckPreviewTooltipWidget);
deckPreviewTooltipLayout->setContentsMargins(11, 0, 11, 0);
deckPreviewTooltipLayout->addWidget(deckPreviewTooltipLabel);
deckPreviewTooltipLayout->addWidget(deckPreviewTooltipComboBox);
// card size slider // card size slider
cardSizeWidget = new CardSizeWidget(this, nullptr, SettingsCache::instance().getVisualDeckStorageCardSize()); cardSizeWidget = new CardSizeWidget(this, nullptr, SettingsCache::instance().getVisualDeckStorageCardSize());
connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, this, connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, this,
@ -92,9 +105,9 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg
this->addSettingsWidget(showTagFilterCheckBox); this->addSettingsWidget(showTagFilterCheckBox);
this->addSettingsWidget(showTagsOnDeckPreviewsCheckBox); this->addSettingsWidget(showTagsOnDeckPreviewsCheckBox);
this->addSettingsWidget(showBannerCardComboBoxCheckBox); this->addSettingsWidget(showBannerCardComboBoxCheckBox);
this->addSettingsWidget(searchFolderNamesCheckBox);
this->addSettingsWidget(drawUnusedColorIdentitiesCheckBox); this->addSettingsWidget(drawUnusedColorIdentitiesCheckBox);
this->addSettingsWidget(unusedColorIdentityOpacityWidget); this->addSettingsWidget(unusedColorIdentityOpacityWidget);
this->addSettingsWidget(deckPreviewTooltipWidget);
this->addSettingsWidget(cardSizeWidget); this->addSettingsWidget(cardSizeWidget);
connect(&SettingsCache::instance(), &SettingsCache::langChanged, this, connect(&SettingsCache::instance(), &SettingsCache::langChanged, this,
@ -108,10 +121,13 @@ void VisualDeckStorageQuickSettingsWidget::retranslateUi()
showTagFilterCheckBox->setText(tr("Show Tag Filter")); showTagFilterCheckBox->setText(tr("Show Tag Filter"));
showTagsOnDeckPreviewsCheckBox->setText(tr("Show Tags On Deck Previews")); showTagsOnDeckPreviewsCheckBox->setText(tr("Show Tags On Deck Previews"));
showBannerCardComboBoxCheckBox->setText(tr("Show Banner Card Selection Option")); showBannerCardComboBoxCheckBox->setText(tr("Show Banner Card Selection Option"));
searchFolderNamesCheckBox->setText(tr("Include Folder Names in Search"));
drawUnusedColorIdentitiesCheckBox->setText(tr("Draw unused Color Identities")); drawUnusedColorIdentitiesCheckBox->setText(tr("Draw unused Color Identities"));
unusedColorIdentitiesOpacityLabel->setText(tr("Unused Color Identities Opacity")); unusedColorIdentitiesOpacityLabel->setText(tr("Unused Color Identities Opacity"));
unusedColorIdentitiesOpacitySpinBox->setSuffix("%"); unusedColorIdentitiesOpacitySpinBox->setSuffix("%");
deckPreviewTooltipLabel->setText(tr("Deck tooltip:"));
deckPreviewTooltipComboBox->setItemText(0, tr("None"));
deckPreviewTooltipComboBox->setItemText(1, tr("Filepath"));
} }
bool VisualDeckStorageQuickSettingsWidget::getShowFolders() const bool VisualDeckStorageQuickSettingsWidget::getShowFolders() const
@ -139,16 +155,16 @@ bool VisualDeckStorageQuickSettingsWidget::getShowTagsOnDeckPreviews() const
return showTagsOnDeckPreviewsCheckBox->isChecked(); return showTagsOnDeckPreviewsCheckBox->isChecked();
} }
bool VisualDeckStorageQuickSettingsWidget::getSearchFolderNames() const
{
return searchFolderNamesCheckBox->isChecked();
}
int VisualDeckStorageQuickSettingsWidget::getUnusedColorIdentitiesOpacity() const int VisualDeckStorageQuickSettingsWidget::getUnusedColorIdentitiesOpacity() const
{ {
return unusedColorIdentitiesOpacitySpinBox->value(); return unusedColorIdentitiesOpacitySpinBox->value();
} }
VisualDeckStorageQuickSettingsWidget::TooltipType VisualDeckStorageQuickSettingsWidget::getDeckPreviewTooltip() const
{
return deckPreviewTooltipComboBox->currentData().value<TooltipType>();
}
int VisualDeckStorageQuickSettingsWidget::getCardSize() const int VisualDeckStorageQuickSettingsWidget::getCardSize() const
{ {
return cardSizeWidget->getSlider()->value(); return cardSizeWidget->getSlider()->value();

View file

@ -7,6 +7,7 @@ class CardSizeWidget;
class QLabel; class QLabel;
class QSpinBox; class QSpinBox;
class QCheckBox; class QCheckBox;
class QComboBox;
/** /**
* The VDS's quick settings menu. * The VDS's quick settings menu.
@ -22,12 +23,23 @@ class VisualDeckStorageQuickSettingsWidget : public SettingsButtonWidget
QCheckBox *showBannerCardComboBoxCheckBox; QCheckBox *showBannerCardComboBoxCheckBox;
QCheckBox *showTagFilterCheckBox; QCheckBox *showTagFilterCheckBox;
QCheckBox *showTagsOnDeckPreviewsCheckBox; QCheckBox *showTagsOnDeckPreviewsCheckBox;
QCheckBox *searchFolderNamesCheckBox;
QLabel *unusedColorIdentitiesOpacityLabel; QLabel *unusedColorIdentitiesOpacityLabel;
QSpinBox *unusedColorIdentitiesOpacitySpinBox; QSpinBox *unusedColorIdentitiesOpacitySpinBox;
QLabel *deckPreviewTooltipLabel;
QComboBox *deckPreviewTooltipComboBox;
CardSizeWidget *cardSizeWidget; CardSizeWidget *cardSizeWidget;
public: public:
/**
* The info to display in the deck preview's banner card tooltip.
*/
enum TooltipType
{
None,
Filepath
};
Q_ENUM(TooltipType)
explicit VisualDeckStorageQuickSettingsWidget(QWidget *parent = nullptr); explicit VisualDeckStorageQuickSettingsWidget(QWidget *parent = nullptr);
void retranslateUi(); void retranslateUi();
@ -37,8 +49,8 @@ public:
bool getShowBannerCardComboBox() const; bool getShowBannerCardComboBox() const;
bool getShowTagFilter() const; bool getShowTagFilter() const;
bool getShowTagsOnDeckPreviews() const; bool getShowTagsOnDeckPreviews() const;
bool getSearchFolderNames() const;
int getUnusedColorIdentitiesOpacity() const; int getUnusedColorIdentitiesOpacity() const;
TooltipType getDeckPreviewTooltip() const;
int getCardSize() const; int getCardSize() const;
signals: signals:
@ -47,8 +59,8 @@ signals:
void showBannerCardComboBoxChanged(bool enabled); void showBannerCardComboBoxChanged(bool enabled);
void showTagFilterChanged(bool enabled); void showTagFilterChanged(bool enabled);
void showTagsOnDeckPreviewsChanged(bool enabled); void showTagsOnDeckPreviewsChanged(bool enabled);
void searchFolderNamesChanged(bool enabled);
void unusedColorIdentitiesOpacityChanged(int opacity); void unusedColorIdentitiesOpacityChanged(int opacity);
void deckPreviewTooltipChanged(TooltipType tooltip);
void cardSizeChanged(int scale); void cardSizeChanged(int scale);
}; };

View file

@ -1,6 +1,11 @@
#include "visual_deck_storage_search_widget.h" #include "visual_deck_storage_search_widget.h"
#include "../../../../game/filters/deck_filter_string.h"
#include "../../../../game/filters/syntax_help.h"
#include "../../../../settings/cache_settings.h" #include "../../../../settings/cache_settings.h"
#include "../../pixel_map_generator.h"
#include <QAction>
/** /**
* @brief Constructs a PrintingSelectorCardSearchWidget for searching cards by set name or set code. * @brief Constructs a PrintingSelectorCardSearchWidget for searching cards by set name or set code.
@ -17,7 +22,13 @@ VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(VisualDeckStorageWi
setLayout(layout); setLayout(layout);
searchBar = new QLineEdit(this); searchBar = new QLineEdit(this);
searchBar->setPlaceholderText(tr("Search by filename")); searchBar->setPlaceholderText(tr("Search by filename (or search expression)"));
searchBar->setClearButtonEnabled(true);
searchBar->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
auto help = searchBar->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition);
connect(help, &QAction::triggered, this, [this] { createDeckSearchSyntaxHelpWindow(searchBar); });
layout->addWidget(searchBar); layout->addWidget(searchBar);
// Add a debounce timer for the search bar to limit frequent updates // Add a debounce timer for the search bar to limit frequent updates
@ -41,37 +52,29 @@ QString VisualDeckStorageSearchWidget::getSearchText()
} }
/** /**
* Gets the filename used for the search. * Converts the filepath into a relative filepath starting from the deck folder.
* If the file isn't in the deck folder, then this will just return the filename.
* *
* if includeFolderName is true, then this returns the relative filepath starting from the deck folder. * @param filePath The filepath to convert into a relative filepath
* If the file isn't in the deck folder, or includeFolderName is false, then this will just return the filename.
*
* @param filePath The filePath to convert into a search name
*/ */
static QString getFileSearchName(const QString &filePath, bool includeFolderName) static QString toRelativeFilepath(const QString &filePath)
{ {
QString deckPath = SettingsCache::instance().getDeckPath(); QString deckPath = SettingsCache::instance().getDeckPath();
if (includeFolderName && filePath.startsWith(deckPath)) { if (filePath.startsWith(deckPath)) {
return filePath.mid(deckPath.length()).toLower(); return filePath.mid(deckPath.length());
} }
QFileInfo fileInfo(filePath); QFileInfo fileInfo(filePath);
QString fileName = fileInfo.fileName().toLower(); QString fileName = fileInfo.fileName();
return fileName; return fileName;
} }
void VisualDeckStorageSearchWidget::filterWidgets(QList<DeckPreviewWidget *> widgets, void VisualDeckStorageSearchWidget::filterWidgets(QList<DeckPreviewWidget *> widgets, const QString &searchText)
const QString &searchText,
bool includeFolderName)
{ {
if (searchText.isEmpty() || searchText.isNull()) { auto filterString = DeckFilterString(searchText);
for (auto widget : widgets) {
widget->filteredBySearch = false;
}
}
for (auto file : widgets) { for (auto widget : widgets) {
QString fileSearchName = getFileSearchName(file->filePath, includeFolderName); QString relativeFilePath = toRelativeFilepath(widget->filePath);
file->filteredBySearch = !fileSearchName.contains(searchText.toLower()); widget->filteredBySearch = !filterString.check(widget, {relativeFilePath});
} }
} }

View file

@ -16,7 +16,7 @@ class VisualDeckStorageSearchWidget : public QWidget
public: public:
explicit VisualDeckStorageSearchWidget(VisualDeckStorageWidget *parent); explicit VisualDeckStorageSearchWidget(VisualDeckStorageWidget *parent);
QString getSearchText(); QString getSearchText();
void filterWidgets(QList<DeckPreviewWidget *> widgets, const QString &searchText, bool includeFolderName); void filterWidgets(QList<DeckPreviewWidget *> widgets, const QString &searchText);
private: private:
QHBoxLayout *layout; QHBoxLayout *layout;

View file

@ -46,8 +46,6 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare
&VisualDeckStorageWidget::updateShowFolders); &VisualDeckStorageWidget::updateShowFolders);
connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::showTagFilterChanged, this, connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::showTagFilterChanged, this,
&VisualDeckStorageWidget::updateTagsVisibility); &VisualDeckStorageWidget::updateTagsVisibility);
connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::searchFolderNamesChanged, this,
&VisualDeckStorageWidget::updateSearchFilter);
searchAndSortLayout->addWidget(deckPreviewColorIdentityFilterWidget); searchAndSortLayout->addWidget(deckPreviewColorIdentityFilterWidget);
searchAndSortLayout->addWidget(sortWidget); searchAndSortLayout->addWidget(sortWidget);
@ -196,8 +194,7 @@ void VisualDeckStorageWidget::updateColorFilter()
void VisualDeckStorageWidget::updateSearchFilter() void VisualDeckStorageWidget::updateSearchFilter()
{ {
if (folderWidget) { if (folderWidget) {
searchWidget->filterWidgets(folderWidget->findChildren<DeckPreviewWidget *>(), searchWidget->getSearchText(), searchWidget->filterWidgets(folderWidget->findChildren<DeckPreviewWidget *>(), searchWidget->getSearchText());
quickSettingsWidget->getSearchFolderNames());
folderWidget->updateVisibility(); folderWidget->updateVisibility();
} }
} }

View file

@ -1099,7 +1099,7 @@ void MainWindow::cardDatabaseLoadingFailed()
void MainWindow::cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames) void MainWindow::cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames)
{ {
QMessageBox msgBox; QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("New sets found")); msgBox.setWindowTitle(tr("New sets found"));
msgBox.setIcon(QMessageBox::Question); msgBox.setIcon(QMessageBox::Question);
msgBox.setText(tr("%n new set(s) found in the card database\n" msgBox.setText(tr("%n new set(s) found in the card database\n"

View file

@ -29,6 +29,24 @@ DeckListModel::~DeckListModel()
delete root; delete root;
} }
QString DeckListModel::getSortCriteriaForCard(CardInfoPtr info)
{
if (!info) {
return "unknown";
}
switch (activeGroupCriteria) {
case DeckListModelGroupCriteria::MAIN_TYPE:
return info->getMainCardType();
case DeckListModelGroupCriteria::MANA_COST:
return info->getCmc();
case DeckListModelGroupCriteria::COLOR:
return info->getColors() == "" ? "Colorless" : info->getColors();
default:
return "unknown";
}
}
void DeckListModel::rebuildTree() void DeckListModel::rebuildTree()
{ {
beginResetModel(); beginResetModel();
@ -49,7 +67,7 @@ void DeckListModel::rebuildTree()
} }
CardInfoPtr info = CardDatabaseManager::getInstance()->getCard(currentCard->getName()); CardInfoPtr info = CardDatabaseManager::getInstance()->getCard(currentCard->getName());
QString cardType = info ? info->getMainCardType() : "unknown"; QString cardType = getSortCriteriaForCard(info);
auto *cardTypeNode = dynamic_cast<InnerDecklistNode *>(node->findChild(cardType)); auto *cardTypeNode = dynamic_cast<InnerDecklistNode *>(node->findChild(cardType));
@ -467,6 +485,12 @@ void DeckListModel::sort(int column, Qt::SortOrder order)
emit layoutChanged(); emit layoutChanged();
} }
void DeckListModel::setActiveGroupCriteria(DeckListModelGroupCriteria newCriteria)
{
activeGroupCriteria = newCriteria;
rebuildTree();
}
void DeckListModel::cleanList() void DeckListModel::cleanList()
{ {
setDeckList(new DeckLoader); setDeckList(new DeckLoader);

View file

@ -12,6 +12,13 @@ class CardDatabase;
class QPrinter; class QPrinter;
class QTextCursor; class QTextCursor;
enum DeckListModelGroupCriteria
{
MAIN_TYPE,
MANA_COST,
COLOR
};
class DecklistModelCardNode : public AbstractDecklistCardNode class DecklistModelCardNode : public AbstractDecklistCardNode
{ {
private: private:
@ -85,6 +92,7 @@ signals:
public: public:
explicit DeckListModel(QObject *parent = nullptr); explicit DeckListModel(QObject *parent = nullptr);
~DeckListModel() override; ~DeckListModel() override;
QString getSortCriteriaForCard(CardInfoPtr info);
int rowCount(const QModelIndex &parent) const override; int rowCount(const QModelIndex &parent) const override;
int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override; int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override; QVariant data(const QModelIndex &index, int role) const override;
@ -113,10 +121,12 @@ public:
QList<CardInfoPtr> getCardsAsCardInfoPtrs() const; QList<CardInfoPtr> getCardsAsCardInfoPtrs() const;
QList<CardInfoPtr> getCardsAsCardInfoPtrsForZone(QString zoneName) const; QList<CardInfoPtr> getCardsAsCardInfoPtrsForZone(QString zoneName) const;
QList<QString> *getZones() const; QList<QString> *getZones() const;
void setActiveGroupCriteria(DeckListModelGroupCriteria newCriteria);
private: private:
DeckLoader *deckList; DeckLoader *deckList;
InnerDecklistNode *root; InnerDecklistNode *root;
DeckListModelGroupCriteria activeGroupCriteria = DeckListModelGroupCriteria::MAIN_TYPE;
int lastKnownColumn; int lastKnownColumn;
Qt::SortOrder lastKnownOrder; Qt::SortOrder lastKnownOrder;
InnerDecklistNode *createNodeIfNeeded(const QString &name, InnerDecklistNode *parent); InnerDecklistNode *createNodeIfNeeded(const QString &name, InnerDecklistNode *parent);

View file

@ -4,6 +4,7 @@
#include "../../client/ui/picture_loader/picture_loader.h" #include "../../client/ui/picture_loader/picture_loader.h"
#include "../../deck/deck_loader.h" #include "../../deck/deck_loader.h"
#include "../../dialogs/dlg_load_deck.h" #include "../../dialogs/dlg_load_deck.h"
#include "../../dialogs/dlg_load_deck_from_clipboard.h"
#include "../../dialogs/dlg_load_remote_deck.h" #include "../../dialogs/dlg_load_remote_deck.h"
#include "../../server/pending_command.h" #include "../../server/pending_command.h"
#include "../../settings/cache_settings.h" #include "../../settings/cache_settings.h"
@ -52,6 +53,7 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
{ {
loadLocalButton = new QPushButton; loadLocalButton = new QPushButton;
loadRemoteButton = new QPushButton; loadRemoteButton = new QPushButton;
loadFromClipboardButton = new QPushButton;
unloadDeckButton = new QPushButton; unloadDeckButton = new QPushButton;
readyStartButton = new ToggleButton; readyStartButton = new ToggleButton;
forceStartGameButton = new QPushButton; forceStartGameButton = new QPushButton;
@ -59,6 +61,7 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
connect(loadLocalButton, &QPushButton::clicked, this, &DeckViewContainer::loadLocalDeck); connect(loadLocalButton, &QPushButton::clicked, this, &DeckViewContainer::loadLocalDeck);
connect(loadRemoteButton, &QPushButton::clicked, this, &DeckViewContainer::loadRemoteDeck); connect(loadRemoteButton, &QPushButton::clicked, this, &DeckViewContainer::loadRemoteDeck);
connect(loadFromClipboardButton, &QPushButton::clicked, this, &DeckViewContainer::loadFromClipboard);
connect(readyStartButton, &QPushButton::clicked, this, &DeckViewContainer::readyStart); connect(readyStartButton, &QPushButton::clicked, this, &DeckViewContainer::readyStart);
connect(unloadDeckButton, &QPushButton::clicked, this, &DeckViewContainer::unloadDeck); connect(unloadDeckButton, &QPushButton::clicked, this, &DeckViewContainer::unloadDeck);
connect(forceStartGameButton, &QPushButton::clicked, this, &DeckViewContainer::forceStart); connect(forceStartGameButton, &QPushButton::clicked, this, &DeckViewContainer::forceStart);
@ -68,6 +71,7 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
auto *buttonHBox = new QHBoxLayout; auto *buttonHBox = new QHBoxLayout;
buttonHBox->addWidget(loadLocalButton); buttonHBox->addWidget(loadLocalButton);
buttonHBox->addWidget(loadRemoteButton); buttonHBox->addWidget(loadRemoteButton);
buttonHBox->addWidget(loadFromClipboardButton);
buttonHBox->addWidget(unloadDeckButton); buttonHBox->addWidget(unloadDeckButton);
buttonHBox->addWidget(readyStartButton); buttonHBox->addWidget(readyStartButton);
buttonHBox->addWidget(sideboardLockButton); buttonHBox->addWidget(sideboardLockButton);
@ -118,6 +122,7 @@ void DeckViewContainer::retranslateUi()
{ {
loadLocalButton->setText(tr("Load deck...")); loadLocalButton->setText(tr("Load deck..."));
loadRemoteButton->setText(tr("Load remote deck...")); loadRemoteButton->setText(tr("Load remote deck..."));
loadFromClipboardButton->setText(tr("Load from clipboard..."));
unloadDeckButton->setText(tr("Unload deck")); unloadDeckButton->setText(tr("Unload deck"));
readyStartButton->setText(tr("Ready to start")); readyStartButton->setText(tr("Ready to start"));
forceStartGameButton->setText(tr("Force start")); forceStartGameButton->setText(tr("Force start"));
@ -148,6 +153,7 @@ void DeckViewContainer::switchToDeckSelectView()
setVisibility(loadLocalButton, true); setVisibility(loadLocalButton, true);
setVisibility(loadRemoteButton, !parentGame->getIsLocalGame()); setVisibility(loadRemoteButton, !parentGame->getIsLocalGame());
setVisibility(loadFromClipboardButton, true);
setVisibility(unloadDeckButton, false); setVisibility(unloadDeckButton, false);
setVisibility(readyStartButton, false); setVisibility(readyStartButton, false);
setVisibility(sideboardLockButton, false); setVisibility(sideboardLockButton, false);
@ -172,6 +178,7 @@ void DeckViewContainer::switchToDeckLoadedView()
setVisibility(loadLocalButton, false); setVisibility(loadLocalButton, false);
setVisibility(loadRemoteButton, false); setVisibility(loadRemoteButton, false);
setVisibility(loadFromClipboardButton, false);
setVisibility(unloadDeckButton, true); setVisibility(unloadDeckButton, true);
setVisibility(readyStartButton, true); setVisibility(readyStartButton, true);
setVisibility(sideboardLockButton, true); setVisibility(sideboardLockButton, true);
@ -198,8 +205,11 @@ void DeckViewContainer::refreshShortcuts()
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts(); ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
loadLocalButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadLocalButton")); loadLocalButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadLocalButton"));
loadRemoteButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadRemoteButton")); loadRemoteButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadRemoteButton"));
loadFromClipboardButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadFromClipboardButton"));
unloadDeckButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/unloadDeckButton"));
readyStartButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/readyStartButton")); readyStartButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/readyStartButton"));
sideboardLockButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/sideboardLockButton")); sideboardLockButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/sideboardLockButton"));
forceStartGameButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/forceStartGameButton"));
} }
/** /**
@ -247,19 +257,27 @@ void DeckViewContainer::loadLocalDeck()
void DeckViewContainer::loadDeckFromFile(const QString &filePath) void DeckViewContainer::loadDeckFromFile(const QString &filePath)
{ {
DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(filePath); DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(filePath);
QString deckString;
DeckLoader deck; DeckLoader deck;
bool error = !deck.loadFromFile(filePath, fmt, true); bool success = deck.loadFromFile(filePath, fmt, true);
if (!error) {
deckString = deck.writeToString_Native(); if (!success) {
error = deckString.length() > MAX_FILE_LENGTH;
}
if (error) {
QMessageBox::critical(this, tr("Error"), tr("The selected file could not be loaded.")); QMessageBox::critical(this, tr("Error"), tr("The selected file could not be loaded."));
return; return;
} }
loadDeckFromDeckLoader(&deck);
}
void DeckViewContainer::loadDeckFromDeckLoader(const DeckLoader *deck)
{
QString deckString = deck->writeToString_Native();
if (deckString.length() > MAX_FILE_LENGTH) {
QMessageBox::critical(this, tr("Error"), tr("Deck is greater than maximum file size."));
return;
}
Command_DeckSelect cmd; Command_DeckSelect cmd;
cmd.set_deck(deckString.toStdString()); cmd.set_deck(deckString.toStdString());
PendingCommand *pend = parentGame->prepareGameCommand(cmd); PendingCommand *pend = parentGame->prepareGameCommand(cmd);
@ -279,6 +297,18 @@ void DeckViewContainer::loadRemoteDeck()
} }
} }
void DeckViewContainer::loadFromClipboard()
{
auto dlg = DlgLoadDeckFromClipboard(this);
if (!dlg.exec()) {
return;
}
DeckLoader *deck = dlg.getDeckList();
loadDeckFromDeckLoader(deck);
}
void DeckViewContainer::deckSelectFinished(const Response &r) void DeckViewContainer::deckSelectFinished(const Response &r)
{ {
const Response_DeckDownload &resp = r.GetExtension(Response_DeckDownload::ext); const Response_DeckDownload &resp = r.GetExtension(Response_DeckDownload::ext);

View file

@ -44,7 +44,8 @@ class DeckViewContainer : public QWidget
Q_OBJECT Q_OBJECT
private: private:
QVBoxLayout *deckViewLayout; QVBoxLayout *deckViewLayout;
QPushButton *loadLocalButton, *loadRemoteButton, *unloadDeckButton, *forceStartGameButton; QPushButton *loadLocalButton, *loadRemoteButton, *loadFromClipboardButton;
QPushButton *unloadDeckButton, *forceStartGameButton;
ToggleButton *readyStartButton, *sideboardLockButton; ToggleButton *readyStartButton, *sideboardLockButton;
DeckView *deckView; DeckView *deckView;
VisualDeckStorageWidget *visualDeckStorageWidget; VisualDeckStorageWidget *visualDeckStorageWidget;
@ -58,6 +59,7 @@ private slots:
void switchToDeckLoadedView(); void switchToDeckLoadedView();
void loadLocalDeck(); void loadLocalDeck();
void loadRemoteDeck(); void loadRemoteDeck();
void loadFromClipboard();
void unloadDeck(); void unloadDeck();
void readyStart(); void readyStart();
void forceStart(); void forceStart();
@ -81,6 +83,7 @@ public:
public slots: public slots:
void loadDeckFromFile(const QString &filePath); void loadDeckFromFile(const QString &filePath);
void loadDeckFromDeckLoader(const DeckLoader *deck);
}; };
#endif // DECK_VIEW_CONTAINER_H #endif // DECK_VIEW_CONTAINER_H

View file

@ -0,0 +1,193 @@
#include "deck_filter_string.h"
#include "../cards/card_database_manager.h"
#include "filter_string.h"
#include "lib/peglib.h"
static peg::parser search(R"(
Start <- QueryPartList
~ws <- [ ]+
QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws*
ComplexQueryPart <- SomewhatComplexQueryPart ws "OR" ws ComplexQueryPart / SomewhatComplexQueryPart
SomewhatComplexQueryPart <- [(] QueryPartList [)] / QueryPart
QueryPart <- NotQuery / DeckContentQuery / DeckNameQuery / FileNameQuery / PathQuery / GenericQuery
NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart
DeckContentQuery <- CardSearch NumericExpression?
CardSearch <- '[[' CardFilterString ']]'
CardFilterString <- (!']]'.)*
DeckNameQuery <- ([Dd] 'eck')? [Nn] 'ame'? [:] String
FileNameQuery <- [Ff] ('ile' 'name'?)? [:] String
PathQuery <- [Pp] 'ath'? [:] String
GenericQuery <- String
NonDoubleQuoteUnlessEscaped <- '\\\"'. / !["].
NonSingleQuoteUnlessEscaped <- "\\\'". / !['].
UnescapedStringListPart <- !['":<>=! ].
SingleApostropheString <- (UnescapedStringListPart+ ws*)* ['] (UnescapedStringListPart+ ws*)*
String <- SingleApostropheString / UnescapedStringListPart+ / ["] <NonDoubleQuoteUnlessEscaped*> ["] / ['] <NonSingleQuoteUnlessEscaped*> [']
NumericExpression <- NumericOperator ws? NumericValue
NumericOperator <- [=:] / <[><!][=]?>
NumericValue <- [0-9]+
)");
static std::once_flag init;
static void setupParserRules()
{
// plumbing
auto passthru = [](const peg::SemanticValues &sv) -> DeckFilter {
return !sv.empty() ? std::any_cast<DeckFilter>(sv[0]) : nullptr;
};
search["Start"] = passthru;
search["QueryPartList"] = [](const peg::SemanticValues &sv) -> DeckFilter {
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) {
auto matchesFilter = [&deck, &info](const std::any &query) {
return std::any_cast<DeckFilter>(query)(deck, info);
};
return std::all_of(sv.begin(), sv.end(), matchesFilter);
};
};
search["ComplexQueryPart"] = [](const peg::SemanticValues &sv) -> DeckFilter {
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) {
auto matchesFilter = [&deck, &info](const std::any &query) {
return std::any_cast<DeckFilter>(query)(deck, info);
};
return std::any_of(sv.begin(), sv.end(), matchesFilter);
};
};
search["SomewhatComplexQueryPart"] = passthru;
search["QueryPart"] = passthru;
search["NotQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
const auto dependent = std::any_cast<DeckFilter>(sv[0]);
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) -> bool {
return !dependent(deck, info);
};
};
search["String"] = [](const peg::SemanticValues &sv) -> QString {
if (sv.choice() == 0) {
return QString::fromStdString(std::string(sv.sv()));
}
return QString::fromStdString(std::string(sv.token(0)));
};
search["NumericExpression"] = [](const peg::SemanticValues &sv) -> NumberMatcher {
const auto arg = std::any_cast<int>(sv[1]);
const auto op = std::any_cast<QString>(sv[0]);
if (op == ">")
return [=](const int s) { return s > arg; };
if (op == ">=")
return [=](const int s) { return s >= arg; };
if (op == "<")
return [=](const int s) { return s < arg; };
if (op == "<=")
return [=](const int s) { return s <= arg; };
if (op == "=")
return [=](const int s) { return s == arg; };
if (op == ":")
return [=](const int s) { return s == arg; };
if (op == "!=")
return [=](const int s) { return s != arg; };
return [](int) { return false; };
};
search["NumericValue"] = [](const peg::SemanticValues &sv) -> int {
return QString::fromStdString(std::string(sv.sv())).toInt();
};
search["NumericOperator"] = [](const peg::SemanticValues &sv) -> QString {
return QString::fromStdString(std::string(sv.sv()));
};
// actual functionality
search["DeckContentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto cardFilter = FilterString(std::any_cast<QString>(sv[0]));
auto numberMatcher = sv.size() > 1 ? std::any_cast<NumberMatcher>(sv[1]) : [](int count) { return count > 0; };
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) -> bool {
int count = 0;
deck->deckLoader->forEachCard([&](InnerDecklistNode *, const DecklistCardNode *node) {
auto cardInfoPtr = CardDatabaseManager::getInstance()->getCard(node->getName());
if (!cardInfoPtr.isNull() && cardFilter.check(cardInfoPtr)) {
count += node->getNumber();
}
});
return numberMatcher(count);
};
};
search["CardSearch"] = [](const peg::SemanticValues &sv) -> QString { return std::any_cast<QString>(sv[0]); };
search["CardFilterString"] = [](const peg::SemanticValues &sv) -> QString {
return QString::fromStdString(std::string(sv.sv()));
};
search["DeckNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto name = std::any_cast<QString>(sv[0]);
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
return deck->deckLoader->getName().contains(name, Qt::CaseInsensitive);
};
};
search["FileNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto name = std::any_cast<QString>(sv[0]);
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
auto filename = QFileInfo(deck->filePath).fileName();
return filename.contains(name, Qt::CaseInsensitive);
};
};
search["PathQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto name = std::any_cast<QString>(sv[0]);
return [=](const DeckPreviewWidget *, const ExtraDeckSearchInfo &info) {
return info.relativeFilePath.contains(name, Qt::CaseInsensitive);
};
};
search["GenericQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto name = std::any_cast<QString>(sv[0]);
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
return deck->getDisplayName().contains(name, Qt::CaseInsensitive);
};
};
}
DeckFilterString::DeckFilterString()
{
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; };
_error = "Not initialized";
}
DeckFilterString::DeckFilterString(const QString &expr)
{
QByteArray ba = expr.simplified().toUtf8();
std::call_once(init, setupParserRules);
_error = QString();
if (ba.isEmpty()) {
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return true; };
return;
}
search.set_logger([&](size_t /*ln*/, size_t col, const std::string &msg) {
_error = QString("Error at position %1: %2").arg(col).arg(QString::fromStdString(msg));
});
if (!search.parse(ba.data(), filter)) {
qCInfo(DeckFilterStringLog).nospace() << "DeckFilterString error for " << expr << "; " << qPrintable(_error);
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; };
}
}

View file

@ -0,0 +1,51 @@
#ifndef DECK_FILTER_STRING_H
#define DECK_FILTER_STRING_H
#include "../../client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h"
#include <QLoggingCategory>
#include <QMap>
#include <QString>
#include <functional>
#include <utility>
inline Q_LOGGING_CATEGORY(DeckFilterStringLog, "deck_filter_string");
/**
* Extra info relevant to filtering that isn't present in the DeckPreviewWidget
*/
struct ExtraDeckSearchInfo
{
/**
* The relative filepath starting from the deck folder
*/
QString relativeFilePath;
};
typedef std::function<bool(const DeckPreviewWidget *, const ExtraDeckSearchInfo &)> DeckFilter;
class DeckFilterString
{
public:
DeckFilterString();
explicit DeckFilterString(const QString &expr);
bool check(const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) const
{
return filter(deck, info);
}
bool valid() const
{
return _error.isEmpty();
}
QString error()
{
return _error;
}
private:
QString _error;
DeckFilter filter;
};
#endif // DECK_FILTER_STRING_H

View file

@ -4,10 +4,11 @@
#include <QByteArray> #include <QByteArray>
#include <QDebug> #include <QDebug>
#include <QRegularExpression>
#include <QString> #include <QString>
#include <functional> #include <functional>
peg::parser search(R"( static peg::parser search(R"(
Start <- QueryPartList Start <- QueryPartList
~ws <- [ ]+ ~ws <- [ ]+
QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws* QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws*
@ -20,7 +21,7 @@ QueryPart <- NotQuery / SetQuery / RarityQuery / CMCQuery / FormatQuery / PowerQ
NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart
SetQuery <- ('e'/'set') [:] FlexStringValue SetQuery <- ('e'/'set') [:] FlexStringValue
OracleQuery <- 'o' [:] RegexString OracleQuery <- 'o' [:] MatcherString
CMCQuery <- ('cmc'/'mv') ws? NumericExpression CMCQuery <- ('cmc'/'mv') ws? NumericExpression
@ -42,7 +43,7 @@ ColorEx <- Color / [mc]
ColorQuery <- [cC] 'olor'? <[iI]?> <[:!]> ColorEx* ColorQuery <- [cC] 'olor'? <[iI]?> <[:!]> ColorEx*
FieldQuery <- String [:] RegexString / String ws? NumericExpression FieldQuery <- String [:] MatcherString / String ws? NumericExpression
NonDoubleQuoteUnlessEscaped <- '\\\"'. / !["]. NonDoubleQuoteUnlessEscaped <- '\\\"'. / !["].
NonSingleQuoteUnlessEscaped <- "\\\'". / ![']. NonSingleQuoteUnlessEscaped <- "\\\'". / !['].
@ -52,8 +53,14 @@ String <- SingleApostropheString / UnescapedStringListPart+ / ["] <NonDoubleQuot
StringValue <- String / [(] StringList [)] StringValue <- String / [(] StringList [)]
StringList <- StringListString (ws? [,] ws? StringListString)* StringList <- StringListString (ws? [,] ws? StringListString)*
StringListString <- UnescapedStringListPart+ StringListString <- UnescapedStringListPart+
GenericQuery <- RegexString GenericQuery <- MatcherString
RegexString <- String
# A String that can either be a normal string or a regex search string
MatcherString <- RegexMatcher / NormalMatcher
NormalMatcher <- String
RegexMatcher <- '/' RegexMatcherString '/'
RegexMatcherString <- ('\\/' / !'/' .)+
FlexStringValue <- CompactStringSet / String / [(] StringList [)] FlexStringValue <- CompactStringSet / String / [(] StringList [)]
CompactStringSet <- StringListString ([,+] StringListString)+ CompactStringSet <- StringListString ([,+] StringListString)+
@ -63,7 +70,7 @@ NumericOperator <- [=:] / <[><!][=]?>
NumericValue <- [0-9]+ NumericValue <- [0-9]+
)"); )");
std::once_flag init; static std::once_flag init;
static void setupParserRules() static void setupParserRules()
{ {
@ -261,14 +268,22 @@ static void setupParserRules()
return QString::fromStdString(std::string(sv.sv())); return QString::fromStdString(std::string(sv.sv()));
}; };
search["RegexString"] = [](const peg::SemanticValues &sv) -> StringMatcher { search["NormalMatcher"] = [](const peg::SemanticValues &sv) -> StringMatcher {
auto target = std::any_cast<QString>(sv[0]); auto target = std::any_cast<QString>(sv[0]);
return [=](const QString &s) { auto sanitizedTarget = QString(target);
auto sanitizedTarget = QString(target); sanitizedTarget.replace("\\\"", "\"");
sanitizedTarget.replace("\\\"", "\""); sanitizedTarget.replace("\\'", "'");
sanitizedTarget.replace("\\'", "'"); return [=](const QString &s) { return s.contains(sanitizedTarget, Qt::CaseInsensitive); };
return s.contains(sanitizedTarget, Qt::CaseInsensitive); };
};
search["RegexMatcher"] = [](const peg::SemanticValues &sv) -> StringMatcher {
auto target = std::any_cast<QString>(sv[0]);
auto regex = QRegularExpression(target, QRegularExpression::CaseInsensitiveOption);
return [=](const QString &s) { return regex.match(s).hasMatch(); };
};
search["RegexMatcherString"] = [](const peg::SemanticValues &sv) -> QString {
return QString::fromStdString(sv.token_to_string());
}; };
search["OracleQuery"] = [](const peg::SemanticValues &sv) -> Filter { search["OracleQuery"] = [](const peg::SemanticValues &sv) -> Filter {

View file

@ -9,11 +9,12 @@
* *
* @return the QTextBrowser * @return the QTextBrowser
*/ */
static QTextBrowser *createBrowser() static QTextBrowser *createBrowser(const QString &helpFile)
{ {
QFile file("theme:help/search.md"); QFile file(helpFile);
if (!file.open(QFile::ReadOnly | QFile::Text)) { if (!file.open(QFile::ReadOnly | QFile::Text)) {
qCWarning(SyntaxHelpLog) << "Could not open syntax help file: " << helpFile;
return nullptr; return nullptr;
} }
@ -54,9 +55,29 @@ static QTextBrowser *createBrowser()
*/ */
QTextBrowser *createSearchSyntaxHelpWindow(QLineEdit *lineEdit) QTextBrowser *createSearchSyntaxHelpWindow(QLineEdit *lineEdit)
{ {
auto browser = createBrowser(); auto browser = createBrowser("theme:help/search.md");
QObject::connect(browser, &QTextBrowser::anchorClicked, QObject::connect(browser, &QTextBrowser::anchorClicked,
[lineEdit](const QUrl &link) { lineEdit->setText(link.fragment()); }); [lineEdit](const QUrl &link) { lineEdit->setText(link.fragment()); });
QObject::connect(lineEdit, &QObject::destroyed, browser, &QTextBrowser::close); QObject::connect(lineEdit, &QObject::destroyed, browser, &QTextBrowser::close);
return browser; return browser;
} }
/**
* Creates the deck search syntax help window and connects its anchorClicked signal to the given QLineEdit.
* The window will automatically close when the QLineEdit is destroyed.
*
* @return the QTextBrowser
*/
QTextBrowser *createDeckSearchSyntaxHelpWindow(QLineEdit *lineEdit)
{
auto browser = createBrowser("theme:help/deck_search.md");
QObject::connect(browser, &QTextBrowser::anchorClicked, [lineEdit](const QUrl &link) {
if (link.fragment() == "cardSearchSyntaxHelp") {
createSearchSyntaxHelpWindow(lineEdit);
} else {
lineEdit->setText(link.fragment());
}
});
QObject::connect(lineEdit, &QObject::destroyed, browser, &QTextBrowser::close);
return browser;
}

View file

@ -2,8 +2,13 @@
#define SEARCH_SYNTAX_HELP_H #define SEARCH_SYNTAX_HELP_H
#include <QLineEdit> #include <QLineEdit>
#include <QLoggingCategory>
#include <QTextBrowser> #include <QTextBrowser>
inline Q_LOGGING_CATEGORY(SyntaxHelpLog, "syntax_help");
QTextBrowser *createSearchSyntaxHelpWindow(QLineEdit *lineEdit); QTextBrowser *createSearchSyntaxHelpWindow(QLineEdit *lineEdit);
QTextBrowser *createDeckSearchSyntaxHelpWindow(QLineEdit *lineEdit);
#endif // SEARCH_SYNTAX_HELP_H #endif // SEARCH_SYNTAX_HELP_H

View file

@ -275,7 +275,6 @@ SettingsCache::SettingsCache()
visualDeckStorageSortingOrder = settings->value("interface/visualdeckstoragesortingorder", 0).toInt(); visualDeckStorageSortingOrder = settings->value("interface/visualdeckstoragesortingorder", 0).toInt();
visualDeckStorageShowFolders = settings->value("interface/visualdeckstorageshowfolders", true).toBool(); visualDeckStorageShowFolders = settings->value("interface/visualdeckstorageshowfolders", true).toBool();
visualDeckStorageShowTagFilter = settings->value("interface/visualdeckstorageshowtagfilter", true).toBool(); visualDeckStorageShowTagFilter = settings->value("interface/visualdeckstorageshowtagfilter", true).toBool();
visualDeckStorageSearchFolderNames = settings->value("interface/visualdeckstoragesearchfoldernames", true).toBool();
visualDeckStorageShowBannerCardComboBox = visualDeckStorageShowBannerCardComboBox =
settings->value("interface/visualdeckstorageshowbannercardcombobox", true).toBool(); settings->value("interface/visualdeckstorageshowbannercardcombobox", true).toBool();
visualDeckStorageShowTagsOnDeckPreviews = visualDeckStorageShowTagsOnDeckPreviews =
@ -284,6 +283,7 @@ SettingsCache::SettingsCache()
settings->value("interface/visualdeckstoragedrawunusedcoloridentities", true).toBool(); settings->value("interface/visualdeckstoragedrawunusedcoloridentities", true).toBool();
visualDeckStorageUnusedColorIdentitiesOpacity = visualDeckStorageUnusedColorIdentitiesOpacity =
settings->value("interface/visualdeckstorageunusedcoloridentitiesopacity", 15).toInt(); settings->value("interface/visualdeckstorageunusedcoloridentitiesopacity", 15).toInt();
visualDeckStorageTooltipType = settings->value("interface/visualdeckstoragetooltiptype", 0).toInt();
visualDeckStoragePromptForConversion = visualDeckStoragePromptForConversion =
settings->value("interface/visualdeckstoragepromptforconversion", true).toBool(); settings->value("interface/visualdeckstoragepromptforconversion", true).toBool();
visualDeckStorageAlwaysConvert = settings->value("interface/visualdeckstoragealwaysconvert", false).toBool(); visualDeckStorageAlwaysConvert = settings->value("interface/visualdeckstoragealwaysconvert", false).toBool();
@ -734,12 +734,6 @@ void SettingsCache::setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T _showTa
emit visualDeckStorageShowTagFilterChanged(visualDeckStorageShowTagFilter); emit visualDeckStorageShowTagFilterChanged(visualDeckStorageShowTagFilter);
} }
void SettingsCache::setVisualDeckStorageSearchFolderNames(QT_STATE_CHANGED_T value)
{
visualDeckStorageSearchFolderNames = value;
settings->setValue("interface/visualdeckstoragesearchfoldernames", visualDeckStorageSearchFolderNames);
}
void SettingsCache::setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T _showBannerCardComboBox) void SettingsCache::setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T _showBannerCardComboBox)
{ {
visualDeckStorageShowBannerCardComboBox = _showBannerCardComboBox; visualDeckStorageShowBannerCardComboBox = _showBannerCardComboBox;
@ -778,6 +772,12 @@ void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity(int _visual
emit visualDeckStorageUnusedColorIdentitiesOpacityChanged(visualDeckStorageUnusedColorIdentitiesOpacity); emit visualDeckStorageUnusedColorIdentitiesOpacityChanged(visualDeckStorageUnusedColorIdentitiesOpacity);
} }
void SettingsCache::setVisualDeckStorageTooltipType(int value)
{
visualDeckStorageTooltipType = value;
settings->setValue("interface/visualdeckstoragetooltiptype", visualDeckStorageTooltipType);
}
void SettingsCache::setVisualDeckStoragePromptForConversion(bool _visualDeckStoragePromptForConversion) void SettingsCache::setVisualDeckStoragePromptForConversion(bool _visualDeckStoragePromptForConversion)
{ {
visualDeckStoragePromptForConversion = _visualDeckStoragePromptForConversion; visualDeckStoragePromptForConversion = _visualDeckStoragePromptForConversion;

View file

@ -148,10 +148,10 @@ private:
bool visualDeckStorageShowBannerCardComboBox; bool visualDeckStorageShowBannerCardComboBox;
bool visualDeckStorageShowTagsOnDeckPreviews; bool visualDeckStorageShowTagsOnDeckPreviews;
bool visualDeckStorageShowTagFilter; bool visualDeckStorageShowTagFilter;
bool visualDeckStorageSearchFolderNames;
int visualDeckStorageCardSize; int visualDeckStorageCardSize;
bool visualDeckStorageDrawUnusedColorIdentities; bool visualDeckStorageDrawUnusedColorIdentities;
int visualDeckStorageUnusedColorIdentitiesOpacity; int visualDeckStorageUnusedColorIdentitiesOpacity;
int visualDeckStorageTooltipType;
bool visualDeckStoragePromptForConversion; bool visualDeckStoragePromptForConversion;
bool visualDeckStorageAlwaysConvert; bool visualDeckStorageAlwaysConvert;
bool visualDeckStorageInGame; bool visualDeckStorageInGame;
@ -454,10 +454,6 @@ public:
{ {
return visualDeckStorageShowTagFilter; return visualDeckStorageShowTagFilter;
} }
bool getVisualDeckStorageSearchFolderNames() const
{
return visualDeckStorageSearchFolderNames;
}
bool getVisualDeckStorageShowBannerCardComboBox() const bool getVisualDeckStorageShowBannerCardComboBox() const
{ {
return visualDeckStorageShowBannerCardComboBox; return visualDeckStorageShowBannerCardComboBox;
@ -478,6 +474,10 @@ public:
{ {
return visualDeckStorageUnusedColorIdentitiesOpacity; return visualDeckStorageUnusedColorIdentitiesOpacity;
} }
int getVisualDeckStorageTooltipType() const
{
return visualDeckStorageTooltipType;
}
bool getVisualDeckStoragePromptForConversion() const bool getVisualDeckStoragePromptForConversion() const
{ {
return visualDeckStoragePromptForConversion; return visualDeckStoragePromptForConversion;
@ -850,12 +850,12 @@ public slots:
void setVisualDeckStorageSortingOrder(int _visualDeckStorageSortingOrder); void setVisualDeckStorageSortingOrder(int _visualDeckStorageSortingOrder);
void setVisualDeckStorageShowFolders(QT_STATE_CHANGED_T value); void setVisualDeckStorageShowFolders(QT_STATE_CHANGED_T value);
void setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T _showTags); void setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T _showTags);
void setVisualDeckStorageSearchFolderNames(QT_STATE_CHANGED_T value);
void setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T _showBannerCardComboBox); void setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T _showBannerCardComboBox);
void setVisualDeckStorageShowTagsOnDeckPreviews(QT_STATE_CHANGED_T _showTags); void setVisualDeckStorageShowTagsOnDeckPreviews(QT_STATE_CHANGED_T _showTags);
void setVisualDeckStorageCardSize(int _visualDeckStorageCardSize); void setVisualDeckStorageCardSize(int _visualDeckStorageCardSize);
void setVisualDeckStorageDrawUnusedColorIdentities(QT_STATE_CHANGED_T _visualDeckStorageDrawUnusedColorIdentities); void setVisualDeckStorageDrawUnusedColorIdentities(QT_STATE_CHANGED_T _visualDeckStorageDrawUnusedColorIdentities);
void setVisualDeckStorageUnusedColorIdentitiesOpacity(int _visualDeckStorageUnusedColorIdentitiesOpacity); void setVisualDeckStorageUnusedColorIdentitiesOpacity(int _visualDeckStorageUnusedColorIdentitiesOpacity);
void setVisualDeckStorageTooltipType(int value);
void setVisualDeckStoragePromptForConversion(bool _visualDeckStoragePromptForConversion); void setVisualDeckStoragePromptForConversion(bool _visualDeckStoragePromptForConversion);
void setVisualDeckStorageAlwaysConvert(bool _visualDeckStorageAlwaysConvert); void setVisualDeckStorageAlwaysConvert(bool _visualDeckStorageAlwaysConvert);
void setVisualDeckStorageInGame(QT_STATE_CHANGED_T value); void setVisualDeckStorageInGame(QT_STATE_CHANGED_T value);

View file

@ -267,6 +267,13 @@ private:
{"DeckViewContainer/loadRemoteButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Remote Deck..."), {"DeckViewContainer/loadRemoteButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Remote Deck..."),
parseSequenceString("Ctrl+Alt+O"), parseSequenceString("Ctrl+Alt+O"),
ShortcutGroup::Game_Lobby)}, ShortcutGroup::Game_Lobby)},
{"DeckViewContainer/loadFromClipboardButton",
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Deck from Clipboard..."),
parseSequenceString("Ctrl+Shift+V"),
ShortcutGroup::Game_Lobby)},
{"DeckViewContainer/unloadDeckButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Unload Deck"),
parseSequenceString("Ctrl+Alt+U"),
ShortcutGroup::Game_Lobby)},
{"DeckViewContainer/readyStartButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Set Ready to Start"), {"DeckViewContainer/readyStartButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Set Ready to Start"),
parseSequenceString("Ctrl+Shift+S"), parseSequenceString("Ctrl+Shift+S"),
ShortcutGroup::Game_Lobby)}, ShortcutGroup::Game_Lobby)},
@ -274,6 +281,9 @@ private:
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Toggle Sideboard Lock"), ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Toggle Sideboard Lock"),
parseSequenceString("Ctrl+Shift+B"), parseSequenceString("Ctrl+Shift+B"),
ShortcutGroup::Game_Lobby)}, ShortcutGroup::Game_Lobby)},
{"DeckViewContainer/forceStartGameButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Force Start"),
parseSequenceString(""),
ShortcutGroup::Game_Lobby)},
{"Player/aCCMagenta", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Add Card Counter (F)"), {"Player/aCCMagenta", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Add Card Counter (F)"),
parseSequenceString(""), parseSequenceString(""),
ShortcutGroup::Card_Counters)}, ShortcutGroup::Card_Counters)},

View file

@ -441,7 +441,7 @@ bool DeckList::readElement(QXmlStreamReader *xml)
return true; return true;
} }
void DeckList::write(QXmlStreamWriter *xml) void DeckList::write(QXmlStreamWriter *xml) const
{ {
xml->writeStartElement("cockatrice_deck"); xml->writeStartElement("cockatrice_deck");
xml->writeAttribute("version", "1"); xml->writeAttribute("version", "1");
@ -508,7 +508,7 @@ bool DeckList::loadFromString_Native(const QString &nativeString)
return loadFromXml(&xml); return loadFromXml(&xml);
} }
QString DeckList::writeToString_Native() QString DeckList::writeToString_Native() const
{ {
QString result; QString result;
QXmlStreamWriter xml(&result); QXmlStreamWriter xml(&result);

View file

@ -345,10 +345,10 @@ public:
} }
bool readElement(QXmlStreamReader *xml); bool readElement(QXmlStreamReader *xml);
void write(QXmlStreamWriter *xml); void write(QXmlStreamWriter *xml) const;
bool loadFromXml(QXmlStreamReader *xml); bool loadFromXml(QXmlStreamReader *xml);
bool loadFromString_Native(const QString &nativeString); bool loadFromString_Native(const QString &nativeString);
QString writeToString_Native(); QString writeToString_Native() const;
bool loadFromFile_Native(QIODevice *device); bool loadFromFile_Native(QIODevice *device);
bool saveToFile_Native(QIODevice *device); bool saveToFile_Native(QIODevice *device);
bool loadFromStream_Plain(QTextStream &stream, bool preserveMetadata); bool loadFromStream_Plain(QTextStream &stream, bool preserveMetadata);

View file

@ -920,6 +920,8 @@ Server_Player::cmdConcede(const Command_Concede & /*cmd*/, ResponseContainer & /
game->removeArrowsRelatedToPlayer(ges, this); game->removeArrowsRelatedToPlayer(ges, this);
game->unattachCards(ges, this); game->unattachCards(ges, this);
playerMutex.lock();
// Return cards to their rightful owners before conceding the game // Return cards to their rightful owners before conceding the game
static const QRegularExpression ownerRegex{"Owner: ?([^\n]+)"}; static const QRegularExpression ownerRegex{"Owner: ?([^\n]+)"};
for (const auto &card : zones.value("table")->getCards()) { for (const auto &card : zones.value("table")->getCards()) {
@ -959,6 +961,8 @@ Server_Player::cmdConcede(const Command_Concede & /*cmd*/, ResponseContainer & /
} }
} }
playerMutex.unlock();
// All borrowed cards have been returned, can now continue cleanup process // All borrowed cards have been returned, can now continue cleanup process
clearZones(); clearZones();

View file

@ -227,9 +227,6 @@ void SettingsCache::setVisualDeckStorageShowFolders(QT_STATE_CHANGED_T /* value
void SettingsCache::setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T /* _showTags */) void SettingsCache::setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T /* _showTags */)
{ {
} }
void SettingsCache::setVisualDeckStorageSearchFolderNames(QT_STATE_CHANGED_T /* value */)
{
}
void SettingsCache::setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T /* _showBannerCardComboBox */) void SettingsCache::setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T /* _showBannerCardComboBox */)
{ {
} }
@ -247,6 +244,9 @@ void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity(
int /* _visualDeckStorageUnusedColorIdentitiesOpacity */) int /* _visualDeckStorageUnusedColorIdentitiesOpacity */)
{ {
} }
void SettingsCache::setVisualDeckStorageTooltipType(int /* value */)
{
}
void SettingsCache::setVisualDeckStoragePromptForConversion(bool /* _visualDeckStoragePromptForConversion */) void SettingsCache::setVisualDeckStoragePromptForConversion(bool /* _visualDeckStoragePromptForConversion */)
{ {
} }

View file

@ -56,6 +56,7 @@
<xs:element type="xs:integer" name="tablerow" minOccurs="0" maxOccurs="1" /> <xs:element type="xs:integer" name="tablerow" minOccurs="0" maxOccurs="1" />
<xs:element type="xs:boolean" name="cipt" minOccurs="0" maxOccurs="1" /> <xs:element type="xs:boolean" name="cipt" minOccurs="0" maxOccurs="1" />
<xs:element type="xs:boolean" name="upsidedown" minOccurs="0" maxOccurs="1" /> <xs:element type="xs:boolean" name="upsidedown" minOccurs="0" maxOccurs="1" />
<xs:element type="xs:boolean" name="landscapeOrientation" minOccurs="0" maxOccurs="1" />
</xs:sequence> </xs:sequence>
</xs:complexType> </xs:complexType>
<xs:element name="cockatrice_carddatabase"> <xs:element name="cockatrice_carddatabase">

View file

@ -21,8 +21,13 @@ set(oracle_SOURCES
../cockatrice/src/game/cards/card_database_manager.cpp ../cockatrice/src/game/cards/card_database_manager.cpp
../cockatrice/src/game/cards/card_info.cpp ../cockatrice/src/game/cards/card_info.cpp
../cockatrice/src/client/ui/picture_loader/picture_loader.cpp ../cockatrice/src/client/ui/picture_loader/picture_loader.cpp
../cockatrice/src/client/ui/picture_loader/picture_loader_request_status_display_widget.cpp
../cockatrice/src/client/ui/picture_loader/picture_loader_status_bar.cpp
../cockatrice/src/client/ui/picture_loader/picture_loader_worker.cpp ../cockatrice/src/client/ui/picture_loader/picture_loader_worker.cpp
../cockatrice/src/client/ui/picture_loader/picture_loader_worker_work.cpp
../cockatrice/src/client/ui/picture_loader/picture_to_load.cpp ../cockatrice/src/client/ui/picture_loader/picture_to_load.cpp
../cockatrice/src/client/ui/widgets/quick_settings/settings_button_widget.cpp
../cockatrice/src/client/ui/widgets/quick_settings/settings_popup_widget.cpp
../cockatrice/src/game/cards/card_database_parser/card_database_parser.cpp ../cockatrice/src/game/cards/card_database_parser/card_database_parser.cpp
../cockatrice/src/game/cards/card_database_parser/cockatrice_xml_3.cpp ../cockatrice/src/game/cards/card_database_parser/cockatrice_xml_3.cpp
../cockatrice/src/game/cards/card_database_parser/cockatrice_xml_4.cpp ../cockatrice/src/game/cards/card_database_parser/cockatrice_xml_4.cpp

View file

@ -231,9 +231,6 @@ void SettingsCache::setVisualDeckStorageShowFolders(QT_STATE_CHANGED_T /* value
void SettingsCache::setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T /* _showTags */) void SettingsCache::setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T /* _showTags */)
{ {
} }
void SettingsCache::setVisualDeckStorageSearchFolderNames(QT_STATE_CHANGED_T /* value */)
{
}
void SettingsCache::setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T /* _showBannerCardComboBox */) void SettingsCache::setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T /* _showBannerCardComboBox */)
{ {
} }
@ -251,6 +248,9 @@ void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity(
int /* _visualDeckStorageUnusedColorIdentitiesOpacity */) int /* _visualDeckStorageUnusedColorIdentitiesOpacity */)
{ {
} }
void SettingsCache::setVisualDeckStorageTooltipType(int /* value */)
{
}
void SettingsCache::setVisualDeckStoragePromptForConversion(bool /* _visualDeckStoragePromptForConversion */) void SettingsCache::setVisualDeckStoragePromptForConversion(bool /* _visualDeckStoragePromptForConversion */)
{ {
} }

File diff suppressed because it is too large Load diff