mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-07-14 14:32:15 -07:00
Merge branch 'master' into cccolors
This commit is contained in:
commit
f022ee793c
78 changed files with 2299 additions and 1203 deletions
|
|
@ -7,6 +7,7 @@ RUN pacman --sync --refresh --sysupgrade --needed --noconfirm \
|
|||
git \
|
||||
gtest \
|
||||
mariadb-libs \
|
||||
ninja \
|
||||
protobuf \
|
||||
qt6-base \
|
||||
qt6-imageformats \
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ RUN apt-get update && \
|
|||
libqt5sql5-mysql \
|
||||
libqt5svg5-dev \
|
||||
libqt5websockets5-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
qt5-image-formats-plugins \
|
||||
qtmultimedia5-dev \
|
||||
|
|
|
|||
|
|
@ -15,13 +15,14 @@ RUN apt-get update && \
|
|||
libprotobuf-dev \
|
||||
libqt6multimedia6 \
|
||||
libqt6sql6-mysql \
|
||||
qt6-svg-dev \
|
||||
qt6-websockets-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
qt6-image-formats-plugins \
|
||||
qt6-l10n-tools \
|
||||
qt6-multimedia-dev \
|
||||
qt6-svg-dev \
|
||||
qt6-tools-dev \
|
||||
qt6-tools-dev-tools \
|
||||
qt6-websockets-dev \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ RUN dnf install -y \
|
|||
gcc-c++ \
|
||||
git \
|
||||
mariadb-devel \
|
||||
ninja-build \
|
||||
protobuf-devel \
|
||||
qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||
qt6-qtimageformats \
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ RUN dnf install -y \
|
|||
gcc-c++ \
|
||||
git \
|
||||
mariadb-devel \
|
||||
ninja-build \
|
||||
protobuf-devel \
|
||||
qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||
qt6-qtimageformats \
|
||||
|
|
|
|||
|
|
@ -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/*
|
||||
|
|
@ -17,6 +17,7 @@ RUN apt-get update && \
|
|||
libqt6sql6-mysql \
|
||||
libqt6svg6-dev \
|
||||
libqt6websockets6-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
qt6-image-formats-plugins \
|
||||
qt6-l10n-tools \
|
||||
|
|
|
|||
|
|
@ -15,13 +15,14 @@ RUN apt-get update && \
|
|||
libprotobuf-dev \
|
||||
libqt6multimedia6 \
|
||||
libqt6sql6-mysql \
|
||||
qt6-svg-dev \
|
||||
qt6-websockets-dev \
|
||||
ninja-build \
|
||||
protobuf-compiler \
|
||||
qt6-image-formats-plugins \
|
||||
qt6-l10n-tools \
|
||||
qt6-multimedia-dev \
|
||||
qt6-svg-dev \
|
||||
qt6-tools-dev \
|
||||
qt6-tools-dev-tools \
|
||||
qt6-websockets-dev \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
|
|
|||
|
|
@ -11,9 +11,8 @@
|
|||
# --debug or --release sets the build type ie CMAKE_BUILD_TYPE
|
||||
# --ccache [<size>] uses ccache and shows stats, optionally provide size
|
||||
# --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 PARALLEL_COUNT
|
||||
# (correspond to args: --debug/--release --install --package <package type> --suffix <suffix> --server --test --ccache <ccache_size> --dir <dir> --parallel <core_count>)
|
||||
# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_TEST USE_CCACHE CCACHE_SIZE BUILD_DIR CMAKE_GENERATOR
|
||||
# (correspond to args: --debug/--release --install --package <package type> --suffix <suffix> --server --test --ccache <ccache_size> --dir <dir>)
|
||||
# exitcode: 1 for failure, 3 for invalid arguments
|
||||
|
||||
# Read arguments
|
||||
|
|
@ -76,15 +75,6 @@ while [[ $# != 0 ]]; do
|
|||
BUILD_DIR="$1"
|
||||
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"
|
||||
exit 3
|
||||
|
|
@ -126,16 +116,6 @@ fi
|
|||
|
||||
# Add cmake --build flags
|
||||
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() {
|
||||
# note, verbose only works on newer ccache, discard the error
|
||||
|
|
|
|||
|
|
@ -148,6 +148,9 @@ function RUN ()
|
|||
args+=(--mount "type=bind,source=$CCACHE_DIR,target=/.ccache")
|
||||
args+=(--env "CCACHE_DIR=/.ccache")
|
||||
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 "$@"
|
||||
return $?
|
||||
else
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ Available pre-compiled binaries for installation:
|
|||
<b>Linux</b>
|
||||
• <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 20.04 LTS</kbd> <sub><i>Focal Fossa</i></sub>
|
||||
• <kbd>Debian 12</kbd> <sub><i>Bookworm</i></sub>
|
||||
• <kbd>Debian 11</kbd> <sub><i>Bullseye</i></sub>
|
||||
• <kbd>Fedora 42</kbd>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
*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
|
||||
|
|
|
|||
18
.github/ISSUE_TEMPLATE/bug_report.md
vendored
18
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -9,14 +9,24 @@ assignees: ''
|
|||
---
|
||||
|
||||
<!-- READ THIS BEFORE POSTING
|
||||
Go to "Help → View Debug Log" in Cockatrice and copy all information at the
|
||||
top (above the separation line) below "System Information" in this ticket!
|
||||
In Cockatrice, go to "Help" → "View Debug Log" and copy all information displayed at the
|
||||
top (above the separation line "----"), to below "System Information" section in this ticket!
|
||||
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:**
|
||||
<!-- 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. -->
|
||||
|
|
@ -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:**
|
||||
- Do A
|
||||
|
|
|
|||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -4,6 +4,6 @@ contact_links:
|
|||
url: https://discord.gg/3Z9yzmA
|
||||
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)
|
||||
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
|
||||
about: For more information and guidance check our Translation FAQ on our wiki!
|
||||
|
|
|
|||
147
.github/workflows/desktop-build.yml
vendored
147
.github/workflows/desktop-build.yml
vendored
|
|
@ -110,11 +110,6 @@ jobs:
|
|||
version: 42
|
||||
package: RPM
|
||||
|
||||
- distro: Ubuntu
|
||||
version: 20.04
|
||||
package: DEB
|
||||
test: skip # Ubuntu 20.04 has a broken Qt for debug builds
|
||||
|
||||
- distro: Ubuntu
|
||||
version: 22.04
|
||||
package: DEB
|
||||
|
|
@ -130,29 +125,25 @@ jobs:
|
|||
continue-on-error: ${{matrix.allow-failure == 'yes'}}
|
||||
env:
|
||||
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:
|
||||
# https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
||||
CCACHE_SIZE: 500M
|
||||
CMAKE_GENERATOR: 'Ninja'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Generate cache timestamp
|
||||
id: cache_timestamp
|
||||
shell: bash
|
||||
run: echo "timestamp=$(date -u '+%Y%m%d%H%M%S')" >>"$GITHUB_OUTPUT"
|
||||
|
||||
- name: Restore cache
|
||||
uses: actions/cache@v4
|
||||
- name: Restore compiler cache (ccache)
|
||||
id: ccache_restore
|
||||
uses: actions/cache/restore@v4
|
||||
env:
|
||||
timestamp: ${{steps.cache_timestamp.outputs.timestamp}}
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
with:
|
||||
path: ${{env.CACHE}}
|
||||
key: docker-${{matrix.distro}}${{matrix.version}}-cache-${{env.timestamp}}
|
||||
restore-keys: |
|
||||
docker-${{matrix.distro}}${{matrix.version}}-cache-
|
||||
key: ccache-${{matrix.distro}}${{matrix.version}}-${{env.BRANCH_NAME}}
|
||||
restore-keys: ccache-${{matrix.distro}}${{matrix.version}}-
|
||||
|
||||
- name: Build ${{matrix.distro}} ${{matrix.version}} Docker image
|
||||
shell: bash
|
||||
|
|
@ -161,9 +152,11 @@ jobs:
|
|||
- name: Build debug and test
|
||||
if: matrix.test != 'skip'
|
||||
shell: bash
|
||||
env:
|
||||
CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}'
|
||||
run: |
|
||||
source .ci/docker.sh
|
||||
RUN --server --debug --test --ccache "$CCACHE_SIZE" --parallel 4
|
||||
RUN --server --debug --test --ccache "$CCACHE_SIZE"
|
||||
|
||||
- name: Build release package
|
||||
id: build
|
||||
|
|
@ -172,14 +165,23 @@ jobs:
|
|||
env:
|
||||
BUILD_DIR: build
|
||||
SUFFIX: '-${{matrix.distro}}${{matrix.version}}'
|
||||
type: '${{matrix.package}}'
|
||||
package: '${{matrix.package}}'
|
||||
CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}'
|
||||
run: |
|
||||
source .ci/docker.sh
|
||||
RUN --server --release --package "$type" --dir "$BUILD_DIR" \
|
||||
--ccache "$CCACHE_SIZE" --parallel 4
|
||||
RUN --server --release --package "$package" --dir "$BUILD_DIR" \
|
||||
--ccache "$CCACHE_SIZE"
|
||||
.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
|
||||
id: upload_artifact
|
||||
if: matrix.package != 'skip'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
|
@ -188,6 +190,7 @@ jobs:
|
|||
if-no-files-found: error
|
||||
|
||||
- name: Upload to release
|
||||
id: upload_release
|
||||
if: matrix.package != 'skip' && needs.configure.outputs.tag != null
|
||||
shell: bash
|
||||
env:
|
||||
|
|
@ -197,6 +200,20 @@ jobs:
|
|||
asset_name: ${{steps.build.outputs.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:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
@ -207,7 +224,6 @@ jobs:
|
|||
os: macos-13
|
||||
xcode: "14.3.1"
|
||||
type: Release
|
||||
core_count: 4
|
||||
make_package: 1
|
||||
|
||||
- target: 14
|
||||
|
|
@ -215,7 +231,6 @@ jobs:
|
|||
os: macos-14
|
||||
xcode: "15.4"
|
||||
type: Release
|
||||
core_count: 3
|
||||
make_package: 1
|
||||
|
||||
- target: 15
|
||||
|
|
@ -223,7 +238,6 @@ jobs:
|
|||
os: macos-15
|
||||
xcode: "16.2"
|
||||
type: Release
|
||||
core_count: 3
|
||||
make_package: 1
|
||||
|
||||
- target: 15
|
||||
|
|
@ -231,15 +245,17 @@ jobs:
|
|||
os: macos-15
|
||||
xcode: "16.2"
|
||||
type: Debug
|
||||
core_count: 3
|
||||
|
||||
name: macOS ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
|
||||
needs: configure
|
||||
runs-on: ${{matrix.os}}
|
||||
continue-on-error: ${{matrix.allow-failure == 'yes'}}
|
||||
env:
|
||||
CCACHE_DIR: ${{github.workspace}}/.ccache/${{matrix.os}}-${{matrix.type}}
|
||||
CCACHE_SIZE: 500M
|
||||
DEVELOPER_DIR:
|
||||
/Applications/Xcode_${{matrix.xcode}}.app/Contents/Developer
|
||||
CMAKE_GENERATOR: 'Ninja'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
|
@ -253,23 +269,30 @@ jobs:
|
|||
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
|
||||
run: |
|
||||
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}}
|
||||
shell: bash
|
||||
id: build
|
||||
env:
|
||||
BUILDTYPE: '${{matrix.type}}'
|
||||
MAKE_TEST: 1
|
||||
MAKE_PACKAGE: '${{matrix.make_package}}'
|
||||
PACKAGE_SUFFIX: '-macOS${{matrix.target}}_${{matrix.soc}}'
|
||||
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
|
||||
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
|
||||
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
|
||||
# macOS runner have 3 cores usually - only the macos-13 image has 4:
|
||||
# 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
|
||||
CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}'
|
||||
run: |
|
||||
if [[ -n "$MACOS_CERTIFICATE_NAME" ]]
|
||||
then
|
||||
|
|
@ -281,10 +304,17 @@ jobs:
|
|||
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
|
||||
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
|
||||
if: matrix.make_package
|
||||
if: matrix.make_package && (github.ref == 'refs/heads/master' || needs.configure.outputs.tag != null)
|
||||
env:
|
||||
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
|
||||
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
|
||||
|
|
@ -296,7 +326,7 @@ jobs:
|
|||
fi
|
||||
|
||||
- name: Notarize app bundle
|
||||
if: matrix.make_package
|
||||
if: matrix.make_package && (github.ref == 'refs/heads/master' || needs.configure.outputs.tag != null)
|
||||
env:
|
||||
MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
|
||||
MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
|
||||
|
|
@ -328,6 +358,7 @@ jobs:
|
|||
fi
|
||||
|
||||
- name: Upload artifact
|
||||
id: upload_artifact
|
||||
if: matrix.make_package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
|
@ -336,7 +367,8 @@ jobs:
|
|||
if-no-files-found: error
|
||||
|
||||
- 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
|
||||
env:
|
||||
GH_TOKEN: ${{github.token}}
|
||||
|
|
@ -345,6 +377,20 @@ jobs:
|
|||
asset_name: ${{steps.build.outputs.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:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
@ -387,16 +433,11 @@ jobs:
|
|||
tools: ${{matrix.qt_tools}}
|
||||
modules: ${{matrix.qt_modules}}
|
||||
|
||||
# TODO: re-enable when https://github.com/lukka/run-vcpkg/issues/243 is fixed
|
||||
- if: false
|
||||
name: Run vcpkg (disabled)
|
||||
uses: lukka/run-vcpkg@v11
|
||||
- name: Setup vcpkg cache
|
||||
id: vcpkg-cache
|
||||
uses: TAServers/vcpkg-cache@v3
|
||||
with:
|
||||
runVcpkgInstall: true
|
||||
doNotCache: false
|
||||
env:
|
||||
VCPKG_DEFAULT_TRIPLET: 'x64-windows'
|
||||
VCPKG_DISABLE_METRICS: 1
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Cockatrice
|
||||
id: build
|
||||
|
|
@ -406,11 +447,14 @@ jobs:
|
|||
CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}'
|
||||
CMAKE_GENERATOR_PLATFORM: 'x64'
|
||||
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,
|
||||
# project and process parallelism: https://devblogs.microsoft.com/cppblog/improved-parallelism-in-msbuild/
|
||||
run: .ci/compile.sh --server --release --test --package
|
||||
|
||||
- name: Upload artifact
|
||||
id: upload_artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Windows${{matrix.target}}-installer
|
||||
|
|
@ -427,7 +471,8 @@ jobs:
|
|||
if-no-files-found: error
|
||||
|
||||
- name: Upload to release
|
||||
if: matrix.package != 'skip' && needs.configure.outputs.tag != null
|
||||
id: upload_release
|
||||
if: needs.configure.outputs.tag != null
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{github.token}}
|
||||
|
|
@ -435,3 +480,17 @@ jobs:
|
|||
asset_path: ${{steps.build.outputs.path}}
|
||||
asset_name: ${{steps.build.outputs.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
|
||||
|
|
|
|||
2
.github/workflows/translations-pull.yml
vendored
2
.github/workflows/translations-pull.yml
vendored
|
|
@ -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>
|
||||
*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
|
||||
labels: |
|
||||
CI
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</details>
|
||||
|
||||
### Translations [](https://transifex.com/cockatrice/cockatrice/)
|
||||
### Translations [](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>
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,10 @@ set(cockatrice_SOURCES
|
|||
src/client/ui/line_edit_completer.cpp
|
||||
src/client/ui/phases_toolbar.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_work.cpp
|
||||
src/client/ui/picture_loader/picture_to_load.cpp
|
||||
src/client/ui/pixel_map_generator.cpp
|
||||
src/client/ui/theme_manager.cpp
|
||||
|
|
@ -190,6 +193,7 @@ set(cockatrice_SOURCES
|
|||
src/game/cards/card_search_model.cpp
|
||||
src/game/deckview/deck_view.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_card.cpp
|
||||
src/game/filters/filter_string.cpp
|
||||
|
|
|
|||
|
|
@ -379,5 +379,6 @@
|
|||
<file>resources/tips/tips_of_the_day.xml</file>
|
||||
|
||||
<file>resources/help/search.md</file>
|
||||
<file>resources/help/deck_search.md</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
|
|||
|
|
@ -60,4 +60,6 @@
|
|||
|
||||
# pixel_map_generator = false
|
||||
|
||||
# deck_filter_string = false
|
||||
# filter_string = false
|
||||
# syntax_help = false
|
||||
|
|
|
|||
40
cockatrice/resources/help/deck_search.md
Normal file
40
cockatrice/resources/help/deck_search.md
Normal 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>
|
||||
|
|
@ -58,4 +58,9 @@ In this list of examples below, each entry has an explanation and can be clicked
|
|||
<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>
|
||||
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -1538,7 +1538,7 @@ void TabGame::createPlayAreaWidget(bool bReplay)
|
|||
scene = new GameScene(phasesToolbar, this);
|
||||
gameView = new GameView(scene);
|
||||
|
||||
gamePlayAreaVBox = new QVBoxLayout;
|
||||
auto gamePlayAreaVBox = new QVBoxLayout;
|
||||
gamePlayAreaVBox->setContentsMargins(0, 0, 0, 0);
|
||||
gamePlayAreaVBox->addWidget(gameView);
|
||||
|
||||
|
|
@ -1594,12 +1594,12 @@ void TabGame::createReplayDock()
|
|||
connect(replayFastForwardButton, &QToolButton::toggled, this, &TabGame::replayFastForwardButtonToggled);
|
||||
|
||||
// putting everything together
|
||||
replayControlLayout = new QHBoxLayout;
|
||||
auto replayControlLayout = new QHBoxLayout;
|
||||
replayControlLayout->addWidget(timelineWidget, 10);
|
||||
replayControlLayout->addWidget(replayPlayButton);
|
||||
replayControlLayout->addWidget(replayFastForwardButton);
|
||||
|
||||
replayControlWidget = new QWidget();
|
||||
auto replayControlWidget = new QWidget();
|
||||
replayControlWidget->setObjectName("replayControlWidget");
|
||||
replayControlWidget->setLayout(replayControlLayout);
|
||||
|
||||
|
|
@ -1635,13 +1635,13 @@ void TabGame::createCardInfoDock(bool bReplay)
|
|||
Q_UNUSED(bReplay);
|
||||
|
||||
cardInfoFrameWidget = new CardInfoFrameWidget();
|
||||
cardHInfoLayout = new QHBoxLayout;
|
||||
cardVInfoLayout = new QVBoxLayout;
|
||||
auto cardHInfoLayout = new QHBoxLayout;
|
||||
auto cardVInfoLayout = new QVBoxLayout;
|
||||
cardVInfoLayout->setContentsMargins(0, 0, 0, 0);
|
||||
cardVInfoLayout->addWidget(cardInfoFrameWidget);
|
||||
cardVInfoLayout->addLayout(cardHInfoLayout);
|
||||
|
||||
cardBoxLayoutWidget = new QWidget;
|
||||
auto cardBoxLayoutWidget = new QWidget;
|
||||
cardBoxLayoutWidget->setLayout(cardVInfoLayout);
|
||||
|
||||
cardInfoDock = new QDockWidget(this);
|
||||
|
|
@ -1679,6 +1679,22 @@ void TabGame::createPlayerListDock(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);
|
||||
connect(messageLog, &MessageLogWidget::cardNameHovered, cardInfoFrameWidget,
|
||||
qOverload<const QString &>(&CardInfoFrameWidget::setCard));
|
||||
|
|
@ -1690,14 +1706,12 @@ void TabGame::createMessageDock(bool bReplay)
|
|||
connect(messageLog, &MessageLogWidget::addMentionTag, this, &TabGame::addMentionTag);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::chatMentionCompleterChanged, this,
|
||||
&TabGame::actCompleterChanged);
|
||||
}
|
||||
|
||||
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(messageLog);
|
||||
|
||||
// chat entry
|
||||
if (!bReplay) {
|
||||
sayLabel = new QLabel;
|
||||
sayEdit = new LineEditCompleter;
|
||||
sayEdit->setMaxLength(MAX_TEXT_LENGTH);
|
||||
|
|
@ -1726,20 +1740,15 @@ void TabGame::createMessageDock(bool bReplay)
|
|||
connect(tabSupervisor, &TabSupervisor::adminLockChanged, this, &TabGame::adminLockChanged);
|
||||
connect(sayEdit, &LineEditCompleter::returnPressed, this, &TabGame::actSay);
|
||||
|
||||
sayHLayout = new QHBoxLayout;
|
||||
auto sayHLayout = new QHBoxLayout;
|
||||
sayHLayout->addWidget(sayLabel);
|
||||
sayHLayout->addWidget(sayEdit);
|
||||
|
||||
messageLogLayout->addLayout(sayHLayout);
|
||||
}
|
||||
|
||||
messageLogLayout = new QVBoxLayout;
|
||||
messageLogLayout->setContentsMargins(0, 0, 0, 0);
|
||||
if (!bReplay)
|
||||
messageLogLayout->addWidget(timeElapsedLabel);
|
||||
messageLogLayout->addWidget(messageLog);
|
||||
if (!bReplay)
|
||||
messageLogLayout->addLayout(sayHLayout);
|
||||
|
||||
messageLogLayoutWidget = new QWidget;
|
||||
// dock
|
||||
auto messageLogLayoutWidget = new QWidget;
|
||||
messageLogLayoutWidget->setLayout(messageLogLayout);
|
||||
|
||||
messageLayoutDock = new QDockWidget(this);
|
||||
|
|
|
|||
|
|
@ -111,10 +111,8 @@ private:
|
|||
GameScene *scene;
|
||||
GameView *gameView;
|
||||
QMap<int, DeckViewContainer *> deckViewContainers;
|
||||
QVBoxLayout *cardVInfoLayout, *messageLogLayout, *gamePlayAreaVBox, *deckViewContainerLayout;
|
||||
QHBoxLayout *cardHInfoLayout, *sayHLayout, *mainHLayout, *replayControlLayout;
|
||||
QWidget *cardBoxLayoutWidget, *messageLogLayoutWidget, *gamePlayAreaWidget, *deckViewContainerWidget,
|
||||
*replayControlWidget;
|
||||
QVBoxLayout *deckViewContainerLayout;
|
||||
QWidget *gamePlayAreaWidget, *deckViewContainerWidget;
|
||||
QDockWidget *cardInfoDock, *messageLayoutDock, *playerListDock, *replayDock;
|
||||
QAction *playersSeparator;
|
||||
QMenu *gameMenu, *viewMenu, *cardInfoDockMenu, *messageLayoutDockMenu, *playerListDockMenu, *replayDockMenu;
|
||||
|
|
|
|||
|
|
@ -7,12 +7,14 @@
|
|||
#include <QDebug>
|
||||
#include <QDirIterator>
|
||||
#include <QFileInfo>
|
||||
#include <QMainWindow>
|
||||
#include <QMovie>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QNetworkRequest>
|
||||
#include <QPainter>
|
||||
#include <QPixmapCache>
|
||||
#include <QScreen>
|
||||
#include <QStatusBar>
|
||||
#include <QThread>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
|
@ -27,6 +29,16 @@ PictureLoader::PictureLoader() : QObject(nullptr)
|
|||
connect(&SettingsCache::instance(), &SettingsCache::picDownloadChanged, this, &PictureLoader::picDownloadChanged);
|
||||
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#define PICTURELOADER_H
|
||||
|
||||
#include "../../../game/cards/card_info.h"
|
||||
#include "picture_loader_status_bar.h"
|
||||
#include "picture_loader_worker.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
|
|
@ -27,6 +28,7 @@ private:
|
|||
void operator=(PictureLoader const &);
|
||||
|
||||
PictureLoaderWorker *worker;
|
||||
PictureLoaderStatusBar *statusBar;
|
||||
|
||||
public:
|
||||
static void getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -2,13 +2,15 @@
|
|||
|
||||
#include "../../../game/cards/card_database_manager.h"
|
||||
#include "../../../settings/cache_settings.h"
|
||||
#include "picture_loader_worker_work.h"
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QDirIterator>
|
||||
#include <QJsonDocument>
|
||||
#include <QMovie>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QNetworkReply>
|
||||
#include <QThread>
|
||||
#include <utility>
|
||||
|
||||
// Card back returned by gatherer when card is not found
|
||||
QStringList PictureLoaderWorker::md5Blacklist = QStringList() << "db0c48db407a907c16ade38de048a441";
|
||||
|
|
@ -19,11 +21,8 @@ PictureLoaderWorker::PictureLoaderWorker()
|
|||
picDownload(SettingsCache::instance().getPicDownload()), downloadRunning(false), loadQueueRunning(false),
|
||||
overrideAllCardArtWithPersonalPreference(SettingsCache::instance().getOverrideAllCardArtWithPersonalPreference())
|
||||
{
|
||||
connect(this, &PictureLoaderWorker::startLoadQueue, this, &PictureLoaderWorker::processLoadQueue,
|
||||
Qt::QueuedConnection);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::picsPathChanged, this, &PictureLoaderWorker::picsPathChanged);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::picDownloadChanged, this,
|
||||
&PictureLoaderWorker::picDownloadChanged);
|
||||
connect(&SettingsCache::instance(), SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged()));
|
||||
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
|
||||
connect(&SettingsCache::instance(), &SettingsCache::overrideAllCardArtWithPersonalPreferenceChanged, this,
|
||||
&PictureLoaderWorker::setOverrideAllCardArtWithPersonalPreference);
|
||||
|
||||
|
|
@ -34,18 +33,17 @@ PictureLoaderWorker::PictureLoaderWorker()
|
|||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
|
||||
networkManager->setTransferTimeout();
|
||||
#endif
|
||||
auto cache = new QNetworkDiskCache(this);
|
||||
cache = new QNetworkDiskCache(this);
|
||||
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
|
||||
cache->setMaximumCacheSize(1024L * 1024L *
|
||||
static_cast<qint64>(SettingsCache::instance().getNetworkCacheSizeInMB()));
|
||||
// Note: the settings is in MB, but QNetworkDiskCache uses bytes
|
||||
connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, cache,
|
||||
[cache](int newSizeInMB) { cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB)); });
|
||||
connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, this,
|
||||
[this](int newSizeInMB) { cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB)); });
|
||||
networkManager->setCache(cache);
|
||||
// Use a ManualRedirectPolicy since we keep track of redirects in picDownloadFinished
|
||||
// We can't use NoLessSafeRedirectPolicy because it is not applied with AlwaysCache
|
||||
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
|
||||
connect(networkManager, &QNetworkAccessManager::finished, this, &PictureLoaderWorker::picDownloadFinished);
|
||||
|
||||
cacheFilePath = SettingsCache::instance().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
|
||||
loadRedirectCache();
|
||||
|
|
@ -57,244 +55,104 @@ PictureLoaderWorker::PictureLoaderWorker()
|
|||
pictureLoaderThread = new QThread;
|
||||
pictureLoaderThread->start(QThread::LowPriority);
|
||||
moveToThread(pictureLoaderThread);
|
||||
|
||||
connect(&requestTimer, &QTimer::timeout, this, &PictureLoaderWorker::processQueuedRequests);
|
||||
requestTimer.setInterval(1000);
|
||||
requestTimer.start();
|
||||
}
|
||||
|
||||
PictureLoaderWorker::~PictureLoaderWorker()
|
||||
{
|
||||
saveRedirectCache();
|
||||
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);
|
||||
if (!cachedRedirect.isEmpty()) {
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Using cached redirect for " << url.toDisplayString()
|
||||
<< " to " << cachedRedirect.toDisplayString();
|
||||
return makeRequest(cachedRedirect); // Use the cached redirect
|
||||
queueRequest(cachedRedirect, worker);
|
||||
}
|
||||
if (cache->metaData(url).isValid()) {
|
||||
makeRequest(url, worker);
|
||||
} 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);
|
||||
|
||||
// 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) {
|
||||
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (redirectTarget.isValid()) {
|
||||
QUrl redirectUrl = redirectTarget.toUrl();
|
||||
if (redirectUrl.isRelative()) {
|
||||
redirectUrl = url.resolved(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();
|
||||
});
|
||||
|
||||
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)
|
||||
{
|
||||
redirectCache[originalUrl] = qMakePair(redirectUrl, QDateTime::currentDateTimeUtc());
|
||||
saveRedirectCache();
|
||||
// saveRedirectCache();
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
#ifndef PICTURE_LOADER_WORKER_H
|
||||
#define PICTURE_LOADER_WORKER_H
|
||||
|
||||
#include "../../../game/cards/card_database.h"
|
||||
#include "../../../game/cards/card_info.h"
|
||||
#include "picture_loader_worker_work.h"
|
||||
#include "picture_to_load.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QMutex>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QObject>
|
||||
#include <QQueue>
|
||||
#include <QTimer>
|
||||
|
||||
#define REDIRECT_HEADER_NAME "redirects"
|
||||
#define REDIRECT_ORIGINAL_URL "original"
|
||||
|
|
@ -17,16 +22,23 @@
|
|||
|
||||
inline Q_LOGGING_CATEGORY(PictureLoaderWorkerLog, "picture_loader.worker");
|
||||
|
||||
class PictureLoaderWorkerWork;
|
||||
class PictureLoaderWorker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PictureLoaderWorker();
|
||||
~PictureLoaderWorker() override;
|
||||
void queueRequest(const QUrl &url, PictureLoaderWorkerWork *worker);
|
||||
|
||||
void enqueueImageLoad(CardInfoPtr card);
|
||||
void enqueueImageLoad(const CardInfoPtr &card);
|
||||
void clearNetworkCache();
|
||||
|
||||
public slots:
|
||||
QNetworkReply *makeRequest(const QUrl &url, PictureLoaderWorkerWork *workThread);
|
||||
void processQueuedRequests();
|
||||
void imageLoadedSuccessfully(CardInfoPtr card, const QImage &image);
|
||||
|
||||
private:
|
||||
static QStringList md5Blacklist;
|
||||
|
||||
|
|
@ -35,6 +47,7 @@ private:
|
|||
QList<PictureToLoad> loadQueue;
|
||||
QMutex mutex;
|
||||
QNetworkAccessManager *networkManager;
|
||||
QNetworkDiskCache *cache;
|
||||
QHash<QUrl, QPair<QUrl, QDateTime>> redirectCache; // Stores redirect and timestamp
|
||||
QString cacheFilePath; // Path to persistent storage
|
||||
static constexpr int CacheTTLInDays = 30; // TODO: Make user configurable
|
||||
|
|
@ -42,18 +55,11 @@ private:
|
|||
PictureToLoad cardBeingLoaded;
|
||||
PictureToLoad cardBeingDownloaded;
|
||||
bool picDownload, downloadRunning, loadQueueRunning;
|
||||
QQueue<QPair<QUrl, PictureLoaderWorkerWork *>> requestLoadQueue;
|
||||
QList<QPair<QUrl, PictureLoaderWorkerWork *>> requestQueue;
|
||||
QTimer requestTimer; // Timer for processing delayed requests
|
||||
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);
|
||||
QUrl getCachedRedirect(const QUrl &originalUrl) const;
|
||||
void loadRedirectCache();
|
||||
|
|
@ -61,18 +67,15 @@ private:
|
|||
void cleanStaleEntries();
|
||||
|
||||
private slots:
|
||||
void picDownloadFinished(QNetworkReply *reply);
|
||||
void picDownloadFailed();
|
||||
|
||||
void picDownloadChanged();
|
||||
void picsPathChanged();
|
||||
void setOverrideAllCardArtWithPersonalPreference(bool _overrideAllCardArtWithPersonalPreference);
|
||||
public slots:
|
||||
void processLoadQueue();
|
||||
|
||||
signals:
|
||||
void startLoadQueue();
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -12,12 +12,13 @@ DeckCardZoneDisplayWidget::DeckCardZoneDisplayWidget(QWidget *parent,
|
|||
QString _zoneName,
|
||||
QString _activeGroupCriteria,
|
||||
QStringList _activeSortCriteria,
|
||||
DisplayType _displayType,
|
||||
int bannerOpacity,
|
||||
int subBannerOpacity,
|
||||
CardSizeWidget *_cardSizeWidget)
|
||||
: QWidget(parent), deckListModel(_deckListModel), zoneName(_zoneName), activeGroupCriteria(_activeGroupCriteria),
|
||||
activeSortCriteria(_activeSortCriteria), bannerOpacity(bannerOpacity), subBannerOpacity(subBannerOpacity),
|
||||
cardSizeWidget(_cardSizeWidget)
|
||||
activeSortCriteria(_activeSortCriteria), displayType(_displayType), bannerOpacity(bannerOpacity),
|
||||
subBannerOpacity(subBannerOpacity), cardSizeWidget(_cardSizeWidget)
|
||||
{
|
||||
layout = new QVBoxLayout(this);
|
||||
setLayout(layout);
|
||||
|
|
@ -60,7 +61,7 @@ void DeckCardZoneDisplayWidget::displayCards()
|
|||
deleteCardGroupIfItDoesNotExist();
|
||||
}
|
||||
|
||||
void DeckCardZoneDisplayWidget::refreshDisplayType(const QString &_displayType)
|
||||
void DeckCardZoneDisplayWidget::refreshDisplayType(const DisplayType &_displayType)
|
||||
{
|
||||
displayType = _displayType;
|
||||
QLayoutItem *item;
|
||||
|
|
@ -102,7 +103,7 @@ void DeckCardZoneDisplayWidget::addCardGroupIfItDoesNotExist()
|
|||
continue;
|
||||
}
|
||||
|
||||
if (displayType == "overlap") {
|
||||
if (displayType == DisplayType::Overlap) {
|
||||
auto *display_widget = new OverlappedCardGroupDisplayWidget(
|
||||
cardGroupContainer, deckListModel, zoneName, cardGroup, activeGroupCriteria, activeSortCriteria,
|
||||
subBannerOpacity, cardSizeWidget);
|
||||
|
|
@ -112,7 +113,7 @@ void DeckCardZoneDisplayWidget::addCardGroupIfItDoesNotExist()
|
|||
connect(this, &DeckCardZoneDisplayWidget::activeSortCriteriaChanged, display_widget,
|
||||
&CardGroupDisplayWidget::onActiveSortCriteriaChanged);
|
||||
cardGroupLayout->addWidget(display_widget);
|
||||
} else if (displayType == "flat") {
|
||||
} else if (displayType == DisplayType::Flat) {
|
||||
auto *display_widget = new FlatCardGroupDisplayWidget(cardGroupContainer, deckListModel, zoneName,
|
||||
cardGroup, activeGroupCriteria, activeSortCriteria,
|
||||
subBannerOpacity, cardSizeWidget);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "../../../../game/cards/card_info.h"
|
||||
#include "../general/display/banner_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_size_widget.h"
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ public:
|
|||
QString zoneName,
|
||||
QString activeGroupCriteria,
|
||||
QStringList activeSortCriteria,
|
||||
DisplayType displayType,
|
||||
int bannerOpacity,
|
||||
int subBannerOpacity,
|
||||
CardSizeWidget *_cardSizeWidget);
|
||||
|
|
@ -33,7 +35,7 @@ public slots:
|
|||
void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card);
|
||||
void onHover(CardInfoPtr card);
|
||||
void displayCards();
|
||||
void refreshDisplayType(const QString &displayType);
|
||||
void refreshDisplayType(const DisplayType &displayType);
|
||||
void addCardGroupIfItDoesNotExist();
|
||||
void deleteCardGroupIfItDoesNotExist();
|
||||
void onActiveGroupCriteriaChanged(QString activeGroupCriteria);
|
||||
|
|
@ -48,6 +50,7 @@ signals:
|
|||
private:
|
||||
QString activeGroupCriteria;
|
||||
QStringList activeSortCriteria;
|
||||
DisplayType displayType = DisplayType::Overlap;
|
||||
int bannerOpacity = 20;
|
||||
int subBannerOpacity = 10;
|
||||
CardSizeWidget *cardSizeWidget;
|
||||
|
|
@ -55,7 +58,6 @@ private:
|
|||
BannerWidget *banner;
|
||||
QWidget *cardGroupContainer;
|
||||
QVBoxLayout *cardGroupLayout;
|
||||
QString displayType = "flat";
|
||||
OverlapWidget *overlapWidget;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -104,6 +104,20 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
|||
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckModel->getDeckList());
|
||||
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->setIcon(QPixmap("theme:icons/increment"));
|
||||
connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrement);
|
||||
|
|
@ -144,6 +158,9 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
|||
|
||||
upperLayout->addWidget(deckTagsDisplayWidget, 3, 1);
|
||||
|
||||
upperLayout->addWidget(activeGroupCriteriaLabel, 4, 0);
|
||||
upperLayout->addWidget(activeGroupCriteriaComboBox, 4, 1);
|
||||
|
||||
hashLabel1 = new QLabel();
|
||||
hashLabel1->setObjectName("hashLabel1");
|
||||
auto *hashSizePolicy = new QSizePolicy();
|
||||
|
|
@ -286,11 +303,7 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox()
|
|||
});
|
||||
|
||||
for (const auto &pair : pairList) {
|
||||
QVariantMap dataMap;
|
||||
dataMap["name"] = pair.first;
|
||||
dataMap["uuid"] = pair.second;
|
||||
|
||||
bannerCardComboBox->addItem(pair.first, dataMap);
|
||||
bannerCardComboBox->addItem(pair.first, QVariant::fromValue(pair));
|
||||
}
|
||||
|
||||
// Try to restore the previous selection by finding the currentText
|
||||
|
|
@ -298,7 +311,7 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox()
|
|||
if (restoredIndex != -1) {
|
||||
bannerCardComboBox->setCurrentIndex(restoredIndex);
|
||||
if (deckModel->getDeckList()->getBannerCard().second !=
|
||||
bannerCardComboBox->itemData(bannerCardComboBox->currentIndex()).toMap()["uuid"].toString()) {
|
||||
bannerCardComboBox->currentData().value<QPair<QString, QString>>().second) {
|
||||
setBannerCard(restoredIndex);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -318,9 +331,8 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox()
|
|||
|
||||
void DeckEditorDeckDockWidget::setBannerCard(int /* changedIndex */)
|
||||
{
|
||||
QVariantMap itemData = bannerCardComboBox->itemData(bannerCardComboBox->currentIndex()).toMap();
|
||||
deckModel->getDeckList()->setBannerCard(
|
||||
QPair<QString, QString>(itemData["name"].toString(), itemData["uuid"].toString()));
|
||||
auto cardAndId = bannerCardComboBox->currentData().value<QPair<QString, QString>>();
|
||||
deckModel->getDeckList()->setBannerCard(cardAndId);
|
||||
emit deckModified();
|
||||
}
|
||||
|
||||
|
|
@ -578,6 +590,7 @@ void DeckEditorDeckDockWidget::retranslateUi()
|
|||
showBannerCardCheckBox->setText(tr("Show banner card selection menu"));
|
||||
showTagsWidgetCheckBox->setText(tr("Show tags selection menu"));
|
||||
commentsLabel->setText(tr("&Comments:"));
|
||||
activeGroupCriteriaLabel->setText(tr("Group by:"));
|
||||
hashLabel1->setText(tr("Hash:"));
|
||||
|
||||
aIncrement->setText(tr("&Increment number"));
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ private:
|
|||
DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget;
|
||||
QLabel *hashLabel1;
|
||||
LineEditUnfocusable *hashLabel;
|
||||
QLabel *activeGroupCriteriaLabel;
|
||||
QComboBox *activeGroupCriteriaComboBox;
|
||||
|
||||
QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ void SettingsButtonWidget::addSettingsWidget(QWidget *toAdd) const
|
|||
popup->addSettingsWidget(toAdd);
|
||||
}
|
||||
|
||||
void SettingsButtonWidget::removeSettingsWidget(QWidget *toRemove) const
|
||||
{
|
||||
popup->removeSettingsWidget(toRemove);
|
||||
}
|
||||
|
||||
void SettingsButtonWidget::setButtonIcon(QPixmap iconMap)
|
||||
{
|
||||
button->setIcon(iconMap);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ class SettingsButtonWidget : public QWidget
|
|||
public:
|
||||
explicit SettingsButtonWidget(QWidget *parent = nullptr);
|
||||
void addSettingsWidget(QWidget *toAdd) const;
|
||||
void removeSettingsWidget(QWidget *toRemove) const;
|
||||
void setButtonIcon(QPixmap iconMap);
|
||||
|
||||
protected:
|
||||
|
|
@ -25,6 +26,8 @@ private slots:
|
|||
private:
|
||||
QHBoxLayout *layout;
|
||||
QToolButton *button;
|
||||
|
||||
public:
|
||||
SettingsPopupWidget *popup;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,12 @@ void SettingsPopupWidget::addSettingsWidget(QWidget *toAdd) const
|
|||
containerLayout->addWidget(toAdd); // Add to containerWidget's layout
|
||||
}
|
||||
|
||||
void SettingsPopupWidget::removeSettingsWidget(QWidget *toRemove) const
|
||||
{
|
||||
containerLayout->removeWidget(toRemove);
|
||||
toRemove->deleteLater();
|
||||
}
|
||||
|
||||
void SettingsPopupWidget::adjustSizeToFitScreen()
|
||||
{
|
||||
QScreen *screen = QApplication::screenAt(this->pos());
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ class SettingsPopupWidget : public QWidget
|
|||
public:
|
||||
explicit SettingsPopupWidget(QWidget *parent = nullptr);
|
||||
void addSettingsWidget(QWidget *toAdd) const;
|
||||
void removeSettingsWidget(QWidget *toRemove) const;
|
||||
void adjustSizeToFitScreen();
|
||||
|
||||
signals:
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ void VisualDatabaseDisplayFilterSaveLoadWidget::refreshFilterList()
|
|||
|
||||
// Refresh the filter file list
|
||||
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
|
||||
for (const QString &filename : filterFiles) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "visual_database_display_name_filter_widget.h"
|
||||
|
||||
#include "../../../../dialogs/dlg_load_deck_from_clipboard.h"
|
||||
#include "../../../tabs/abstract_tab_deck_editor.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
|
|
@ -35,6 +36,13 @@ VisualDatabaseDisplayNameFilterWidget::VisualDatabaseDisplayNameFilterWidget(QWi
|
|||
layout->addWidget(loadFromDeckButton);
|
||||
|
||||
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,
|
||||
[this]() { QTimer::singleShot(100, this, &VisualDatabaseDisplayNameFilterWidget::syncWithFilterModel); });
|
||||
|
||||
|
|
@ -46,6 +54,8 @@ void VisualDatabaseDisplayNameFilterWidget::retranslateUi()
|
|||
searchBox->setPlaceholderText(tr("Filter by name..."));
|
||||
loadFromDeckButton->setText(tr("Load from Deck"));
|
||||
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()
|
||||
|
|
@ -75,6 +85,20 @@ void VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck()
|
|||
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)
|
||||
{
|
||||
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; }"
|
||||
"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);
|
||||
activeFilters[name] = button;
|
||||
|
|
@ -96,9 +124,6 @@ void VisualDatabaseDisplayNameFilterWidget::removeNameFilter(const QString &name
|
|||
if (activeFilters.contains(name)) {
|
||||
activeFilters[name]->deleteLater(); // Safe deletion
|
||||
activeFilters.remove(name);
|
||||
|
||||
QTimer::singleShot(0, this,
|
||||
&VisualDatabaseDisplayNameFilterWidget::updateFilterModel); // Avoid concurrent modification
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,11 +35,13 @@ private:
|
|||
QLineEdit *searchBox;
|
||||
FlowWidget *flowWidget;
|
||||
QPushButton *loadFromDeckButton;
|
||||
QPushButton *loadFromClipboardButton;
|
||||
|
||||
QMap<QString, QPushButton *> activeFilters; // Store active name filter buttons
|
||||
|
||||
private slots:
|
||||
void actLoadFromDeck();
|
||||
void actLoadFromClipboard();
|
||||
};
|
||||
|
||||
#endif // VISUAL_DATABASE_DISPLAY_NAME_FILTER_WIDGET_H
|
||||
|
|
|
|||
|
|
@ -95,7 +95,15 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
|||
clearFilterWidget = new QToolButton();
|
||||
clearFilterWidget->setFixedSize(32, 32);
|
||||
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->setButtonIcon(QPixmap("theme:icons/lock"));
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ void VisualDeckEditorWidget::retranslateUi()
|
|||
searchPushButton->setToolTip(tr("Search for closest match in the database (with auto-suggestions) and add "
|
||||
"preferred printing to the deck on pressing enter"));
|
||||
sortCriteriaButton->setToolTip(tr("Configure how cards are sorted within their groups"));
|
||||
displayTypeButton->setText(tr("Flat Layout"));
|
||||
displayTypeButton->setText(tr("Overlap Layout"));
|
||||
displayTypeButton->setToolTip(
|
||||
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
|
||||
switch (currentDisplayType) {
|
||||
case DisplayType::Flat:
|
||||
emit displayTypeChanged("flat");
|
||||
displayTypeButton->setText(tr("Flat Layout"));
|
||||
break;
|
||||
case DisplayType::Overlap:
|
||||
emit displayTypeChanged("overlap");
|
||||
displayTypeButton->setText(tr("Overlap Layout"));
|
||||
break;
|
||||
}
|
||||
emit displayTypeChanged(currentDisplayType);
|
||||
}
|
||||
|
||||
void VisualDeckEditorWidget::addZoneIfDoesNotExist()
|
||||
|
|
@ -239,8 +238,9 @@ void VisualDeckEditorWidget::addZoneIfDoesNotExist()
|
|||
if (found) {
|
||||
continue;
|
||||
}
|
||||
DeckCardZoneDisplayWidget *zoneDisplayWidget = new DeckCardZoneDisplayWidget(
|
||||
zoneContainer, deckListModel, zone, activeGroupCriteria, activeSortCriteria, 20, 10, cardSizeWidget);
|
||||
DeckCardZoneDisplayWidget *zoneDisplayWidget =
|
||||
new DeckCardZoneDisplayWidget(zoneContainer, deckListModel, zone, activeGroupCriteria, activeSortCriteria,
|
||||
currentDisplayType, 20, 10, cardSizeWidget);
|
||||
connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardHovered, this, &VisualDeckEditorWidget::onHover);
|
||||
connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardClicked, this, &VisualDeckEditorWidget::onCardClick);
|
||||
connect(this, &VisualDeckEditorWidget::activeSortCriteriaChanged, zoneDisplayWidget,
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ signals:
|
|||
void activeSortCriteriaChanged(QStringList activeSortCriteria);
|
||||
void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName);
|
||||
void cardAdditionRequested(CardInfoPtr card);
|
||||
void displayTypeChanged(QString displayType);
|
||||
void displayTypeChanged(DisplayType displayType);
|
||||
|
||||
protected slots:
|
||||
void onHover(CardInfoPtr hoveredCard);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#include <QDirIterator>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
|
||||
DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckList *_deckList)
|
||||
: QWidget(_parent), deckList(nullptr)
|
||||
|
|
@ -77,6 +78,21 @@ static QStringList getAllFiles(const QString &filePath)
|
|||
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()
|
||||
{
|
||||
if (qobject_cast<DeckPreviewWidget *>(parentWidget())) {
|
||||
|
|
@ -91,6 +107,10 @@ void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
|
|||
// Retrieve saved preference if the prompt is disabled
|
||||
if (!SettingsCache::instance().getVisualDeckStoragePromptForConversion()) {
|
||||
if (SettingsCache::instance().getVisualDeckStorageAlwaysConvert()) {
|
||||
|
||||
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath))
|
||||
return;
|
||||
|
||||
deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath);
|
||||
deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastFileName();
|
||||
deckPreviewWidget->refreshBannerCardText();
|
||||
|
|
@ -100,6 +120,10 @@ void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
|
|||
// Show the dialog to the user
|
||||
DialogConvertDeckToCodFormat conversionDialog(parentWidget());
|
||||
if (conversionDialog.exec() == QDialog::Accepted) {
|
||||
|
||||
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath))
|
||||
return;
|
||||
|
||||
deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath);
|
||||
deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastFileName();
|
||||
deckPreviewWidget->refreshBannerCardText();
|
||||
|
|
|
|||
|
|
@ -50,10 +50,10 @@ void DeckPreviewTagDisplayWidget::mousePressEvent(QMouseEvent *event)
|
|||
{
|
||||
switch (event->button()) {
|
||||
case Qt::LeftButton:
|
||||
setState(TagState::Selected);
|
||||
setState(state != TagState::Selected ? TagState::Selected : TagState::NotSelected);
|
||||
break;
|
||||
case Qt::RightButton:
|
||||
setState(TagState::Excluded);
|
||||
setState(state != TagState::Excluded ? TagState::Excluded : TagState::NotSelected);
|
||||
break;
|
||||
case Qt::MiddleButton:
|
||||
setState(TagState::NotSelected);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent,
|
|||
&DeckPreviewWidget::updateTagsVisibility);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::visualDeckStorageShowBannerCardComboBoxChanged, this,
|
||||
&DeckPreviewWidget::updateBannerCardComboBoxVisibility);
|
||||
connect(visualDeckStorageWidget->settings(), &VisualDeckStorageQuickSettingsWidget::deckPreviewTooltipChanged, this,
|
||||
&DeckPreviewWidget::refreshBannerCardToolTip);
|
||||
|
||||
layout->addWidget(bannerCardDisplayWidget);
|
||||
}
|
||||
|
|
@ -79,7 +81,6 @@ void DeckPreviewWidget::initializeUi(const bool deckLoadSuccess)
|
|||
|
||||
bannerCardDisplayWidget->setCard(bannerCard);
|
||||
bannerCardDisplayWidget->setFontSize(24);
|
||||
refreshBannerCardText();
|
||||
setFilePath(deckLoader->getLastFileName());
|
||||
|
||||
colorIdentityWidget = new ColorIdentityWidget(this, getColorIdentity());
|
||||
|
|
@ -104,6 +105,8 @@ void DeckPreviewWidget::initializeUi(const bool deckLoadSuccess)
|
|||
layout->addWidget(bannerCardLabel);
|
||||
layout->addWidget(bannerCardComboBox);
|
||||
|
||||
refreshBannerCardText();
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
|
|
@ -179,15 +182,42 @@ QString DeckPreviewWidget::getColorIdentity()
|
|||
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)
|
||||
{
|
||||
filePath = _filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the banner card text.
|
||||
* This also calls `refreshBannerCardToolTip`, since those two often need to be updated together.
|
||||
*/
|
||||
void DeckPreviewWidget::refreshBannerCardText()
|
||||
{
|
||||
bannerCardDisplayWidget->setOverlayText(
|
||||
deckLoader->getName().isEmpty() ? QFileInfo(deckLoader->getLastFileName()).fileName() : deckLoader->getName());
|
||||
bannerCardDisplayWidget->setOverlayText(getDisplayName());
|
||||
|
||||
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()
|
||||
|
|
@ -260,11 +290,11 @@ void DeckPreviewWidget::updateBannerCardComboBox()
|
|||
|
||||
void DeckPreviewWidget::setBannerCard(int /* changedIndex */)
|
||||
{
|
||||
QVariant itemData = bannerCardComboBox->itemData(bannerCardComboBox->currentIndex());
|
||||
deckLoader->setBannerCard(QPair<QString, QString>(bannerCardComboBox->currentText(), itemData.toString()));
|
||||
auto nameAndId = bannerCardComboBox->currentData().value<QPair<QString, QString>>();
|
||||
deckLoader->setBannerCard(nameAndId);
|
||||
deckLoader->saveToFile(filePath, DeckLoader::getFormatFromName(filePath));
|
||||
bannerCardDisplayWidget->setCard(CardDatabaseManager::getInstance()->getCardByNameAndProviderId(
|
||||
bannerCardComboBox->currentText(), itemData.toString()));
|
||||
bannerCardDisplayWidget->setCard(
|
||||
CardDatabaseManager::getInstance()->getCardByNameAndProviderId(nameAndId.first, nameAndId.second));
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ public:
|
|||
const QString &_filePath);
|
||||
void retranslateUi();
|
||||
QString getColorIdentity();
|
||||
QString getDisplayName() const;
|
||||
|
||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||
QVBoxLayout *layout;
|
||||
|
|
@ -49,6 +50,7 @@ signals:
|
|||
public slots:
|
||||
void setFilePath(const QString &filePath);
|
||||
void refreshBannerCardText();
|
||||
void refreshBannerCardToolTip();
|
||||
void updateBannerCardComboBox();
|
||||
void setBannerCard(int);
|
||||
void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "visual_deck_storage_widget.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QSpinBox>
|
||||
|
||||
VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidget *parent)
|
||||
|
|
@ -41,14 +42,6 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg
|
|||
connect(showBannerCardComboBoxCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
|
||||
&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
|
||||
drawUnusedColorIdentitiesCheckBox = new QCheckBox(this);
|
||||
drawUnusedColorIdentitiesCheckBox->setChecked(
|
||||
|
|
@ -80,6 +73,26 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg
|
|||
unusedColorIdentityOpacityLayout->addWidget(unusedColorIdentitiesOpacityLabel);
|
||||
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
|
||||
cardSizeWidget = new CardSizeWidget(this, nullptr, SettingsCache::instance().getVisualDeckStorageCardSize());
|
||||
connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, this,
|
||||
|
|
@ -92,9 +105,9 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg
|
|||
this->addSettingsWidget(showTagFilterCheckBox);
|
||||
this->addSettingsWidget(showTagsOnDeckPreviewsCheckBox);
|
||||
this->addSettingsWidget(showBannerCardComboBoxCheckBox);
|
||||
this->addSettingsWidget(searchFolderNamesCheckBox);
|
||||
this->addSettingsWidget(drawUnusedColorIdentitiesCheckBox);
|
||||
this->addSettingsWidget(unusedColorIdentityOpacityWidget);
|
||||
this->addSettingsWidget(deckPreviewTooltipWidget);
|
||||
this->addSettingsWidget(cardSizeWidget);
|
||||
|
||||
connect(&SettingsCache::instance(), &SettingsCache::langChanged, this,
|
||||
|
|
@ -108,10 +121,13 @@ void VisualDeckStorageQuickSettingsWidget::retranslateUi()
|
|||
showTagFilterCheckBox->setText(tr("Show Tag Filter"));
|
||||
showTagsOnDeckPreviewsCheckBox->setText(tr("Show Tags On Deck Previews"));
|
||||
showBannerCardComboBoxCheckBox->setText(tr("Show Banner Card Selection Option"));
|
||||
searchFolderNamesCheckBox->setText(tr("Include Folder Names in Search"));
|
||||
drawUnusedColorIdentitiesCheckBox->setText(tr("Draw unused Color Identities"));
|
||||
unusedColorIdentitiesOpacityLabel->setText(tr("Unused Color Identities Opacity"));
|
||||
unusedColorIdentitiesOpacitySpinBox->setSuffix("%");
|
||||
|
||||
deckPreviewTooltipLabel->setText(tr("Deck tooltip:"));
|
||||
deckPreviewTooltipComboBox->setItemText(0, tr("None"));
|
||||
deckPreviewTooltipComboBox->setItemText(1, tr("Filepath"));
|
||||
}
|
||||
|
||||
bool VisualDeckStorageQuickSettingsWidget::getShowFolders() const
|
||||
|
|
@ -139,16 +155,16 @@ bool VisualDeckStorageQuickSettingsWidget::getShowTagsOnDeckPreviews() const
|
|||
return showTagsOnDeckPreviewsCheckBox->isChecked();
|
||||
}
|
||||
|
||||
bool VisualDeckStorageQuickSettingsWidget::getSearchFolderNames() const
|
||||
{
|
||||
return searchFolderNamesCheckBox->isChecked();
|
||||
}
|
||||
|
||||
int VisualDeckStorageQuickSettingsWidget::getUnusedColorIdentitiesOpacity() const
|
||||
{
|
||||
return unusedColorIdentitiesOpacitySpinBox->value();
|
||||
}
|
||||
|
||||
VisualDeckStorageQuickSettingsWidget::TooltipType VisualDeckStorageQuickSettingsWidget::getDeckPreviewTooltip() const
|
||||
{
|
||||
return deckPreviewTooltipComboBox->currentData().value<TooltipType>();
|
||||
}
|
||||
|
||||
int VisualDeckStorageQuickSettingsWidget::getCardSize() const
|
||||
{
|
||||
return cardSizeWidget->getSlider()->value();
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ class CardSizeWidget;
|
|||
class QLabel;
|
||||
class QSpinBox;
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
|
||||
/**
|
||||
* The VDS's quick settings menu.
|
||||
|
|
@ -22,12 +23,23 @@ class VisualDeckStorageQuickSettingsWidget : public SettingsButtonWidget
|
|||
QCheckBox *showBannerCardComboBoxCheckBox;
|
||||
QCheckBox *showTagFilterCheckBox;
|
||||
QCheckBox *showTagsOnDeckPreviewsCheckBox;
|
||||
QCheckBox *searchFolderNamesCheckBox;
|
||||
QLabel *unusedColorIdentitiesOpacityLabel;
|
||||
QSpinBox *unusedColorIdentitiesOpacitySpinBox;
|
||||
QLabel *deckPreviewTooltipLabel;
|
||||
QComboBox *deckPreviewTooltipComboBox;
|
||||
CardSizeWidget *cardSizeWidget;
|
||||
|
||||
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);
|
||||
|
||||
void retranslateUi();
|
||||
|
|
@ -37,8 +49,8 @@ public:
|
|||
bool getShowBannerCardComboBox() const;
|
||||
bool getShowTagFilter() const;
|
||||
bool getShowTagsOnDeckPreviews() const;
|
||||
bool getSearchFolderNames() const;
|
||||
int getUnusedColorIdentitiesOpacity() const;
|
||||
TooltipType getDeckPreviewTooltip() const;
|
||||
int getCardSize() const;
|
||||
|
||||
signals:
|
||||
|
|
@ -47,8 +59,8 @@ signals:
|
|||
void showBannerCardComboBoxChanged(bool enabled);
|
||||
void showTagFilterChanged(bool enabled);
|
||||
void showTagsOnDeckPreviewsChanged(bool enabled);
|
||||
void searchFolderNamesChanged(bool enabled);
|
||||
void unusedColorIdentitiesOpacityChanged(int opacity);
|
||||
void deckPreviewTooltipChanged(TooltipType tooltip);
|
||||
void cardSizeChanged(int scale);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
#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 "../../pixel_map_generator.h"
|
||||
|
||||
#include <QAction>
|
||||
|
||||
/**
|
||||
* @brief Constructs a PrintingSelectorCardSearchWidget for searching cards by set name or set code.
|
||||
|
|
@ -17,7 +22,13 @@ VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(VisualDeckStorageWi
|
|||
setLayout(layout);
|
||||
|
||||
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);
|
||||
|
||||
// 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.
|
||||
* 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
|
||||
* @param filePath The filepath to convert into a relative filepath
|
||||
*/
|
||||
static QString getFileSearchName(const QString &filePath, bool includeFolderName)
|
||||
static QString toRelativeFilepath(const QString &filePath)
|
||||
{
|
||||
QString deckPath = SettingsCache::instance().getDeckPath();
|
||||
if (includeFolderName && filePath.startsWith(deckPath)) {
|
||||
return filePath.mid(deckPath.length()).toLower();
|
||||
if (filePath.startsWith(deckPath)) {
|
||||
return filePath.mid(deckPath.length());
|
||||
}
|
||||
|
||||
QFileInfo fileInfo(filePath);
|
||||
QString fileName = fileInfo.fileName().toLower();
|
||||
QString fileName = fileInfo.fileName();
|
||||
return fileName;
|
||||
}
|
||||
|
||||
void VisualDeckStorageSearchWidget::filterWidgets(QList<DeckPreviewWidget *> widgets,
|
||||
const QString &searchText,
|
||||
bool includeFolderName)
|
||||
void VisualDeckStorageSearchWidget::filterWidgets(QList<DeckPreviewWidget *> widgets, const QString &searchText)
|
||||
{
|
||||
if (searchText.isEmpty() || searchText.isNull()) {
|
||||
for (auto widget : widgets) {
|
||||
widget->filteredBySearch = false;
|
||||
}
|
||||
}
|
||||
auto filterString = DeckFilterString(searchText);
|
||||
|
||||
for (auto file : widgets) {
|
||||
QString fileSearchName = getFileSearchName(file->filePath, includeFolderName);
|
||||
file->filteredBySearch = !fileSearchName.contains(searchText.toLower());
|
||||
for (auto widget : widgets) {
|
||||
QString relativeFilePath = toRelativeFilepath(widget->filePath);
|
||||
widget->filteredBySearch = !filterString.check(widget, {relativeFilePath});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class VisualDeckStorageSearchWidget : public QWidget
|
|||
public:
|
||||
explicit VisualDeckStorageSearchWidget(VisualDeckStorageWidget *parent);
|
||||
QString getSearchText();
|
||||
void filterWidgets(QList<DeckPreviewWidget *> widgets, const QString &searchText, bool includeFolderName);
|
||||
void filterWidgets(QList<DeckPreviewWidget *> widgets, const QString &searchText);
|
||||
|
||||
private:
|
||||
QHBoxLayout *layout;
|
||||
|
|
|
|||
|
|
@ -46,8 +46,6 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare
|
|||
&VisualDeckStorageWidget::updateShowFolders);
|
||||
connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::showTagFilterChanged, this,
|
||||
&VisualDeckStorageWidget::updateTagsVisibility);
|
||||
connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::searchFolderNamesChanged, this,
|
||||
&VisualDeckStorageWidget::updateSearchFilter);
|
||||
|
||||
searchAndSortLayout->addWidget(deckPreviewColorIdentityFilterWidget);
|
||||
searchAndSortLayout->addWidget(sortWidget);
|
||||
|
|
@ -196,8 +194,7 @@ void VisualDeckStorageWidget::updateColorFilter()
|
|||
void VisualDeckStorageWidget::updateSearchFilter()
|
||||
{
|
||||
if (folderWidget) {
|
||||
searchWidget->filterWidgets(folderWidget->findChildren<DeckPreviewWidget *>(), searchWidget->getSearchText(),
|
||||
quickSettingsWidget->getSearchFolderNames());
|
||||
searchWidget->filterWidgets(folderWidget->findChildren<DeckPreviewWidget *>(), searchWidget->getSearchText());
|
||||
folderWidget->updateVisibility();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1099,7 +1099,7 @@ void MainWindow::cardDatabaseLoadingFailed()
|
|||
|
||||
void MainWindow::cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames)
|
||||
{
|
||||
QMessageBox msgBox;
|
||||
QMessageBox msgBox(this);
|
||||
msgBox.setWindowTitle(tr("New sets found"));
|
||||
msgBox.setIcon(QMessageBox::Question);
|
||||
msgBox.setText(tr("%n new set(s) found in the card database\n"
|
||||
|
|
|
|||
|
|
@ -29,6 +29,24 @@ DeckListModel::~DeckListModel()
|
|||
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()
|
||||
{
|
||||
beginResetModel();
|
||||
|
|
@ -49,7 +67,7 @@ void DeckListModel::rebuildTree()
|
|||
}
|
||||
|
||||
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));
|
||||
|
||||
|
|
@ -467,6 +485,12 @@ void DeckListModel::sort(int column, Qt::SortOrder order)
|
|||
emit layoutChanged();
|
||||
}
|
||||
|
||||
void DeckListModel::setActiveGroupCriteria(DeckListModelGroupCriteria newCriteria)
|
||||
{
|
||||
activeGroupCriteria = newCriteria;
|
||||
rebuildTree();
|
||||
}
|
||||
|
||||
void DeckListModel::cleanList()
|
||||
{
|
||||
setDeckList(new DeckLoader);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,13 @@ class CardDatabase;
|
|||
class QPrinter;
|
||||
class QTextCursor;
|
||||
|
||||
enum DeckListModelGroupCriteria
|
||||
{
|
||||
MAIN_TYPE,
|
||||
MANA_COST,
|
||||
COLOR
|
||||
};
|
||||
|
||||
class DecklistModelCardNode : public AbstractDecklistCardNode
|
||||
{
|
||||
private:
|
||||
|
|
@ -85,6 +92,7 @@ signals:
|
|||
public:
|
||||
explicit DeckListModel(QObject *parent = nullptr);
|
||||
~DeckListModel() override;
|
||||
QString getSortCriteriaForCard(CardInfoPtr info);
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
|
@ -113,10 +121,12 @@ public:
|
|||
QList<CardInfoPtr> getCardsAsCardInfoPtrs() const;
|
||||
QList<CardInfoPtr> getCardsAsCardInfoPtrsForZone(QString zoneName) const;
|
||||
QList<QString> *getZones() const;
|
||||
void setActiveGroupCriteria(DeckListModelGroupCriteria newCriteria);
|
||||
|
||||
private:
|
||||
DeckLoader *deckList;
|
||||
InnerDecklistNode *root;
|
||||
DeckListModelGroupCriteria activeGroupCriteria = DeckListModelGroupCriteria::MAIN_TYPE;
|
||||
int lastKnownColumn;
|
||||
Qt::SortOrder lastKnownOrder;
|
||||
InnerDecklistNode *createNodeIfNeeded(const QString &name, InnerDecklistNode *parent);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "../../client/ui/picture_loader/picture_loader.h"
|
||||
#include "../../deck/deck_loader.h"
|
||||
#include "../../dialogs/dlg_load_deck.h"
|
||||
#include "../../dialogs/dlg_load_deck_from_clipboard.h"
|
||||
#include "../../dialogs/dlg_load_remote_deck.h"
|
||||
#include "../../server/pending_command.h"
|
||||
#include "../../settings/cache_settings.h"
|
||||
|
|
@ -52,6 +53,7 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
|||
{
|
||||
loadLocalButton = new QPushButton;
|
||||
loadRemoteButton = new QPushButton;
|
||||
loadFromClipboardButton = new QPushButton;
|
||||
unloadDeckButton = new QPushButton;
|
||||
readyStartButton = new ToggleButton;
|
||||
forceStartGameButton = new QPushButton;
|
||||
|
|
@ -59,6 +61,7 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
|||
|
||||
connect(loadLocalButton, &QPushButton::clicked, this, &DeckViewContainer::loadLocalDeck);
|
||||
connect(loadRemoteButton, &QPushButton::clicked, this, &DeckViewContainer::loadRemoteDeck);
|
||||
connect(loadFromClipboardButton, &QPushButton::clicked, this, &DeckViewContainer::loadFromClipboard);
|
||||
connect(readyStartButton, &QPushButton::clicked, this, &DeckViewContainer::readyStart);
|
||||
connect(unloadDeckButton, &QPushButton::clicked, this, &DeckViewContainer::unloadDeck);
|
||||
connect(forceStartGameButton, &QPushButton::clicked, this, &DeckViewContainer::forceStart);
|
||||
|
|
@ -68,6 +71,7 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
|||
auto *buttonHBox = new QHBoxLayout;
|
||||
buttonHBox->addWidget(loadLocalButton);
|
||||
buttonHBox->addWidget(loadRemoteButton);
|
||||
buttonHBox->addWidget(loadFromClipboardButton);
|
||||
buttonHBox->addWidget(unloadDeckButton);
|
||||
buttonHBox->addWidget(readyStartButton);
|
||||
buttonHBox->addWidget(sideboardLockButton);
|
||||
|
|
@ -118,6 +122,7 @@ void DeckViewContainer::retranslateUi()
|
|||
{
|
||||
loadLocalButton->setText(tr("Load deck..."));
|
||||
loadRemoteButton->setText(tr("Load remote deck..."));
|
||||
loadFromClipboardButton->setText(tr("Load from clipboard..."));
|
||||
unloadDeckButton->setText(tr("Unload deck"));
|
||||
readyStartButton->setText(tr("Ready to start"));
|
||||
forceStartGameButton->setText(tr("Force start"));
|
||||
|
|
@ -148,6 +153,7 @@ void DeckViewContainer::switchToDeckSelectView()
|
|||
|
||||
setVisibility(loadLocalButton, true);
|
||||
setVisibility(loadRemoteButton, !parentGame->getIsLocalGame());
|
||||
setVisibility(loadFromClipboardButton, true);
|
||||
setVisibility(unloadDeckButton, false);
|
||||
setVisibility(readyStartButton, false);
|
||||
setVisibility(sideboardLockButton, false);
|
||||
|
|
@ -172,6 +178,7 @@ void DeckViewContainer::switchToDeckLoadedView()
|
|||
|
||||
setVisibility(loadLocalButton, false);
|
||||
setVisibility(loadRemoteButton, false);
|
||||
setVisibility(loadFromClipboardButton, false);
|
||||
setVisibility(unloadDeckButton, true);
|
||||
setVisibility(readyStartButton, true);
|
||||
setVisibility(sideboardLockButton, true);
|
||||
|
|
@ -198,8 +205,11 @@ void DeckViewContainer::refreshShortcuts()
|
|||
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
|
||||
loadLocalButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadLocalButton"));
|
||||
loadRemoteButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadRemoteButton"));
|
||||
loadFromClipboardButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadFromClipboardButton"));
|
||||
unloadDeckButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/unloadDeckButton"));
|
||||
readyStartButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/readyStartButton"));
|
||||
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)
|
||||
{
|
||||
DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(filePath);
|
||||
QString deckString;
|
||||
DeckLoader deck;
|
||||
|
||||
bool error = !deck.loadFromFile(filePath, fmt, true);
|
||||
if (!error) {
|
||||
deckString = deck.writeToString_Native();
|
||||
error = deckString.length() > MAX_FILE_LENGTH;
|
||||
}
|
||||
if (error) {
|
||||
bool success = deck.loadFromFile(filePath, fmt, true);
|
||||
|
||||
if (!success) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("The selected file could not be loaded."));
|
||||
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;
|
||||
cmd.set_deck(deckString.toStdString());
|
||||
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)
|
||||
{
|
||||
const Response_DeckDownload &resp = r.GetExtension(Response_DeckDownload::ext);
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ class DeckViewContainer : public QWidget
|
|||
Q_OBJECT
|
||||
private:
|
||||
QVBoxLayout *deckViewLayout;
|
||||
QPushButton *loadLocalButton, *loadRemoteButton, *unloadDeckButton, *forceStartGameButton;
|
||||
QPushButton *loadLocalButton, *loadRemoteButton, *loadFromClipboardButton;
|
||||
QPushButton *unloadDeckButton, *forceStartGameButton;
|
||||
ToggleButton *readyStartButton, *sideboardLockButton;
|
||||
DeckView *deckView;
|
||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||
|
|
@ -58,6 +59,7 @@ private slots:
|
|||
void switchToDeckLoadedView();
|
||||
void loadLocalDeck();
|
||||
void loadRemoteDeck();
|
||||
void loadFromClipboard();
|
||||
void unloadDeck();
|
||||
void readyStart();
|
||||
void forceStart();
|
||||
|
|
@ -81,6 +83,7 @@ public:
|
|||
|
||||
public slots:
|
||||
void loadDeckFromFile(const QString &filePath);
|
||||
void loadDeckFromDeckLoader(const DeckLoader *deck);
|
||||
};
|
||||
|
||||
#endif // DECK_VIEW_CONTAINER_H
|
||||
|
|
|
|||
193
cockatrice/src/game/filters/deck_filter_string.cpp
Normal file
193
cockatrice/src/game/filters/deck_filter_string.cpp
Normal 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; };
|
||||
}
|
||||
}
|
||||
51
cockatrice/src/game/filters/deck_filter_string.h
Normal file
51
cockatrice/src/game/filters/deck_filter_string.h
Normal 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
|
||||
|
|
@ -4,10 +4,11 @@
|
|||
|
||||
#include <QByteArray>
|
||||
#include <QDebug>
|
||||
#include <QRegularExpression>
|
||||
#include <QString>
|
||||
#include <functional>
|
||||
|
||||
peg::parser search(R"(
|
||||
static peg::parser search(R"(
|
||||
Start <- QueryPartList
|
||||
~ws <- [ ]+
|
||||
QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws*
|
||||
|
|
@ -20,7 +21,7 @@ QueryPart <- NotQuery / SetQuery / RarityQuery / CMCQuery / FormatQuery / PowerQ
|
|||
|
||||
NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart
|
||||
SetQuery <- ('e'/'set') [:] FlexStringValue
|
||||
OracleQuery <- 'o' [:] RegexString
|
||||
OracleQuery <- 'o' [:] MatcherString
|
||||
|
||||
|
||||
CMCQuery <- ('cmc'/'mv') ws? NumericExpression
|
||||
|
|
@ -42,7 +43,7 @@ ColorEx <- Color / [mc]
|
|||
|
||||
ColorQuery <- [cC] 'olor'? <[iI]?> <[:!]> ColorEx*
|
||||
|
||||
FieldQuery <- String [:] RegexString / String ws? NumericExpression
|
||||
FieldQuery <- String [:] MatcherString / String ws? NumericExpression
|
||||
|
||||
NonDoubleQuoteUnlessEscaped <- '\\\"'. / !["].
|
||||
NonSingleQuoteUnlessEscaped <- "\\\'". / !['].
|
||||
|
|
@ -52,8 +53,14 @@ String <- SingleApostropheString / UnescapedStringListPart+ / ["] <NonDoubleQuot
|
|||
StringValue <- String / [(] StringList [)]
|
||||
StringList <- StringListString (ws? [,] ws? StringListString)*
|
||||
StringListString <- UnescapedStringListPart+
|
||||
GenericQuery <- RegexString
|
||||
RegexString <- String
|
||||
GenericQuery <- MatcherString
|
||||
|
||||
# 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 [)]
|
||||
CompactStringSet <- StringListString ([,+] StringListString)+
|
||||
|
|
@ -63,7 +70,7 @@ NumericOperator <- [=:] / <[><!][=]?>
|
|||
NumericValue <- [0-9]+
|
||||
)");
|
||||
|
||||
std::once_flag init;
|
||||
static std::once_flag init;
|
||||
|
||||
static void setupParserRules()
|
||||
{
|
||||
|
|
@ -261,14 +268,22 @@ static void setupParserRules()
|
|||
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]);
|
||||
return [=](const QString &s) {
|
||||
auto sanitizedTarget = QString(target);
|
||||
sanitizedTarget.replace("\\\"", "\"");
|
||||
sanitizedTarget.replace("\\'", "'");
|
||||
return s.contains(sanitizedTarget, Qt::CaseInsensitive);
|
||||
};
|
||||
auto sanitizedTarget = QString(target);
|
||||
sanitizedTarget.replace("\\\"", "\"");
|
||||
sanitizedTarget.replace("\\'", "'");
|
||||
return [=](const QString &s) { 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 {
|
||||
|
|
|
|||
|
|
@ -9,11 +9,12 @@
|
|||
*
|
||||
* @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)) {
|
||||
qCWarning(SyntaxHelpLog) << "Could not open syntax help file: " << helpFile;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
|
@ -54,9 +55,29 @@ static QTextBrowser *createBrowser()
|
|||
*/
|
||||
QTextBrowser *createSearchSyntaxHelpWindow(QLineEdit *lineEdit)
|
||||
{
|
||||
auto browser = createBrowser();
|
||||
auto browser = createBrowser("theme:help/search.md");
|
||||
QObject::connect(browser, &QTextBrowser::anchorClicked,
|
||||
[lineEdit](const QUrl &link) { lineEdit->setText(link.fragment()); });
|
||||
QObject::connect(lineEdit, &QObject::destroyed, browser, &QTextBrowser::close);
|
||||
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;
|
||||
}
|
||||
|
|
@ -2,8 +2,13 @@
|
|||
#define SEARCH_SYNTAX_HELP_H
|
||||
|
||||
#include <QLineEdit>
|
||||
#include <QLoggingCategory>
|
||||
#include <QTextBrowser>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(SyntaxHelpLog, "syntax_help");
|
||||
|
||||
QTextBrowser *createSearchSyntaxHelpWindow(QLineEdit *lineEdit);
|
||||
|
||||
QTextBrowser *createDeckSearchSyntaxHelpWindow(QLineEdit *lineEdit);
|
||||
|
||||
#endif // SEARCH_SYNTAX_HELP_H
|
||||
|
|
|
|||
|
|
@ -275,7 +275,6 @@ SettingsCache::SettingsCache()
|
|||
visualDeckStorageSortingOrder = settings->value("interface/visualdeckstoragesortingorder", 0).toInt();
|
||||
visualDeckStorageShowFolders = settings->value("interface/visualdeckstorageshowfolders", true).toBool();
|
||||
visualDeckStorageShowTagFilter = settings->value("interface/visualdeckstorageshowtagfilter", true).toBool();
|
||||
visualDeckStorageSearchFolderNames = settings->value("interface/visualdeckstoragesearchfoldernames", true).toBool();
|
||||
visualDeckStorageShowBannerCardComboBox =
|
||||
settings->value("interface/visualdeckstorageshowbannercardcombobox", true).toBool();
|
||||
visualDeckStorageShowTagsOnDeckPreviews =
|
||||
|
|
@ -284,6 +283,7 @@ SettingsCache::SettingsCache()
|
|||
settings->value("interface/visualdeckstoragedrawunusedcoloridentities", true).toBool();
|
||||
visualDeckStorageUnusedColorIdentitiesOpacity =
|
||||
settings->value("interface/visualdeckstorageunusedcoloridentitiesopacity", 15).toInt();
|
||||
visualDeckStorageTooltipType = settings->value("interface/visualdeckstoragetooltiptype", 0).toInt();
|
||||
visualDeckStoragePromptForConversion =
|
||||
settings->value("interface/visualdeckstoragepromptforconversion", true).toBool();
|
||||
visualDeckStorageAlwaysConvert = settings->value("interface/visualdeckstoragealwaysconvert", false).toBool();
|
||||
|
|
@ -734,12 +734,6 @@ void SettingsCache::setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T _showTa
|
|||
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)
|
||||
{
|
||||
visualDeckStorageShowBannerCardComboBox = _showBannerCardComboBox;
|
||||
|
|
@ -778,6 +772,12 @@ void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity(int _visual
|
|||
emit visualDeckStorageUnusedColorIdentitiesOpacityChanged(visualDeckStorageUnusedColorIdentitiesOpacity);
|
||||
}
|
||||
|
||||
void SettingsCache::setVisualDeckStorageTooltipType(int value)
|
||||
{
|
||||
visualDeckStorageTooltipType = value;
|
||||
settings->setValue("interface/visualdeckstoragetooltiptype", visualDeckStorageTooltipType);
|
||||
}
|
||||
|
||||
void SettingsCache::setVisualDeckStoragePromptForConversion(bool _visualDeckStoragePromptForConversion)
|
||||
{
|
||||
visualDeckStoragePromptForConversion = _visualDeckStoragePromptForConversion;
|
||||
|
|
|
|||
|
|
@ -148,10 +148,10 @@ private:
|
|||
bool visualDeckStorageShowBannerCardComboBox;
|
||||
bool visualDeckStorageShowTagsOnDeckPreviews;
|
||||
bool visualDeckStorageShowTagFilter;
|
||||
bool visualDeckStorageSearchFolderNames;
|
||||
int visualDeckStorageCardSize;
|
||||
bool visualDeckStorageDrawUnusedColorIdentities;
|
||||
int visualDeckStorageUnusedColorIdentitiesOpacity;
|
||||
int visualDeckStorageTooltipType;
|
||||
bool visualDeckStoragePromptForConversion;
|
||||
bool visualDeckStorageAlwaysConvert;
|
||||
bool visualDeckStorageInGame;
|
||||
|
|
@ -454,10 +454,6 @@ public:
|
|||
{
|
||||
return visualDeckStorageShowTagFilter;
|
||||
}
|
||||
bool getVisualDeckStorageSearchFolderNames() const
|
||||
{
|
||||
return visualDeckStorageSearchFolderNames;
|
||||
}
|
||||
bool getVisualDeckStorageShowBannerCardComboBox() const
|
||||
{
|
||||
return visualDeckStorageShowBannerCardComboBox;
|
||||
|
|
@ -478,6 +474,10 @@ public:
|
|||
{
|
||||
return visualDeckStorageUnusedColorIdentitiesOpacity;
|
||||
}
|
||||
int getVisualDeckStorageTooltipType() const
|
||||
{
|
||||
return visualDeckStorageTooltipType;
|
||||
}
|
||||
bool getVisualDeckStoragePromptForConversion() const
|
||||
{
|
||||
return visualDeckStoragePromptForConversion;
|
||||
|
|
@ -850,12 +850,12 @@ public slots:
|
|||
void setVisualDeckStorageSortingOrder(int _visualDeckStorageSortingOrder);
|
||||
void setVisualDeckStorageShowFolders(QT_STATE_CHANGED_T value);
|
||||
void setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T _showTags);
|
||||
void setVisualDeckStorageSearchFolderNames(QT_STATE_CHANGED_T value);
|
||||
void setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T _showBannerCardComboBox);
|
||||
void setVisualDeckStorageShowTagsOnDeckPreviews(QT_STATE_CHANGED_T _showTags);
|
||||
void setVisualDeckStorageCardSize(int _visualDeckStorageCardSize);
|
||||
void setVisualDeckStorageDrawUnusedColorIdentities(QT_STATE_CHANGED_T _visualDeckStorageDrawUnusedColorIdentities);
|
||||
void setVisualDeckStorageUnusedColorIdentitiesOpacity(int _visualDeckStorageUnusedColorIdentitiesOpacity);
|
||||
void setVisualDeckStorageTooltipType(int value);
|
||||
void setVisualDeckStoragePromptForConversion(bool _visualDeckStoragePromptForConversion);
|
||||
void setVisualDeckStorageAlwaysConvert(bool _visualDeckStorageAlwaysConvert);
|
||||
void setVisualDeckStorageInGame(QT_STATE_CHANGED_T value);
|
||||
|
|
|
|||
|
|
@ -267,6 +267,13 @@ private:
|
|||
{"DeckViewContainer/loadRemoteButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Remote Deck..."),
|
||||
parseSequenceString("Ctrl+Alt+O"),
|
||||
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"),
|
||||
parseSequenceString("Ctrl+Shift+S"),
|
||||
ShortcutGroup::Game_Lobby)},
|
||||
|
|
@ -274,6 +281,9 @@ private:
|
|||
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Toggle Sideboard Lock"),
|
||||
parseSequenceString("Ctrl+Shift+B"),
|
||||
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)"),
|
||||
parseSequenceString(""),
|
||||
ShortcutGroup::Card_Counters)},
|
||||
|
|
|
|||
|
|
@ -441,7 +441,7 @@ bool DeckList::readElement(QXmlStreamReader *xml)
|
|||
return true;
|
||||
}
|
||||
|
||||
void DeckList::write(QXmlStreamWriter *xml)
|
||||
void DeckList::write(QXmlStreamWriter *xml) const
|
||||
{
|
||||
xml->writeStartElement("cockatrice_deck");
|
||||
xml->writeAttribute("version", "1");
|
||||
|
|
@ -508,7 +508,7 @@ bool DeckList::loadFromString_Native(const QString &nativeString)
|
|||
return loadFromXml(&xml);
|
||||
}
|
||||
|
||||
QString DeckList::writeToString_Native()
|
||||
QString DeckList::writeToString_Native() const
|
||||
{
|
||||
QString result;
|
||||
QXmlStreamWriter xml(&result);
|
||||
|
|
|
|||
|
|
@ -345,10 +345,10 @@ public:
|
|||
}
|
||||
|
||||
bool readElement(QXmlStreamReader *xml);
|
||||
void write(QXmlStreamWriter *xml);
|
||||
void write(QXmlStreamWriter *xml) const;
|
||||
bool loadFromXml(QXmlStreamReader *xml);
|
||||
bool loadFromString_Native(const QString &nativeString);
|
||||
QString writeToString_Native();
|
||||
QString writeToString_Native() const;
|
||||
bool loadFromFile_Native(QIODevice *device);
|
||||
bool saveToFile_Native(QIODevice *device);
|
||||
bool loadFromStream_Plain(QTextStream &stream, bool preserveMetadata);
|
||||
|
|
|
|||
|
|
@ -920,6 +920,8 @@ Server_Player::cmdConcede(const Command_Concede & /*cmd*/, ResponseContainer & /
|
|||
game->removeArrowsRelatedToPlayer(ges, this);
|
||||
game->unattachCards(ges, this);
|
||||
|
||||
playerMutex.lock();
|
||||
|
||||
// Return cards to their rightful owners before conceding the game
|
||||
static const QRegularExpression ownerRegex{"Owner: ?([^\n]+)"};
|
||||
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
|
||||
clearZones();
|
||||
|
||||
|
|
|
|||
|
|
@ -227,9 +227,6 @@ void SettingsCache::setVisualDeckStorageShowFolders(QT_STATE_CHANGED_T /* value
|
|||
void SettingsCache::setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T /* _showTags */)
|
||||
{
|
||||
}
|
||||
void SettingsCache::setVisualDeckStorageSearchFolderNames(QT_STATE_CHANGED_T /* value */)
|
||||
{
|
||||
}
|
||||
void SettingsCache::setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T /* _showBannerCardComboBox */)
|
||||
{
|
||||
}
|
||||
|
|
@ -247,6 +244,9 @@ void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity(
|
|||
int /* _visualDeckStorageUnusedColorIdentitiesOpacity */)
|
||||
{
|
||||
}
|
||||
void SettingsCache::setVisualDeckStorageTooltipType(int /* value */)
|
||||
{
|
||||
}
|
||||
void SettingsCache::setVisualDeckStoragePromptForConversion(bool /* _visualDeckStoragePromptForConversion */)
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@
|
|||
<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="upsidedown" minOccurs="0" maxOccurs="1" />
|
||||
<xs:element type="xs:boolean" name="landscapeOrientation" minOccurs="0" maxOccurs="1" />
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
<xs:element name="cockatrice_carddatabase">
|
||||
|
|
|
|||
|
|
@ -21,8 +21,13 @@ set(oracle_SOURCES
|
|||
../cockatrice/src/game/cards/card_database_manager.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_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_work.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/cockatrice_xml_3.cpp
|
||||
../cockatrice/src/game/cards/card_database_parser/cockatrice_xml_4.cpp
|
||||
|
|
|
|||
|
|
@ -231,9 +231,6 @@ void SettingsCache::setVisualDeckStorageShowFolders(QT_STATE_CHANGED_T /* value
|
|||
void SettingsCache::setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T /* _showTags */)
|
||||
{
|
||||
}
|
||||
void SettingsCache::setVisualDeckStorageSearchFolderNames(QT_STATE_CHANGED_T /* value */)
|
||||
{
|
||||
}
|
||||
void SettingsCache::setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T /* _showBannerCardComboBox */)
|
||||
{
|
||||
}
|
||||
|
|
@ -251,6 +248,9 @@ void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity(
|
|||
int /* _visualDeckStorageUnusedColorIdentitiesOpacity */)
|
||||
{
|
||||
}
|
||||
void SettingsCache::setVisualDeckStorageTooltipType(int /* value */)
|
||||
{
|
||||
}
|
||||
void SettingsCache::setVisualDeckStoragePromptForConversion(bool /* _visualDeckStoragePromptForConversion */)
|
||||
{
|
||||
}
|
||||
|
|
|
|||
1440
webclient/package-lock.json
generated
1440
webclient/package-lock.json
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue