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 \
|
git \
|
||||||
gtest \
|
gtest \
|
||||||
mariadb-libs \
|
mariadb-libs \
|
||||||
|
ninja \
|
||||||
protobuf \
|
protobuf \
|
||||||
qt6-base \
|
qt6-base \
|
||||||
qt6-imageformats \
|
qt6-imageformats \
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ RUN apt-get update && \
|
||||||
libqt5sql5-mysql \
|
libqt5sql5-mysql \
|
||||||
libqt5svg5-dev \
|
libqt5svg5-dev \
|
||||||
libqt5websockets5-dev \
|
libqt5websockets5-dev \
|
||||||
|
ninja-build \
|
||||||
protobuf-compiler \
|
protobuf-compiler \
|
||||||
qt5-image-formats-plugins \
|
qt5-image-formats-plugins \
|
||||||
qtmultimedia5-dev \
|
qtmultimedia5-dev \
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,14 @@ RUN apt-get update && \
|
||||||
libprotobuf-dev \
|
libprotobuf-dev \
|
||||||
libqt6multimedia6 \
|
libqt6multimedia6 \
|
||||||
libqt6sql6-mysql \
|
libqt6sql6-mysql \
|
||||||
qt6-svg-dev \
|
ninja-build \
|
||||||
qt6-websockets-dev \
|
|
||||||
protobuf-compiler \
|
protobuf-compiler \
|
||||||
qt6-image-formats-plugins \
|
qt6-image-formats-plugins \
|
||||||
qt6-l10n-tools \
|
qt6-l10n-tools \
|
||||||
qt6-multimedia-dev \
|
qt6-multimedia-dev \
|
||||||
|
qt6-svg-dev \
|
||||||
qt6-tools-dev \
|
qt6-tools-dev \
|
||||||
qt6-tools-dev-tools \
|
qt6-tools-dev-tools \
|
||||||
|
qt6-websockets-dev \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ RUN dnf install -y \
|
||||||
gcc-c++ \
|
gcc-c++ \
|
||||||
git \
|
git \
|
||||||
mariadb-devel \
|
mariadb-devel \
|
||||||
|
ninja-build \
|
||||||
protobuf-devel \
|
protobuf-devel \
|
||||||
qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||||
qt6-qtimageformats \
|
qt6-qtimageformats \
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ RUN dnf install -y \
|
||||||
gcc-c++ \
|
gcc-c++ \
|
||||||
git \
|
git \
|
||||||
mariadb-devel \
|
mariadb-devel \
|
||||||
|
ninja-build \
|
||||||
protobuf-devel \
|
protobuf-devel \
|
||||||
qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
qt6-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
|
||||||
qt6-qtimageformats \
|
qt6-qtimageformats \
|
||||||
|
|
|
||||||
|
|
@ -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 \
|
libqt6sql6-mysql \
|
||||||
libqt6svg6-dev \
|
libqt6svg6-dev \
|
||||||
libqt6websockets6-dev \
|
libqt6websockets6-dev \
|
||||||
|
ninja-build \
|
||||||
protobuf-compiler \
|
protobuf-compiler \
|
||||||
qt6-image-formats-plugins \
|
qt6-image-formats-plugins \
|
||||||
qt6-l10n-tools \
|
qt6-l10n-tools \
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,14 @@ RUN apt-get update && \
|
||||||
libprotobuf-dev \
|
libprotobuf-dev \
|
||||||
libqt6multimedia6 \
|
libqt6multimedia6 \
|
||||||
libqt6sql6-mysql \
|
libqt6sql6-mysql \
|
||||||
qt6-svg-dev \
|
ninja-build \
|
||||||
qt6-websockets-dev \
|
|
||||||
protobuf-compiler \
|
protobuf-compiler \
|
||||||
qt6-image-formats-plugins \
|
qt6-image-formats-plugins \
|
||||||
qt6-l10n-tools \
|
qt6-l10n-tools \
|
||||||
qt6-multimedia-dev \
|
qt6-multimedia-dev \
|
||||||
|
qt6-svg-dev \
|
||||||
qt6-tools-dev \
|
qt6-tools-dev \
|
||||||
qt6-tools-dev-tools \
|
qt6-tools-dev-tools \
|
||||||
|
qt6-websockets-dev \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,8 @@
|
||||||
# --debug or --release sets the build type ie CMAKE_BUILD_TYPE
|
# --debug or --release sets the build type ie CMAKE_BUILD_TYPE
|
||||||
# --ccache [<size>] uses ccache and shows stats, optionally provide size
|
# --ccache [<size>] uses ccache and shows stats, optionally provide size
|
||||||
# --dir <dir> sets the name of the build dir, default is "build"
|
# --dir <dir> sets the name of the build dir, default is "build"
|
||||||
# --parallel <core count> sets how many cores cmake should build with in parallel
|
# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_TEST USE_CCACHE CCACHE_SIZE BUILD_DIR CMAKE_GENERATOR
|
||||||
# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_TEST USE_CCACHE CCACHE_SIZE BUILD_DIR PARALLEL_COUNT
|
# (correspond to args: --debug/--release --install --package <package type> --suffix <suffix> --server --test --ccache <ccache_size> --dir <dir>)
|
||||||
# (correspond to args: --debug/--release --install --package <package type> --suffix <suffix> --server --test --ccache <ccache_size> --dir <dir> --parallel <core_count>)
|
|
||||||
# exitcode: 1 for failure, 3 for invalid arguments
|
# exitcode: 1 for failure, 3 for invalid arguments
|
||||||
|
|
||||||
# Read arguments
|
# Read arguments
|
||||||
|
|
@ -76,15 +75,6 @@ while [[ $# != 0 ]]; do
|
||||||
BUILD_DIR="$1"
|
BUILD_DIR="$1"
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
'--parallel')
|
|
||||||
shift
|
|
||||||
if [[ $# == 0 ]]; then
|
|
||||||
echo "::error file=$0::--parallel expects an argument"
|
|
||||||
exit 3
|
|
||||||
fi
|
|
||||||
PARALLEL_COUNT="$1"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
*)
|
*)
|
||||||
echo "::error file=$0::unrecognized option: $1"
|
echo "::error file=$0::unrecognized option: $1"
|
||||||
exit 3
|
exit 3
|
||||||
|
|
@ -126,16 +116,6 @@ fi
|
||||||
|
|
||||||
# Add cmake --build flags
|
# Add cmake --build flags
|
||||||
buildflags=(--config "$BUILDTYPE")
|
buildflags=(--config "$BUILDTYPE")
|
||||||
if [[ $PARALLEL_COUNT ]]; then
|
|
||||||
if [[ $(cmake --build /not_a_dir --parallel 2>&1 | head -1) =~ parallel ]]; then
|
|
||||||
# workaround for bionic having an old cmake
|
|
||||||
echo "this version of cmake does not support --parallel, using native build tool -j instead"
|
|
||||||
buildflags+=(-- -j "$PARALLEL_COUNT")
|
|
||||||
# note, no normal build flags should be added after this
|
|
||||||
else
|
|
||||||
buildflags+=(--parallel "$PARALLEL_COUNT")
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
function ccachestatsverbose() {
|
function ccachestatsverbose() {
|
||||||
# note, verbose only works on newer ccache, discard the error
|
# note, verbose only works on newer ccache, discard the error
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,9 @@ function RUN ()
|
||||||
args+=(--mount "type=bind,source=$CCACHE_DIR,target=/.ccache")
|
args+=(--mount "type=bind,source=$CCACHE_DIR,target=/.ccache")
|
||||||
args+=(--env "CCACHE_DIR=/.ccache")
|
args+=(--env "CCACHE_DIR=/.ccache")
|
||||||
fi
|
fi
|
||||||
|
if [[ -n "$CMAKE_GENERATOR" ]]; then
|
||||||
|
args+=(--env "CMAKE_GENERATOR=$CMAKE_GENERATOR")
|
||||||
|
fi
|
||||||
docker run "${args[@]}" $RUN_ARGS "$IMAGE_NAME" bash "$BUILD_SCRIPT" $RUN_OPTS "$@"
|
docker run "${args[@]}" $RUN_ARGS "$IMAGE_NAME" bash "$BUILD_SCRIPT" $RUN_OPTS "$@"
|
||||||
return $?
|
return $?
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ Available pre-compiled binaries for installation:
|
||||||
<b>Linux</b>
|
<b>Linux</b>
|
||||||
• <kbd>Ubuntu 24.04 LTS</kbd> <sub><i>Noble Numbat</i></sub>
|
• <kbd>Ubuntu 24.04 LTS</kbd> <sub><i>Noble Numbat</i></sub>
|
||||||
• <kbd>Ubuntu 22.04 LTS</kbd> <sub><i>Jammy Jellyfish</i></sub>
|
• <kbd>Ubuntu 22.04 LTS</kbd> <sub><i>Jammy Jellyfish</i></sub>
|
||||||
• <kbd>Ubuntu 20.04 LTS</kbd> <sub><i>Focal Fossa</i></sub>
|
|
||||||
• <kbd>Debian 12</kbd> <sub><i>Bookworm</i></sub>
|
• <kbd>Debian 12</kbd> <sub><i>Bookworm</i></sub>
|
||||||
• <kbd>Debian 11</kbd> <sub><i>Bullseye</i></sub>
|
• <kbd>Debian 11</kbd> <sub><i>Bullseye</i></sub>
|
||||||
• <kbd>Fedora 42</kbd>
|
• <kbd>Fedora 42</kbd>
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,5 @@ Last changes are based on commit {{ .commit }}.
|
||||||
*This PR is automatically generated and updated by the workflow at `.github/workflows/translations-push.yml`. Review [action runs][2].*<br>
|
*This PR is automatically generated and updated by the workflow at `.github/workflows/translations-push.yml`. Review [action runs][2].*<br>
|
||||||
*After merging, all changes to the source language are available for translation at [Transifex][1] shortly.*
|
*After merging, all changes to the source language are available for translation at [Transifex][1] shortly.*
|
||||||
|
|
||||||
[1]: https://app.transifex.com/cockatrice/cockatrice/
|
[1]: https://explore.transifex.com/cockatrice/cockatrice/
|
||||||
[2]: https://github.com/Cockatrice/Cockatrice/actions/workflows/translations-push.yml?query=branch%3Amaster
|
[2]: https://github.com/Cockatrice/Cockatrice/actions/workflows/translations-push.yml?query=branch%3Amaster
|
||||||
|
|
|
||||||
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
|
<!-- READ THIS BEFORE POSTING
|
||||||
Go to "Help → View Debug Log" in Cockatrice and copy all information at the
|
In Cockatrice, go to "Help" → "View Debug Log" and copy all information displayed at the
|
||||||
top (above the separation line) below "System Information" in this ticket!
|
top (above the separation line "----"), to below "System Information" section in this ticket!
|
||||||
If you can't start Cockatrice to access these details, make
|
If you can't start Cockatrice to access these details, make
|
||||||
sure to post your OS and the file name of the setup binary instead. -->
|
sure to post your OS and the file name of the setup binary instead.
|
||||||
|
-->
|
||||||
|
|
||||||
**System Information:**
|
**System Information:**
|
||||||
|
<!-- Read the hint above on where to find the important information to provide here! -->
|
||||||
|
|
||||||
|
|
||||||
|
<details><summary>Debug Log:</summary>
|
||||||
|
<!--
|
||||||
|
In Cockatrice, go to "Help" → "View Debug Log", click the "Copy to clickboard" button and paste the output here.
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
_______________________________________________________________________________________
|
_______________________________________________________________________________________
|
||||||
|
|
||||||
<!-- Explain your issue in detail here! Please attach screenshots if possible. -->
|
<!-- Explain your issue in detail here! Please attach screenshots if possible. -->
|
||||||
|
|
@ -26,7 +36,7 @@ ________________________________________________________________________________
|
||||||
|
|
||||||
_______________________________________________________________________________________
|
_______________________________________________________________________________________
|
||||||
|
|
||||||
<!-- Describe the sequence of actions needed to experience the bug -->
|
<!-- Describe the sequence of actions needed to experience the bug. -->
|
||||||
|
|
||||||
**Steps to reproduce:**
|
**Steps to reproduce:**
|
||||||
- Do A
|
- Do A
|
||||||
|
|
|
||||||
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
|
url: https://discord.gg/3Z9yzmA
|
||||||
about: Need help with using the client? Want to find some games? Try the Discord server!
|
about: Need help with using the client? Want to find some games? Try the Discord server!
|
||||||
- name: 🌐 Translations (Help improve the localization of the app)
|
- name: 🌐 Translations (Help improve the localization of the app)
|
||||||
url: https://www.transifex.com/cockatrice/cockatrice/
|
url: https://explore.transifex.com/cockatrice/cockatrice/
|
||||||
# it is not possible to add a link to the wiki to this description
|
# it is not possible to add a link to the wiki to this description
|
||||||
about: For more information and guidance check our Translation FAQ on our wiki!
|
about: For more information and guidance check our Translation FAQ on our wiki!
|
||||||
|
|
|
||||||
147
.github/workflows/desktop-build.yml
vendored
147
.github/workflows/desktop-build.yml
vendored
|
|
@ -110,11 +110,6 @@ jobs:
|
||||||
version: 42
|
version: 42
|
||||||
package: RPM
|
package: RPM
|
||||||
|
|
||||||
- distro: Ubuntu
|
|
||||||
version: 20.04
|
|
||||||
package: DEB
|
|
||||||
test: skip # Ubuntu 20.04 has a broken Qt for debug builds
|
|
||||||
|
|
||||||
- distro: Ubuntu
|
- distro: Ubuntu
|
||||||
version: 22.04
|
version: 22.04
|
||||||
package: DEB
|
package: DEB
|
||||||
|
|
@ -130,29 +125,25 @@ jobs:
|
||||||
continue-on-error: ${{matrix.allow-failure == 'yes'}}
|
continue-on-error: ${{matrix.allow-failure == 'yes'}}
|
||||||
env:
|
env:
|
||||||
NAME: ${{matrix.distro}}${{matrix.version}}
|
NAME: ${{matrix.distro}}${{matrix.version}}
|
||||||
CACHE: /tmp/${{matrix.distro}}${{matrix.version}}-cache # ${{runner.temp}} does not work?
|
CACHE: ${{github.workspace}}/.cache/${{matrix.distro}}${{matrix.version}} # directory for caching docker image and ccache
|
||||||
# Cache size over the entire repo is 10Gi:
|
# Cache size over the entire repo is 10Gi:
|
||||||
# https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
# https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
|
||||||
CCACHE_SIZE: 500M
|
CCACHE_SIZE: 500M
|
||||||
|
CMAKE_GENERATOR: 'Ninja'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Generate cache timestamp
|
- name: Restore compiler cache (ccache)
|
||||||
id: cache_timestamp
|
id: ccache_restore
|
||||||
shell: bash
|
uses: actions/cache/restore@v4
|
||||||
run: echo "timestamp=$(date -u '+%Y%m%d%H%M%S')" >>"$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Restore cache
|
|
||||||
uses: actions/cache@v4
|
|
||||||
env:
|
env:
|
||||||
timestamp: ${{steps.cache_timestamp.outputs.timestamp}}
|
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||||
with:
|
with:
|
||||||
path: ${{env.CACHE}}
|
path: ${{env.CACHE}}
|
||||||
key: docker-${{matrix.distro}}${{matrix.version}}-cache-${{env.timestamp}}
|
key: ccache-${{matrix.distro}}${{matrix.version}}-${{env.BRANCH_NAME}}
|
||||||
restore-keys: |
|
restore-keys: ccache-${{matrix.distro}}${{matrix.version}}-
|
||||||
docker-${{matrix.distro}}${{matrix.version}}-cache-
|
|
||||||
|
|
||||||
- name: Build ${{matrix.distro}} ${{matrix.version}} Docker image
|
- name: Build ${{matrix.distro}} ${{matrix.version}} Docker image
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
@ -161,9 +152,11 @@ jobs:
|
||||||
- name: Build debug and test
|
- name: Build debug and test
|
||||||
if: matrix.test != 'skip'
|
if: matrix.test != 'skip'
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}'
|
||||||
run: |
|
run: |
|
||||||
source .ci/docker.sh
|
source .ci/docker.sh
|
||||||
RUN --server --debug --test --ccache "$CCACHE_SIZE" --parallel 4
|
RUN --server --debug --test --ccache "$CCACHE_SIZE"
|
||||||
|
|
||||||
- name: Build release package
|
- name: Build release package
|
||||||
id: build
|
id: build
|
||||||
|
|
@ -172,14 +165,23 @@ jobs:
|
||||||
env:
|
env:
|
||||||
BUILD_DIR: build
|
BUILD_DIR: build
|
||||||
SUFFIX: '-${{matrix.distro}}${{matrix.version}}'
|
SUFFIX: '-${{matrix.distro}}${{matrix.version}}'
|
||||||
type: '${{matrix.package}}'
|
package: '${{matrix.package}}'
|
||||||
|
CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}'
|
||||||
run: |
|
run: |
|
||||||
source .ci/docker.sh
|
source .ci/docker.sh
|
||||||
RUN --server --release --package "$type" --dir "$BUILD_DIR" \
|
RUN --server --release --package "$package" --dir "$BUILD_DIR" \
|
||||||
--ccache "$CCACHE_SIZE" --parallel 4
|
--ccache "$CCACHE_SIZE"
|
||||||
.ci/name_build.sh
|
.ci/name_build.sh
|
||||||
|
|
||||||
|
- name: Save compiler cache (ccache)
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
|
uses: actions/cache/save@v4
|
||||||
|
with:
|
||||||
|
path: ${{env.CACHE}}
|
||||||
|
key: ${{ steps.ccache_restore.outputs.cache-primary-key }}
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
|
id: upload_artifact
|
||||||
if: matrix.package != 'skip'
|
if: matrix.package != 'skip'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|
@ -188,6 +190,7 @@ jobs:
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload to release
|
- name: Upload to release
|
||||||
|
id: upload_release
|
||||||
if: matrix.package != 'skip' && needs.configure.outputs.tag != null
|
if: matrix.package != 'skip' && needs.configure.outputs.tag != null
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
|
|
@ -197,6 +200,20 @@ jobs:
|
||||||
asset_name: ${{steps.build.outputs.name}}
|
asset_name: ${{steps.build.outputs.name}}
|
||||||
run: gh release upload "$tag_name" "$asset_path#$asset_name"
|
run: gh release upload "$tag_name" "$asset_path#$asset_name"
|
||||||
|
|
||||||
|
- name: Attest binary provenance
|
||||||
|
id: attestation
|
||||||
|
if: steps.upload_release.outcome == 'success'
|
||||||
|
uses: actions/attest-build-provenance@v2
|
||||||
|
with:
|
||||||
|
subject-path: ${{steps.build.outputs.path}}
|
||||||
|
subject-name: ${{steps.build.outputs.name}}
|
||||||
|
subject-digest: sha256:${{ steps.upload_artifact.outputs.artifact-digest }}
|
||||||
|
|
||||||
|
- name: Verify binary attestation
|
||||||
|
if: steps.attestation.outcome == 'success'
|
||||||
|
shell: bash
|
||||||
|
run: gh attestation verify ${{steps.build.outputs.path}} -R Cockatrice/Cockatrice
|
||||||
|
|
||||||
build-macos:
|
build-macos:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
@ -207,7 +224,6 @@ jobs:
|
||||||
os: macos-13
|
os: macos-13
|
||||||
xcode: "14.3.1"
|
xcode: "14.3.1"
|
||||||
type: Release
|
type: Release
|
||||||
core_count: 4
|
|
||||||
make_package: 1
|
make_package: 1
|
||||||
|
|
||||||
- target: 14
|
- target: 14
|
||||||
|
|
@ -215,7 +231,6 @@ jobs:
|
||||||
os: macos-14
|
os: macos-14
|
||||||
xcode: "15.4"
|
xcode: "15.4"
|
||||||
type: Release
|
type: Release
|
||||||
core_count: 3
|
|
||||||
make_package: 1
|
make_package: 1
|
||||||
|
|
||||||
- target: 15
|
- target: 15
|
||||||
|
|
@ -223,7 +238,6 @@ jobs:
|
||||||
os: macos-15
|
os: macos-15
|
||||||
xcode: "16.2"
|
xcode: "16.2"
|
||||||
type: Release
|
type: Release
|
||||||
core_count: 3
|
|
||||||
make_package: 1
|
make_package: 1
|
||||||
|
|
||||||
- target: 15
|
- target: 15
|
||||||
|
|
@ -231,15 +245,17 @@ jobs:
|
||||||
os: macos-15
|
os: macos-15
|
||||||
xcode: "16.2"
|
xcode: "16.2"
|
||||||
type: Debug
|
type: Debug
|
||||||
core_count: 3
|
|
||||||
|
|
||||||
name: macOS ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
|
name: macOS ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
|
||||||
needs: configure
|
needs: configure
|
||||||
runs-on: ${{matrix.os}}
|
runs-on: ${{matrix.os}}
|
||||||
continue-on-error: ${{matrix.allow-failure == 'yes'}}
|
continue-on-error: ${{matrix.allow-failure == 'yes'}}
|
||||||
env:
|
env:
|
||||||
|
CCACHE_DIR: ${{github.workspace}}/.ccache/${{matrix.os}}-${{matrix.type}}
|
||||||
|
CCACHE_SIZE: 500M
|
||||||
DEVELOPER_DIR:
|
DEVELOPER_DIR:
|
||||||
/Applications/Xcode_${{matrix.xcode}}.app/Contents/Developer
|
/Applications/Xcode_${{matrix.xcode}}.app/Contents/Developer
|
||||||
|
CMAKE_GENERATOR: 'Ninja'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|
@ -253,23 +269,30 @@ jobs:
|
||||||
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
|
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
|
||||||
run: |
|
run: |
|
||||||
brew update
|
brew update
|
||||||
brew install protobuf qt --force-bottle
|
brew install ccache protobuf qt --force-bottle
|
||||||
|
|
||||||
|
- name: Restore compiler cache (ccache)
|
||||||
|
id: ccache_restore
|
||||||
|
uses: actions/cache/restore@v4
|
||||||
|
env:
|
||||||
|
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||||
|
with:
|
||||||
|
path: ${{env.CCACHE_DIR}}
|
||||||
|
key: ccache-${{matrix.os}}-${{matrix.type}}-${{env.BRANCH_NAME}}
|
||||||
|
restore-keys: ccache-${{matrix.os}}-${{matrix.type}}-
|
||||||
|
|
||||||
- name: Build & Sign on Xcode ${{matrix.xcode}}
|
- name: Build & Sign on Xcode ${{matrix.xcode}}
|
||||||
shell: bash
|
shell: bash
|
||||||
id: build
|
id: build
|
||||||
env:
|
env:
|
||||||
BUILDTYPE: '${{matrix.type}}'
|
BUILDTYPE: '${{matrix.type}}'
|
||||||
MAKE_TEST: 1
|
|
||||||
MAKE_PACKAGE: '${{matrix.make_package}}'
|
MAKE_PACKAGE: '${{matrix.make_package}}'
|
||||||
PACKAGE_SUFFIX: '-macOS${{matrix.target}}_${{matrix.soc}}'
|
PACKAGE_SUFFIX: '-macOS${{matrix.target}}_${{matrix.soc}}'
|
||||||
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
|
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
|
||||||
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
|
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
|
||||||
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
|
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
|
||||||
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
|
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
|
||||||
# macOS runner have 3 cores usually - only the macos-13 image has 4:
|
CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}'
|
||||||
# https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories
|
|
||||||
# https://github.com/actions/runner-images?tab=readme-ov-file#available-images
|
|
||||||
run: |
|
run: |
|
||||||
if [[ -n "$MACOS_CERTIFICATE_NAME" ]]
|
if [[ -n "$MACOS_CERTIFICATE_NAME" ]]
|
||||||
then
|
then
|
||||||
|
|
@ -281,10 +304,17 @@ jobs:
|
||||||
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
|
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
|
||||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain
|
||||||
fi
|
fi
|
||||||
.ci/compile.sh --server --parallel ${{matrix.core_count}}
|
.ci/compile.sh --server --test --ccache "$CCACHE_SIZE"
|
||||||
|
|
||||||
|
- name: Save compiler cache (ccache)
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
|
uses: actions/cache/save@v4
|
||||||
|
with:
|
||||||
|
path: ${{env.CCACHE_DIR}}
|
||||||
|
key: ${{ steps.ccache_restore.outputs.cache-primary-key }}
|
||||||
|
|
||||||
- name: Sign app bundle
|
- name: Sign app bundle
|
||||||
if: matrix.make_package
|
if: matrix.make_package && (github.ref == 'refs/heads/master' || needs.configure.outputs.tag != null)
|
||||||
env:
|
env:
|
||||||
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
|
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
|
||||||
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
|
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
|
||||||
|
|
@ -296,7 +326,7 @@ jobs:
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Notarize app bundle
|
- name: Notarize app bundle
|
||||||
if: matrix.make_package
|
if: matrix.make_package && (github.ref == 'refs/heads/master' || needs.configure.outputs.tag != null)
|
||||||
env:
|
env:
|
||||||
MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
|
MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
|
||||||
MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
|
MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
|
||||||
|
|
@ -328,6 +358,7 @@ jobs:
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
|
id: upload_artifact
|
||||||
if: matrix.make_package
|
if: matrix.make_package
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|
@ -336,7 +367,8 @@ jobs:
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload to release
|
- name: Upload to release
|
||||||
if: matrix.package != 'skip' && needs.configure.outputs.tag != null
|
id: upload_release
|
||||||
|
if: matrix.make_package && needs.configure.outputs.tag != null
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{github.token}}
|
GH_TOKEN: ${{github.token}}
|
||||||
|
|
@ -345,6 +377,20 @@ jobs:
|
||||||
asset_name: ${{steps.build.outputs.name}}
|
asset_name: ${{steps.build.outputs.name}}
|
||||||
run: gh release upload "$tag_name" "$asset_path#$asset_name"
|
run: gh release upload "$tag_name" "$asset_path#$asset_name"
|
||||||
|
|
||||||
|
- name: Attest binary provenance
|
||||||
|
id: attestation
|
||||||
|
if: steps.upload_release.outcome == 'success'
|
||||||
|
uses: actions/attest-build-provenance@v2
|
||||||
|
with:
|
||||||
|
subject-path: ${{steps.build.outputs.path}}
|
||||||
|
subject-name: ${{steps.build.outputs.name}}
|
||||||
|
subject-digest: sha256:${{ steps.upload_artifact.outputs.artifact-digest }}
|
||||||
|
|
||||||
|
- name: Verify binary attestation
|
||||||
|
if: steps.attestation.outcome == 'success'
|
||||||
|
shell: bash
|
||||||
|
run: gh attestation verify ${{steps.build.outputs.path}} -R Cockatrice/Cockatrice
|
||||||
|
|
||||||
build-windows:
|
build-windows:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
@ -387,16 +433,11 @@ jobs:
|
||||||
tools: ${{matrix.qt_tools}}
|
tools: ${{matrix.qt_tools}}
|
||||||
modules: ${{matrix.qt_modules}}
|
modules: ${{matrix.qt_modules}}
|
||||||
|
|
||||||
# TODO: re-enable when https://github.com/lukka/run-vcpkg/issues/243 is fixed
|
- name: Setup vcpkg cache
|
||||||
- if: false
|
id: vcpkg-cache
|
||||||
name: Run vcpkg (disabled)
|
uses: TAServers/vcpkg-cache@v3
|
||||||
uses: lukka/run-vcpkg@v11
|
|
||||||
with:
|
with:
|
||||||
runVcpkgInstall: true
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
doNotCache: false
|
|
||||||
env:
|
|
||||||
VCPKG_DEFAULT_TRIPLET: 'x64-windows'
|
|
||||||
VCPKG_DISABLE_METRICS: 1
|
|
||||||
|
|
||||||
- name: Build Cockatrice
|
- name: Build Cockatrice
|
||||||
id: build
|
id: build
|
||||||
|
|
@ -406,11 +447,14 @@ jobs:
|
||||||
CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}'
|
CMAKE_GENERATOR: '${{env.CMAKE_GENERATOR}}'
|
||||||
CMAKE_GENERATOR_PLATFORM: 'x64'
|
CMAKE_GENERATOR_PLATFORM: 'x64'
|
||||||
QTDIR: '${{github.workspace}}\Qt\${{matrix.qt_version}}\win64_${{matrix.qt_arch}}'
|
QTDIR: '${{github.workspace}}\Qt\${{matrix.qt_version}}\win64_${{matrix.qt_arch}}'
|
||||||
|
VCPKG_DISABLE_METRICS: 1
|
||||||
|
VCPKG_BINARY_SOURCES: 'clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite'
|
||||||
# No need for --parallel flag, MTT is added in the compile script to let cmake/msbuild manage core count,
|
# No need for --parallel flag, MTT is added in the compile script to let cmake/msbuild manage core count,
|
||||||
# project and process parallelism: https://devblogs.microsoft.com/cppblog/improved-parallelism-in-msbuild/
|
# project and process parallelism: https://devblogs.microsoft.com/cppblog/improved-parallelism-in-msbuild/
|
||||||
run: .ci/compile.sh --server --release --test --package
|
run: .ci/compile.sh --server --release --test --package
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
|
id: upload_artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Windows${{matrix.target}}-installer
|
name: Windows${{matrix.target}}-installer
|
||||||
|
|
@ -427,7 +471,8 @@ jobs:
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload to release
|
- name: Upload to release
|
||||||
if: matrix.package != 'skip' && needs.configure.outputs.tag != null
|
id: upload_release
|
||||||
|
if: needs.configure.outputs.tag != null
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{github.token}}
|
GH_TOKEN: ${{github.token}}
|
||||||
|
|
@ -435,3 +480,17 @@ jobs:
|
||||||
asset_path: ${{steps.build.outputs.path}}
|
asset_path: ${{steps.build.outputs.path}}
|
||||||
asset_name: ${{steps.build.outputs.name}}
|
asset_name: ${{steps.build.outputs.name}}
|
||||||
run: gh release upload "$tag_name" "$asset_path#$asset_name"
|
run: gh release upload "$tag_name" "$asset_path#$asset_name"
|
||||||
|
|
||||||
|
- name: Attest binary provenance
|
||||||
|
id: attestation
|
||||||
|
if: steps.upload_release.outcome == 'success'
|
||||||
|
uses: actions/attest-build-provenance@v2
|
||||||
|
with:
|
||||||
|
subject-path: ${{steps.build.outputs.path}}
|
||||||
|
subject-name: ${{steps.build.outputs.name}}
|
||||||
|
subject-digest: sha256:${{ steps.upload_artifact.outputs.artifact-digest }}
|
||||||
|
|
||||||
|
- name: Verify binary attestation
|
||||||
|
if: steps.attestation.outcome == 'success'
|
||||||
|
shell: bash
|
||||||
|
run: gh attestation verify ${{steps.build.outputs.path}} -R Cockatrice/Cockatrice
|
||||||
|
|
|
||||||
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>
|
*This PR is automatically generated and updated by the workflow at `.github/workflows/translations-pull.yml`. Review [action runs][2].*<br>
|
||||||
*After merging, all new languages and translations are available in the next build.*
|
*After merging, all new languages and translations are available in the next build.*
|
||||||
|
|
||||||
[1]: https://app.transifex.com/cockatrice/cockatrice/
|
[1]: https://explore.transifex.com/cockatrice/cockatrice/
|
||||||
[2]: https://github.com/Cockatrice/Cockatrice/actions/workflows/translations-pull.yml?query=branch%3Amaster
|
[2]: https://github.com/Cockatrice/Cockatrice/actions/workflows/translations-pull.yml?query=branch%3Amaster
|
||||||
labels: |
|
labels: |
|
||||||
CI
|
CI
|
||||||
|
|
|
||||||
|
|
@ -86,9 +86,9 @@ Cockatrice tries to use the [Google Developer Documentation Style Guide](https:/
|
||||||
<sub><i>Made with <a href="https://contrib.rocks">contrib.rocks</a></i></sub>
|
<sub><i>Made with <a href="https://contrib.rocks">contrib.rocks</a></i></sub>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Translations [](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>
|
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/line_edit_completer.cpp
|
||||||
src/client/ui/phases_toolbar.cpp
|
src/client/ui/phases_toolbar.cpp
|
||||||
src/client/ui/picture_loader/picture_loader.cpp
|
src/client/ui/picture_loader/picture_loader.cpp
|
||||||
|
src/client/ui/picture_loader/picture_loader_request_status_display_widget.cpp
|
||||||
|
src/client/ui/picture_loader/picture_loader_status_bar.cpp
|
||||||
src/client/ui/picture_loader/picture_loader_worker.cpp
|
src/client/ui/picture_loader/picture_loader_worker.cpp
|
||||||
|
src/client/ui/picture_loader/picture_loader_worker_work.cpp
|
||||||
src/client/ui/picture_loader/picture_to_load.cpp
|
src/client/ui/picture_loader/picture_to_load.cpp
|
||||||
src/client/ui/pixel_map_generator.cpp
|
src/client/ui/pixel_map_generator.cpp
|
||||||
src/client/ui/theme_manager.cpp
|
src/client/ui/theme_manager.cpp
|
||||||
|
|
@ -190,6 +193,7 @@ set(cockatrice_SOURCES
|
||||||
src/game/cards/card_search_model.cpp
|
src/game/cards/card_search_model.cpp
|
||||||
src/game/deckview/deck_view.cpp
|
src/game/deckview/deck_view.cpp
|
||||||
src/game/deckview/deck_view_container.cpp
|
src/game/deckview/deck_view_container.cpp
|
||||||
|
src/game/filters/deck_filter_string.cpp
|
||||||
src/game/filters/filter_builder.cpp
|
src/game/filters/filter_builder.cpp
|
||||||
src/game/filters/filter_card.cpp
|
src/game/filters/filter_card.cpp
|
||||||
src/game/filters/filter_string.cpp
|
src/game/filters/filter_string.cpp
|
||||||
|
|
|
||||||
|
|
@ -379,5 +379,6 @@
|
||||||
<file>resources/tips/tips_of_the_day.xml</file>
|
<file>resources/tips/tips_of_the_day.xml</file>
|
||||||
|
|
||||||
<file>resources/help/search.md</file>
|
<file>resources/help/search.md</file>
|
||||||
|
<file>resources/help/deck_search.md</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|
|
||||||
|
|
@ -60,4 +60,6 @@
|
||||||
|
|
||||||
# pixel_map_generator = false
|
# pixel_map_generator = false
|
||||||
|
|
||||||
|
# deck_filter_string = false
|
||||||
# filter_string = false
|
# filter_string = false
|
||||||
|
# syntax_help = false
|
||||||
|
|
|
||||||
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>
|
<dt>Grouping:</dt>
|
||||||
<dd><a href="#t:angel -(angel or c:w)">t:angel -(angel or c:w)</a> <small>(Any angel that doesn't have angel in its name and isn't white)</small></dd>
|
<dd><a href="#t:angel -(angel or c:w)">t:angel -(angel or c:w)</a> <small>(Any angel that doesn't have angel in its name and isn't white)</small></dd>
|
||||||
|
|
||||||
|
<dt>Regular Expression:</dt>
|
||||||
|
<dd>[/^fell/](#/^fell/) <small>(Any card name that begins with "fell")</small></dd>
|
||||||
|
<dd>[o:/counter target .* spell/](#o:/counter target .* spell/) <small>(Any card text with "counter target *something* spell")</small></dd>
|
||||||
|
<dd>[o:/for each .* and\/or .*/](#o:/for each .* and\/or .*/) <small>(/'s can be escaped with a \)</small></dd>
|
||||||
|
|
||||||
</dl>
|
</dl>
|
||||||
|
|
|
||||||
|
|
@ -1538,7 +1538,7 @@ void TabGame::createPlayAreaWidget(bool bReplay)
|
||||||
scene = new GameScene(phasesToolbar, this);
|
scene = new GameScene(phasesToolbar, this);
|
||||||
gameView = new GameView(scene);
|
gameView = new GameView(scene);
|
||||||
|
|
||||||
gamePlayAreaVBox = new QVBoxLayout;
|
auto gamePlayAreaVBox = new QVBoxLayout;
|
||||||
gamePlayAreaVBox->setContentsMargins(0, 0, 0, 0);
|
gamePlayAreaVBox->setContentsMargins(0, 0, 0, 0);
|
||||||
gamePlayAreaVBox->addWidget(gameView);
|
gamePlayAreaVBox->addWidget(gameView);
|
||||||
|
|
||||||
|
|
@ -1594,12 +1594,12 @@ void TabGame::createReplayDock()
|
||||||
connect(replayFastForwardButton, &QToolButton::toggled, this, &TabGame::replayFastForwardButtonToggled);
|
connect(replayFastForwardButton, &QToolButton::toggled, this, &TabGame::replayFastForwardButtonToggled);
|
||||||
|
|
||||||
// putting everything together
|
// putting everything together
|
||||||
replayControlLayout = new QHBoxLayout;
|
auto replayControlLayout = new QHBoxLayout;
|
||||||
replayControlLayout->addWidget(timelineWidget, 10);
|
replayControlLayout->addWidget(timelineWidget, 10);
|
||||||
replayControlLayout->addWidget(replayPlayButton);
|
replayControlLayout->addWidget(replayPlayButton);
|
||||||
replayControlLayout->addWidget(replayFastForwardButton);
|
replayControlLayout->addWidget(replayFastForwardButton);
|
||||||
|
|
||||||
replayControlWidget = new QWidget();
|
auto replayControlWidget = new QWidget();
|
||||||
replayControlWidget->setObjectName("replayControlWidget");
|
replayControlWidget->setObjectName("replayControlWidget");
|
||||||
replayControlWidget->setLayout(replayControlLayout);
|
replayControlWidget->setLayout(replayControlLayout);
|
||||||
|
|
||||||
|
|
@ -1635,13 +1635,13 @@ void TabGame::createCardInfoDock(bool bReplay)
|
||||||
Q_UNUSED(bReplay);
|
Q_UNUSED(bReplay);
|
||||||
|
|
||||||
cardInfoFrameWidget = new CardInfoFrameWidget();
|
cardInfoFrameWidget = new CardInfoFrameWidget();
|
||||||
cardHInfoLayout = new QHBoxLayout;
|
auto cardHInfoLayout = new QHBoxLayout;
|
||||||
cardVInfoLayout = new QVBoxLayout;
|
auto cardVInfoLayout = new QVBoxLayout;
|
||||||
cardVInfoLayout->setContentsMargins(0, 0, 0, 0);
|
cardVInfoLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
cardVInfoLayout->addWidget(cardInfoFrameWidget);
|
cardVInfoLayout->addWidget(cardInfoFrameWidget);
|
||||||
cardVInfoLayout->addLayout(cardHInfoLayout);
|
cardVInfoLayout->addLayout(cardHInfoLayout);
|
||||||
|
|
||||||
cardBoxLayoutWidget = new QWidget;
|
auto cardBoxLayoutWidget = new QWidget;
|
||||||
cardBoxLayoutWidget->setLayout(cardVInfoLayout);
|
cardBoxLayoutWidget->setLayout(cardVInfoLayout);
|
||||||
|
|
||||||
cardInfoDock = new QDockWidget(this);
|
cardInfoDock = new QDockWidget(this);
|
||||||
|
|
@ -1679,6 +1679,22 @@ void TabGame::createPlayerListDock(bool bReplay)
|
||||||
|
|
||||||
void TabGame::createMessageDock(bool bReplay)
|
void TabGame::createMessageDock(bool bReplay)
|
||||||
{
|
{
|
||||||
|
auto messageLogLayout = new QVBoxLayout;
|
||||||
|
messageLogLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
|
||||||
|
// clock
|
||||||
|
if (!bReplay) {
|
||||||
|
timeElapsedLabel = new QLabel;
|
||||||
|
timeElapsedLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
gameTimer = new QTimer(this);
|
||||||
|
gameTimer->setInterval(1000);
|
||||||
|
connect(gameTimer, &QTimer::timeout, this, &TabGame::incrementGameTime);
|
||||||
|
gameTimer->start();
|
||||||
|
|
||||||
|
messageLogLayout->addWidget(timeElapsedLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// message log
|
||||||
messageLog = new MessageLogWidget(tabSupervisor, this);
|
messageLog = new MessageLogWidget(tabSupervisor, this);
|
||||||
connect(messageLog, &MessageLogWidget::cardNameHovered, cardInfoFrameWidget,
|
connect(messageLog, &MessageLogWidget::cardNameHovered, cardInfoFrameWidget,
|
||||||
qOverload<const QString &>(&CardInfoFrameWidget::setCard));
|
qOverload<const QString &>(&CardInfoFrameWidget::setCard));
|
||||||
|
|
@ -1690,14 +1706,12 @@ void TabGame::createMessageDock(bool bReplay)
|
||||||
connect(messageLog, &MessageLogWidget::addMentionTag, this, &TabGame::addMentionTag);
|
connect(messageLog, &MessageLogWidget::addMentionTag, this, &TabGame::addMentionTag);
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::chatMentionCompleterChanged, this,
|
connect(&SettingsCache::instance(), &SettingsCache::chatMentionCompleterChanged, this,
|
||||||
&TabGame::actCompleterChanged);
|
&TabGame::actCompleterChanged);
|
||||||
|
}
|
||||||
|
|
||||||
timeElapsedLabel = new QLabel;
|
messageLogLayout->addWidget(messageLog);
|
||||||
timeElapsedLabel->setAlignment(Qt::AlignCenter);
|
|
||||||
gameTimer = new QTimer(this);
|
|
||||||
gameTimer->setInterval(1000);
|
|
||||||
connect(gameTimer, &QTimer::timeout, this, &TabGame::incrementGameTime);
|
|
||||||
gameTimer->start();
|
|
||||||
|
|
||||||
|
// chat entry
|
||||||
|
if (!bReplay) {
|
||||||
sayLabel = new QLabel;
|
sayLabel = new QLabel;
|
||||||
sayEdit = new LineEditCompleter;
|
sayEdit = new LineEditCompleter;
|
||||||
sayEdit->setMaxLength(MAX_TEXT_LENGTH);
|
sayEdit->setMaxLength(MAX_TEXT_LENGTH);
|
||||||
|
|
@ -1726,20 +1740,15 @@ void TabGame::createMessageDock(bool bReplay)
|
||||||
connect(tabSupervisor, &TabSupervisor::adminLockChanged, this, &TabGame::adminLockChanged);
|
connect(tabSupervisor, &TabSupervisor::adminLockChanged, this, &TabGame::adminLockChanged);
|
||||||
connect(sayEdit, &LineEditCompleter::returnPressed, this, &TabGame::actSay);
|
connect(sayEdit, &LineEditCompleter::returnPressed, this, &TabGame::actSay);
|
||||||
|
|
||||||
sayHLayout = new QHBoxLayout;
|
auto sayHLayout = new QHBoxLayout;
|
||||||
sayHLayout->addWidget(sayLabel);
|
sayHLayout->addWidget(sayLabel);
|
||||||
sayHLayout->addWidget(sayEdit);
|
sayHLayout->addWidget(sayEdit);
|
||||||
|
|
||||||
|
messageLogLayout->addLayout(sayHLayout);
|
||||||
}
|
}
|
||||||
|
|
||||||
messageLogLayout = new QVBoxLayout;
|
// dock
|
||||||
messageLogLayout->setContentsMargins(0, 0, 0, 0);
|
auto messageLogLayoutWidget = new QWidget;
|
||||||
if (!bReplay)
|
|
||||||
messageLogLayout->addWidget(timeElapsedLabel);
|
|
||||||
messageLogLayout->addWidget(messageLog);
|
|
||||||
if (!bReplay)
|
|
||||||
messageLogLayout->addLayout(sayHLayout);
|
|
||||||
|
|
||||||
messageLogLayoutWidget = new QWidget;
|
|
||||||
messageLogLayoutWidget->setLayout(messageLogLayout);
|
messageLogLayoutWidget->setLayout(messageLogLayout);
|
||||||
|
|
||||||
messageLayoutDock = new QDockWidget(this);
|
messageLayoutDock = new QDockWidget(this);
|
||||||
|
|
|
||||||
|
|
@ -111,10 +111,8 @@ private:
|
||||||
GameScene *scene;
|
GameScene *scene;
|
||||||
GameView *gameView;
|
GameView *gameView;
|
||||||
QMap<int, DeckViewContainer *> deckViewContainers;
|
QMap<int, DeckViewContainer *> deckViewContainers;
|
||||||
QVBoxLayout *cardVInfoLayout, *messageLogLayout, *gamePlayAreaVBox, *deckViewContainerLayout;
|
QVBoxLayout *deckViewContainerLayout;
|
||||||
QHBoxLayout *cardHInfoLayout, *sayHLayout, *mainHLayout, *replayControlLayout;
|
QWidget *gamePlayAreaWidget, *deckViewContainerWidget;
|
||||||
QWidget *cardBoxLayoutWidget, *messageLogLayoutWidget, *gamePlayAreaWidget, *deckViewContainerWidget,
|
|
||||||
*replayControlWidget;
|
|
||||||
QDockWidget *cardInfoDock, *messageLayoutDock, *playerListDock, *replayDock;
|
QDockWidget *cardInfoDock, *messageLayoutDock, *playerListDock, *replayDock;
|
||||||
QAction *playersSeparator;
|
QAction *playersSeparator;
|
||||||
QMenu *gameMenu, *viewMenu, *cardInfoDockMenu, *messageLayoutDockMenu, *playerListDockMenu, *replayDockMenu;
|
QMenu *gameMenu, *viewMenu, *cardInfoDockMenu, *messageLayoutDockMenu, *playerListDockMenu, *replayDockMenu;
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,14 @@
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDirIterator>
|
#include <QDirIterator>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
#include <QMainWindow>
|
||||||
#include <QMovie>
|
#include <QMovie>
|
||||||
#include <QNetworkDiskCache>
|
#include <QNetworkDiskCache>
|
||||||
#include <QNetworkRequest>
|
#include <QNetworkRequest>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPixmapCache>
|
#include <QPixmapCache>
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
|
#include <QStatusBar>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
@ -27,6 +29,16 @@ PictureLoader::PictureLoader() : QObject(nullptr)
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::picDownloadChanged, this, &PictureLoader::picDownloadChanged);
|
connect(&SettingsCache::instance(), &SettingsCache::picDownloadChanged, this, &PictureLoader::picDownloadChanged);
|
||||||
|
|
||||||
connect(worker, &PictureLoaderWorker::imageLoaded, this, &PictureLoader::imageLoaded);
|
connect(worker, &PictureLoaderWorker::imageLoaded, this, &PictureLoader::imageLoaded);
|
||||||
|
|
||||||
|
statusBar = new PictureLoaderStatusBar(nullptr);
|
||||||
|
QMainWindow *mainWindow = qobject_cast<QMainWindow *>(QApplication::activeWindow());
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow->statusBar()->addPermanentWidget(statusBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(worker, &PictureLoaderWorker::imageLoadQueued, statusBar, &PictureLoaderStatusBar::addQueuedImageLoad);
|
||||||
|
connect(worker, &PictureLoaderWorker::imageLoadSuccessful, statusBar,
|
||||||
|
&PictureLoaderStatusBar::addSuccessfulImageLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
PictureLoader::~PictureLoader()
|
PictureLoader::~PictureLoader()
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#define PICTURELOADER_H
|
#define PICTURELOADER_H
|
||||||
|
|
||||||
#include "../../../game/cards/card_info.h"
|
#include "../../../game/cards/card_info.h"
|
||||||
|
#include "picture_loader_status_bar.h"
|
||||||
#include "picture_loader_worker.h"
|
#include "picture_loader_worker.h"
|
||||||
|
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
|
|
@ -27,6 +28,7 @@ private:
|
||||||
void operator=(PictureLoader const &);
|
void operator=(PictureLoader const &);
|
||||||
|
|
||||||
PictureLoaderWorker *worker;
|
PictureLoaderWorker *worker;
|
||||||
|
PictureLoaderStatusBar *statusBar;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static void getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size);
|
static void getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size);
|
||||||
|
|
|
||||||
|
|
@ -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 "../../../game/cards/card_database_manager.h"
|
||||||
#include "../../../settings/cache_settings.h"
|
#include "../../../settings/cache_settings.h"
|
||||||
|
#include "picture_loader_worker_work.h"
|
||||||
|
|
||||||
#include <QBuffer>
|
|
||||||
#include <QDirIterator>
|
#include <QDirIterator>
|
||||||
|
#include <QJsonDocument>
|
||||||
#include <QMovie>
|
#include <QMovie>
|
||||||
#include <QNetworkDiskCache>
|
#include <QNetworkDiskCache>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
// Card back returned by gatherer when card is not found
|
// Card back returned by gatherer when card is not found
|
||||||
QStringList PictureLoaderWorker::md5Blacklist = QStringList() << "db0c48db407a907c16ade38de048a441";
|
QStringList PictureLoaderWorker::md5Blacklist = QStringList() << "db0c48db407a907c16ade38de048a441";
|
||||||
|
|
@ -19,11 +21,8 @@ PictureLoaderWorker::PictureLoaderWorker()
|
||||||
picDownload(SettingsCache::instance().getPicDownload()), downloadRunning(false), loadQueueRunning(false),
|
picDownload(SettingsCache::instance().getPicDownload()), downloadRunning(false), loadQueueRunning(false),
|
||||||
overrideAllCardArtWithPersonalPreference(SettingsCache::instance().getOverrideAllCardArtWithPersonalPreference())
|
overrideAllCardArtWithPersonalPreference(SettingsCache::instance().getOverrideAllCardArtWithPersonalPreference())
|
||||||
{
|
{
|
||||||
connect(this, &PictureLoaderWorker::startLoadQueue, this, &PictureLoaderWorker::processLoadQueue,
|
connect(&SettingsCache::instance(), SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged()));
|
||||||
Qt::QueuedConnection);
|
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::picsPathChanged, this, &PictureLoaderWorker::picsPathChanged);
|
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::picDownloadChanged, this,
|
|
||||||
&PictureLoaderWorker::picDownloadChanged);
|
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::overrideAllCardArtWithPersonalPreferenceChanged, this,
|
connect(&SettingsCache::instance(), &SettingsCache::overrideAllCardArtWithPersonalPreferenceChanged, this,
|
||||||
&PictureLoaderWorker::setOverrideAllCardArtWithPersonalPreference);
|
&PictureLoaderWorker::setOverrideAllCardArtWithPersonalPreference);
|
||||||
|
|
||||||
|
|
@ -34,18 +33,17 @@ PictureLoaderWorker::PictureLoaderWorker()
|
||||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
|
||||||
networkManager->setTransferTimeout();
|
networkManager->setTransferTimeout();
|
||||||
#endif
|
#endif
|
||||||
auto cache = new QNetworkDiskCache(this);
|
cache = new QNetworkDiskCache(this);
|
||||||
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
|
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
|
||||||
cache->setMaximumCacheSize(1024L * 1024L *
|
cache->setMaximumCacheSize(1024L * 1024L *
|
||||||
static_cast<qint64>(SettingsCache::instance().getNetworkCacheSizeInMB()));
|
static_cast<qint64>(SettingsCache::instance().getNetworkCacheSizeInMB()));
|
||||||
// Note: the settings is in MB, but QNetworkDiskCache uses bytes
|
// Note: the settings is in MB, but QNetworkDiskCache uses bytes
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, cache,
|
connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, this,
|
||||||
[cache](int newSizeInMB) { cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB)); });
|
[this](int newSizeInMB) { cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB)); });
|
||||||
networkManager->setCache(cache);
|
networkManager->setCache(cache);
|
||||||
// Use a ManualRedirectPolicy since we keep track of redirects in picDownloadFinished
|
// Use a ManualRedirectPolicy since we keep track of redirects in picDownloadFinished
|
||||||
// We can't use NoLessSafeRedirectPolicy because it is not applied with AlwaysCache
|
// We can't use NoLessSafeRedirectPolicy because it is not applied with AlwaysCache
|
||||||
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
|
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
|
||||||
connect(networkManager, &QNetworkAccessManager::finished, this, &PictureLoaderWorker::picDownloadFinished);
|
|
||||||
|
|
||||||
cacheFilePath = SettingsCache::instance().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
|
cacheFilePath = SettingsCache::instance().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
|
||||||
loadRedirectCache();
|
loadRedirectCache();
|
||||||
|
|
@ -57,244 +55,104 @@ PictureLoaderWorker::PictureLoaderWorker()
|
||||||
pictureLoaderThread = new QThread;
|
pictureLoaderThread = new QThread;
|
||||||
pictureLoaderThread->start(QThread::LowPriority);
|
pictureLoaderThread->start(QThread::LowPriority);
|
||||||
moveToThread(pictureLoaderThread);
|
moveToThread(pictureLoaderThread);
|
||||||
|
|
||||||
|
connect(&requestTimer, &QTimer::timeout, this, &PictureLoaderWorker::processQueuedRequests);
|
||||||
|
requestTimer.setInterval(1000);
|
||||||
|
requestTimer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
PictureLoaderWorker::~PictureLoaderWorker()
|
PictureLoaderWorker::~PictureLoaderWorker()
|
||||||
{
|
{
|
||||||
|
saveRedirectCache();
|
||||||
pictureLoaderThread->deleteLater();
|
pictureLoaderThread->deleteLater();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PictureLoaderWorker::processLoadQueue()
|
void PictureLoaderWorker::queueRequest(const QUrl &url, PictureLoaderWorkerWork *worker)
|
||||||
{
|
{
|
||||||
if (loadQueueRunning) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadQueueRunning = true;
|
|
||||||
while (true) {
|
|
||||||
mutex.lock();
|
|
||||||
if (loadQueue.isEmpty()) {
|
|
||||||
mutex.unlock();
|
|
||||||
loadQueueRunning = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cardBeingLoaded = loadQueue.takeFirst();
|
|
||||||
mutex.unlock();
|
|
||||||
|
|
||||||
QString setName = cardBeingLoaded.getSetName();
|
|
||||||
QString cardName = cardBeingLoaded.getCard()->getName();
|
|
||||||
QString correctedCardName = cardBeingLoaded.getCard()->getCorrectedName();
|
|
||||||
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[card: " << cardName << " set: " << setName << "]: Trying to load picture";
|
|
||||||
|
|
||||||
// FIXME: This is a hack so that to keep old Cockatrice behavior
|
|
||||||
// (ignoring provider ID) when the "override all card art with personal
|
|
||||||
// preference" is set.
|
|
||||||
//
|
|
||||||
// Figure out a proper way to integrate the two systems at some point.
|
|
||||||
//
|
|
||||||
// Note: need to go through a member for
|
|
||||||
// overrideAllCardArtWithPersonalPreference as reading from the
|
|
||||||
// SettingsCache instance from the PictureLoaderWorker thread could
|
|
||||||
// cause race conditions.
|
|
||||||
//
|
|
||||||
// XXX: Reading from the CardDatabaseManager instance from the
|
|
||||||
// PictureLoaderWorker thread might not be safe either
|
|
||||||
bool searchCustomPics = overrideAllCardArtWithPersonalPreference ||
|
|
||||||
CardDatabaseManager::getInstance()->isProviderIdForPreferredPrinting(
|
|
||||||
cardName, cardBeingLoaded.getCard()->getPixmapCacheKey());
|
|
||||||
if (searchCustomPics && cardImageExistsOnDisk(setName, correctedCardName, searchCustomPics)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[card: " << cardName << " set: " << setName << "]: No custom picture, trying to download";
|
|
||||||
cardsToDownload.append(cardBeingLoaded);
|
|
||||||
cardBeingLoaded.clear();
|
|
||||||
if (!downloadRunning) {
|
|
||||||
startNextPicDownload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PictureLoaderWorker::cardImageExistsOnDisk(QString &setName, QString &correctedCardname, bool searchCustomPics)
|
|
||||||
{
|
|
||||||
QImage image;
|
|
||||||
QImageReader imgReader;
|
|
||||||
imgReader.setDecideFormatFromContent(true);
|
|
||||||
QList<QString> picsPaths = QList<QString>();
|
|
||||||
|
|
||||||
if (searchCustomPics) {
|
|
||||||
QDirIterator it(customPicsPath, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
|
|
||||||
|
|
||||||
// Recursively check all subdirectories of the CUSTOM folder
|
|
||||||
while (it.hasNext()) {
|
|
||||||
QString thisPath(it.next());
|
|
||||||
QFileInfo thisFileInfo(thisPath);
|
|
||||||
|
|
||||||
if (thisFileInfo.isFile() &&
|
|
||||||
(thisFileInfo.fileName() == correctedCardname || thisFileInfo.completeBaseName() == correctedCardname ||
|
|
||||||
thisFileInfo.baseName() == correctedCardname)) {
|
|
||||||
picsPaths << thisPath; // Card found in the CUSTOM directory, somewhere
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!setName.isEmpty()) {
|
|
||||||
picsPaths << picsPath + "/" + setName + "/" + correctedCardname
|
|
||||||
// We no longer store downloaded images there, but don't just ignore
|
|
||||||
// stuff that old versions have put there.
|
|
||||||
<< picsPath + "/downloadedPics/" + setName + "/" + correctedCardname;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterates through the list of paths, searching for images with the desired
|
|
||||||
// name with any QImageReader-supported
|
|
||||||
// extension
|
|
||||||
for (const auto &_picsPath : picsPaths) {
|
|
||||||
imgReader.setFileName(_picsPath);
|
|
||||||
if (imgReader.read(&image)) {
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[card: " << correctedCardname << " set: " << setName << "]: Picture found on disk.";
|
|
||||||
imageLoaded(cardBeingLoaded.getCard(), image);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
imgReader.setFileName(_picsPath + ".full");
|
|
||||||
if (imgReader.read(&image)) {
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[card: " << correctedCardname << " set: " << setName << "]: Picture.full found on disk.";
|
|
||||||
imageLoaded(cardBeingLoaded.getCard(), image);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
imgReader.setFileName(_picsPath + ".xlhq");
|
|
||||||
if (imgReader.read(&image)) {
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[card: " << correctedCardname << " set: " << setName << "]: Picture.xlhq found on disk.";
|
|
||||||
imageLoaded(cardBeingLoaded.getCard(), image);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PictureLoaderWorker::startNextPicDownload()
|
|
||||||
{
|
|
||||||
if (cardsToDownload.isEmpty()) {
|
|
||||||
cardBeingDownloaded.clear();
|
|
||||||
downloadRunning = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadRunning = true;
|
|
||||||
|
|
||||||
cardBeingDownloaded = cardsToDownload.takeFirst();
|
|
||||||
|
|
||||||
QString picUrl = cardBeingDownloaded.getCurrentUrl();
|
|
||||||
|
|
||||||
if (picUrl.isEmpty()) {
|
|
||||||
downloadRunning = false;
|
|
||||||
picDownloadFailed();
|
|
||||||
} else {
|
|
||||||
QUrl url(picUrl);
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace() << "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
|
||||||
<< " set: " << cardBeingDownloaded.getSetName()
|
|
||||||
<< "]: Trying to fetch picture from url " << url.toDisplayString();
|
|
||||||
makeRequest(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PictureLoaderWorker::picDownloadFailed()
|
|
||||||
{
|
|
||||||
/* Take advantage of short circuiting here to call the nextUrl until one
|
|
||||||
is not available. Only once nextUrl evaluates to false will this move
|
|
||||||
on to nextSet. If the Urls for a particular card are empty, this will
|
|
||||||
effectively go through the sets for that card. */
|
|
||||||
if (cardBeingDownloaded.nextUrl() || cardBeingDownloaded.nextSet()) {
|
|
||||||
mutex.lock();
|
|
||||||
loadQueue.prepend(cardBeingDownloaded);
|
|
||||||
mutex.unlock();
|
|
||||||
} else {
|
|
||||||
qCWarning(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
|
||||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Picture NOT found, "
|
|
||||||
<< (picDownload ? "download failed" : "downloads disabled")
|
|
||||||
<< ", no more url combinations to try: BAILING OUT";
|
|
||||||
imageLoaded(cardBeingDownloaded.getCard(), QImage());
|
|
||||||
cardBeingDownloaded.clear();
|
|
||||||
}
|
|
||||||
emit startLoadQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PictureLoaderWorker::imageIsBlackListed(const QByteArray &picData)
|
|
||||||
{
|
|
||||||
QString md5sum = QCryptographicHash::hash(picData, QCryptographicHash::Md5).toHex();
|
|
||||||
return md5Blacklist.contains(md5sum);
|
|
||||||
}
|
|
||||||
|
|
||||||
QNetworkReply *PictureLoaderWorker::makeRequest(const QUrl &url)
|
|
||||||
{
|
|
||||||
// Check if the redirect is cached
|
|
||||||
QUrl cachedRedirect = getCachedRedirect(url);
|
QUrl cachedRedirect = getCachedRedirect(url);
|
||||||
if (!cachedRedirect.isEmpty()) {
|
if (!cachedRedirect.isEmpty()) {
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
queueRequest(cachedRedirect, worker);
|
||||||
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
}
|
||||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Using cached redirect for " << url.toDisplayString()
|
if (cache->metaData(url).isValid()) {
|
||||||
<< " to " << cachedRedirect.toDisplayString();
|
makeRequest(url, worker);
|
||||||
return makeRequest(cachedRedirect); // Use the cached redirect
|
} else {
|
||||||
|
requestLoadQueue.append(qMakePair(url, worker));
|
||||||
|
emit imageLoadQueued(url, worker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkReply *PictureLoaderWorker::makeRequest(const QUrl &url, PictureLoaderWorkerWork *worker)
|
||||||
|
{
|
||||||
|
// Check for cached redirects
|
||||||
|
QUrl cachedRedirect = getCachedRedirect(url);
|
||||||
|
if (!cachedRedirect.isEmpty()) {
|
||||||
|
emit imageLoadSuccessful(url, worker);
|
||||||
|
return makeRequest(cachedRedirect, worker);
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkRequest req(url);
|
QNetworkRequest req(url);
|
||||||
|
|
||||||
// QNetworkDiskCache leaks file descriptors when downloading compressed
|
|
||||||
// files, see https://bugreports.qt.io/browse/QTBUG-135641
|
|
||||||
//
|
|
||||||
// We can set the Accept-Encoding header manually to disable Qt's automatic
|
|
||||||
// response decompression, but then we would have to deal with decompression
|
|
||||||
// ourselves.
|
|
||||||
//
|
|
||||||
// Since we are dowloading images that are usually stored in a
|
|
||||||
// compressed format (e.g. jpeg or webp), it's not clear compression at the
|
|
||||||
// HTTP level helps; in fact, images are typically returned without
|
|
||||||
// compression. Redirects, on the other hand, are compressed and cause file
|
|
||||||
// descriptor leaks -- but since redirects have no payload, we don't really
|
|
||||||
// care either.
|
|
||||||
//
|
|
||||||
// In the end, just do the simple thing and disable HTTP compression.
|
|
||||||
req.setRawHeader("accept-encoding", "identity");
|
|
||||||
|
|
||||||
if (!picDownload) {
|
if (!picDownload) {
|
||||||
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache);
|
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkReply *reply = networkManager->get(req);
|
QNetworkReply *reply = networkManager->get(req);
|
||||||
|
|
||||||
connect(reply, &QNetworkReply::finished, this, [this, reply, url]() {
|
// Connect reply handling
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, reply, url, worker]() {
|
||||||
QVariant redirectTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
|
QVariant redirectTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
|
||||||
|
|
||||||
if (redirectTarget.isValid()) {
|
if (redirectTarget.isValid()) {
|
||||||
QUrl redirectUrl = redirectTarget.toUrl();
|
QUrl redirectUrl = redirectTarget.toUrl();
|
||||||
if (redirectUrl.isRelative()) {
|
if (redirectUrl.isRelative()) {
|
||||||
redirectUrl = url.resolved(redirectUrl);
|
redirectUrl = url.resolved(redirectUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheRedirect(url, redirectUrl);
|
cacheRedirect(url, redirectUrl);
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
|
||||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Caching redirect from " << url.toDisplayString()
|
|
||||||
<< " to " << redirectUrl.toDisplayString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reply->error() == QNetworkReply::NoError) {
|
||||||
|
worker->picDownloadFinished(reply);
|
||||||
|
emit imageLoadSuccessful(url, worker);
|
||||||
|
|
||||||
|
// If we hit a cached image, we get to make another request for free.
|
||||||
|
if (reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool()) {
|
||||||
|
if (!requestLoadQueue.isEmpty()) {
|
||||||
|
auto request = requestLoadQueue.takeFirst();
|
||||||
|
makeRequest(request.first, request.second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 429) {
|
||||||
|
qInfo() << "Too many requests.";
|
||||||
|
}
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
});
|
});
|
||||||
|
|
||||||
return reply;
|
return reply;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PictureLoaderWorker::processQueuedRequests()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
if (!requestLoadQueue.isEmpty()) {
|
||||||
|
auto request = requestLoadQueue.takeFirst();
|
||||||
|
makeRequest(request.first, request.second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PictureLoaderWorker::enqueueImageLoad(const CardInfoPtr &card)
|
||||||
|
{
|
||||||
|
auto worker = new PictureLoaderWorkerWork(this, card);
|
||||||
|
Q_UNUSED(worker);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PictureLoaderWorker::imageLoadedSuccessfully(CardInfoPtr card, const QImage &image)
|
||||||
|
{
|
||||||
|
emit imageLoaded(std::move(card), image);
|
||||||
|
}
|
||||||
|
|
||||||
void PictureLoaderWorker::cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl)
|
void PictureLoaderWorker::cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl)
|
||||||
{
|
{
|
||||||
redirectCache[originalUrl] = qMakePair(redirectUrl, QDateTime::currentDateTimeUtc());
|
redirectCache[originalUrl] = qMakePair(redirectUrl, QDateTime::currentDateTimeUtc());
|
||||||
saveRedirectCache();
|
// saveRedirectCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
QUrl PictureLoaderWorker::getCachedRedirect(const QUrl &originalUrl) const
|
QUrl PictureLoaderWorker::getCachedRedirect(const QUrl &originalUrl) const
|
||||||
|
|
@ -353,129 +211,6 @@ void PictureLoaderWorker::cleanStaleEntries()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PictureLoaderWorker::picDownloadFinished(QNetworkReply *reply)
|
|
||||||
{
|
|
||||||
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
|
|
||||||
|
|
||||||
if (reply->error()) {
|
|
||||||
if (isFromCache) {
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
|
|
||||||
<< "]: Removing corrupted cache file for url " << reply->url().toDisplayString() << " and retrying ("
|
|
||||||
<< reply->errorString() << ")";
|
|
||||||
|
|
||||||
networkManager->cache()->remove(reply->url());
|
|
||||||
|
|
||||||
makeRequest(reply->url());
|
|
||||||
} else {
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
|
|
||||||
<< "]: " << (picDownload ? "Download" : "Cache search") << " failed for url "
|
|
||||||
<< reply->url().toDisplayString() << " (" << reply->errorString() << ")";
|
|
||||||
|
|
||||||
picDownloadFailed();
|
|
||||||
startNextPicDownload();
|
|
||||||
}
|
|
||||||
|
|
||||||
reply->deleteLater();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// List of status codes from https://doc.qt.io/qt-6/qnetworkreply.html#redirected
|
|
||||||
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
||||||
if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 305 || statusCode == 307 ||
|
|
||||||
statusCode == 308) {
|
|
||||||
QUrl redirectUrl = reply->header(QNetworkRequest::LocationHeader).toUrl();
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
|
|
||||||
<< "]: following " << (isFromCache ? "cached redirect" : "redirect") << " to "
|
|
||||||
<< redirectUrl.toDisplayString();
|
|
||||||
makeRequest(redirectUrl);
|
|
||||||
reply->deleteLater();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// peek is used to keep the data in the buffer for use by QImageReader
|
|
||||||
const QByteArray &picData = reply->peek(reply->size());
|
|
||||||
|
|
||||||
if (imageIsBlackListed(picData)) {
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
|
|
||||||
<< "]: Picture found, but blacklisted, will consider it as not found";
|
|
||||||
|
|
||||||
picDownloadFailed();
|
|
||||||
reply->deleteLater();
|
|
||||||
startNextPicDownload();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QImage testImage;
|
|
||||||
|
|
||||||
QImageReader imgReader;
|
|
||||||
imgReader.setDecideFormatFromContent(true);
|
|
||||||
imgReader.setDevice(reply);
|
|
||||||
|
|
||||||
bool logSuccessMessage = false;
|
|
||||||
|
|
||||||
static const int riffHeaderSize = 12; // RIFF_HEADER_SIZE from webp/format_constants.h
|
|
||||||
auto replyHeader = reply->peek(riffHeaderSize);
|
|
||||||
|
|
||||||
if (replyHeader.startsWith("RIFF") && replyHeader.endsWith("WEBP")) {
|
|
||||||
auto imgBuf = QBuffer(this);
|
|
||||||
imgBuf.setData(reply->readAll());
|
|
||||||
|
|
||||||
auto movie = QMovie(&imgBuf);
|
|
||||||
movie.start();
|
|
||||||
movie.stop();
|
|
||||||
|
|
||||||
imageLoaded(cardBeingDownloaded.getCard(), movie.currentImage());
|
|
||||||
logSuccessMessage = true;
|
|
||||||
} else if (imgReader.read(&testImage)) {
|
|
||||||
imageLoaded(cardBeingDownloaded.getCard(), testImage);
|
|
||||||
logSuccessMessage = true;
|
|
||||||
} else {
|
|
||||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
|
|
||||||
<< "]: Possible " << (isFromCache ? "cached" : "downloaded") << " picture at "
|
|
||||||
<< reply->url().toDisplayString() << " could not be loaded: " << reply->errorString();
|
|
||||||
|
|
||||||
picDownloadFailed();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logSuccessMessage) {
|
|
||||||
qCInfo(PictureLoaderWorkerLog).nospace()
|
|
||||||
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
|
|
||||||
<< "]: Image successfully " << (isFromCache ? "loaded from cached" : "downloaded from") << " url "
|
|
||||||
<< reply->url().toDisplayString();
|
|
||||||
}
|
|
||||||
|
|
||||||
reply->deleteLater();
|
|
||||||
startNextPicDownload();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PictureLoaderWorker::enqueueImageLoad(CardInfoPtr card)
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&mutex);
|
|
||||||
|
|
||||||
// avoid queueing the same card more than once
|
|
||||||
if (!card || card == cardBeingLoaded.getCard() || card == cardBeingDownloaded.getCard()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const PictureToLoad &pic : loadQueue) {
|
|
||||||
if (pic.getCard() == card)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const PictureToLoad &pic : cardsToDownload) {
|
|
||||||
if (pic.getCard() == card)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadQueue.append(PictureToLoad(card));
|
|
||||||
emit startLoadQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PictureLoaderWorker::picDownloadChanged()
|
void PictureLoaderWorker::picDownloadChanged()
|
||||||
{
|
{
|
||||||
QMutexLocker locker(&mutex);
|
QMutexLocker locker(&mutex);
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,18 @@
|
||||||
#ifndef PICTURE_LOADER_WORKER_H
|
#ifndef PICTURE_LOADER_WORKER_H
|
||||||
#define PICTURE_LOADER_WORKER_H
|
#define PICTURE_LOADER_WORKER_H
|
||||||
|
|
||||||
|
#include "../../../game/cards/card_database.h"
|
||||||
#include "../../../game/cards/card_info.h"
|
#include "../../../game/cards/card_info.h"
|
||||||
|
#include "picture_loader_worker_work.h"
|
||||||
#include "picture_to_load.h"
|
#include "picture_to_load.h"
|
||||||
|
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
#include <QMutex>
|
#include <QMutex>
|
||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkDiskCache>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QQueue>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
#define REDIRECT_HEADER_NAME "redirects"
|
#define REDIRECT_HEADER_NAME "redirects"
|
||||||
#define REDIRECT_ORIGINAL_URL "original"
|
#define REDIRECT_ORIGINAL_URL "original"
|
||||||
|
|
@ -17,16 +22,23 @@
|
||||||
|
|
||||||
inline Q_LOGGING_CATEGORY(PictureLoaderWorkerLog, "picture_loader.worker");
|
inline Q_LOGGING_CATEGORY(PictureLoaderWorkerLog, "picture_loader.worker");
|
||||||
|
|
||||||
|
class PictureLoaderWorkerWork;
|
||||||
class PictureLoaderWorker : public QObject
|
class PictureLoaderWorker : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit PictureLoaderWorker();
|
explicit PictureLoaderWorker();
|
||||||
~PictureLoaderWorker() override;
|
~PictureLoaderWorker() override;
|
||||||
|
void queueRequest(const QUrl &url, PictureLoaderWorkerWork *worker);
|
||||||
|
|
||||||
void enqueueImageLoad(CardInfoPtr card);
|
void enqueueImageLoad(const CardInfoPtr &card);
|
||||||
void clearNetworkCache();
|
void clearNetworkCache();
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
QNetworkReply *makeRequest(const QUrl &url, PictureLoaderWorkerWork *workThread);
|
||||||
|
void processQueuedRequests();
|
||||||
|
void imageLoadedSuccessfully(CardInfoPtr card, const QImage &image);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static QStringList md5Blacklist;
|
static QStringList md5Blacklist;
|
||||||
|
|
||||||
|
|
@ -35,6 +47,7 @@ private:
|
||||||
QList<PictureToLoad> loadQueue;
|
QList<PictureToLoad> loadQueue;
|
||||||
QMutex mutex;
|
QMutex mutex;
|
||||||
QNetworkAccessManager *networkManager;
|
QNetworkAccessManager *networkManager;
|
||||||
|
QNetworkDiskCache *cache;
|
||||||
QHash<QUrl, QPair<QUrl, QDateTime>> redirectCache; // Stores redirect and timestamp
|
QHash<QUrl, QPair<QUrl, QDateTime>> redirectCache; // Stores redirect and timestamp
|
||||||
QString cacheFilePath; // Path to persistent storage
|
QString cacheFilePath; // Path to persistent storage
|
||||||
static constexpr int CacheTTLInDays = 30; // TODO: Make user configurable
|
static constexpr int CacheTTLInDays = 30; // TODO: Make user configurable
|
||||||
|
|
@ -42,18 +55,11 @@ private:
|
||||||
PictureToLoad cardBeingLoaded;
|
PictureToLoad cardBeingLoaded;
|
||||||
PictureToLoad cardBeingDownloaded;
|
PictureToLoad cardBeingDownloaded;
|
||||||
bool picDownload, downloadRunning, loadQueueRunning;
|
bool picDownload, downloadRunning, loadQueueRunning;
|
||||||
|
QQueue<QPair<QUrl, PictureLoaderWorkerWork *>> requestLoadQueue;
|
||||||
|
QList<QPair<QUrl, PictureLoaderWorkerWork *>> requestQueue;
|
||||||
|
QTimer requestTimer; // Timer for processing delayed requests
|
||||||
bool overrideAllCardArtWithPersonalPreference;
|
bool overrideAllCardArtWithPersonalPreference;
|
||||||
void startNextPicDownload();
|
|
||||||
|
|
||||||
/** Emit the `imageLoaded` signal and return `true` if a picture is found on
|
|
||||||
disk, return `false` otherwise.
|
|
||||||
|
|
||||||
If `searchCustomPics` is `true`, the CUSTOM folder is searched for a
|
|
||||||
matching image first; otherwise, only the set-based folders are used. */
|
|
||||||
bool cardImageExistsOnDisk(QString &setName, QString &correctedCardName, bool searchCustomPics);
|
|
||||||
|
|
||||||
bool imageIsBlackListed(const QByteArray &);
|
|
||||||
QNetworkReply *makeRequest(const QUrl &url);
|
|
||||||
void cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl);
|
void cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl);
|
||||||
QUrl getCachedRedirect(const QUrl &originalUrl) const;
|
QUrl getCachedRedirect(const QUrl &originalUrl) const;
|
||||||
void loadRedirectCache();
|
void loadRedirectCache();
|
||||||
|
|
@ -61,18 +67,15 @@ private:
|
||||||
void cleanStaleEntries();
|
void cleanStaleEntries();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void picDownloadFinished(QNetworkReply *reply);
|
|
||||||
void picDownloadFailed();
|
|
||||||
|
|
||||||
void picDownloadChanged();
|
void picDownloadChanged();
|
||||||
void picsPathChanged();
|
void picsPathChanged();
|
||||||
void setOverrideAllCardArtWithPersonalPreference(bool _overrideAllCardArtWithPersonalPreference);
|
void setOverrideAllCardArtWithPersonalPreference(bool _overrideAllCardArtWithPersonalPreference);
|
||||||
public slots:
|
|
||||||
void processLoadQueue();
|
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void startLoadQueue();
|
void startLoadQueue();
|
||||||
void imageLoaded(CardInfoPtr card, const QImage &image);
|
void imageLoaded(CardInfoPtr card, const QImage &image);
|
||||||
|
void imageLoadQueued(const QUrl &url, PictureLoaderWorkerWork *worker);
|
||||||
|
void imageLoadSuccessful(const QUrl &url, PictureLoaderWorkerWork *worker);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // PICTURE_LOADER_WORKER_H
|
#endif // PICTURE_LOADER_WORKER_H
|
||||||
|
|
|
||||||
|
|
@ -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 _zoneName,
|
||||||
QString _activeGroupCriteria,
|
QString _activeGroupCriteria,
|
||||||
QStringList _activeSortCriteria,
|
QStringList _activeSortCriteria,
|
||||||
|
DisplayType _displayType,
|
||||||
int bannerOpacity,
|
int bannerOpacity,
|
||||||
int subBannerOpacity,
|
int subBannerOpacity,
|
||||||
CardSizeWidget *_cardSizeWidget)
|
CardSizeWidget *_cardSizeWidget)
|
||||||
: QWidget(parent), deckListModel(_deckListModel), zoneName(_zoneName), activeGroupCriteria(_activeGroupCriteria),
|
: QWidget(parent), deckListModel(_deckListModel), zoneName(_zoneName), activeGroupCriteria(_activeGroupCriteria),
|
||||||
activeSortCriteria(_activeSortCriteria), bannerOpacity(bannerOpacity), subBannerOpacity(subBannerOpacity),
|
activeSortCriteria(_activeSortCriteria), displayType(_displayType), bannerOpacity(bannerOpacity),
|
||||||
cardSizeWidget(_cardSizeWidget)
|
subBannerOpacity(subBannerOpacity), cardSizeWidget(_cardSizeWidget)
|
||||||
{
|
{
|
||||||
layout = new QVBoxLayout(this);
|
layout = new QVBoxLayout(this);
|
||||||
setLayout(layout);
|
setLayout(layout);
|
||||||
|
|
@ -60,7 +61,7 @@ void DeckCardZoneDisplayWidget::displayCards()
|
||||||
deleteCardGroupIfItDoesNotExist();
|
deleteCardGroupIfItDoesNotExist();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeckCardZoneDisplayWidget::refreshDisplayType(const QString &_displayType)
|
void DeckCardZoneDisplayWidget::refreshDisplayType(const DisplayType &_displayType)
|
||||||
{
|
{
|
||||||
displayType = _displayType;
|
displayType = _displayType;
|
||||||
QLayoutItem *item;
|
QLayoutItem *item;
|
||||||
|
|
@ -102,7 +103,7 @@ void DeckCardZoneDisplayWidget::addCardGroupIfItDoesNotExist()
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (displayType == "overlap") {
|
if (displayType == DisplayType::Overlap) {
|
||||||
auto *display_widget = new OverlappedCardGroupDisplayWidget(
|
auto *display_widget = new OverlappedCardGroupDisplayWidget(
|
||||||
cardGroupContainer, deckListModel, zoneName, cardGroup, activeGroupCriteria, activeSortCriteria,
|
cardGroupContainer, deckListModel, zoneName, cardGroup, activeGroupCriteria, activeSortCriteria,
|
||||||
subBannerOpacity, cardSizeWidget);
|
subBannerOpacity, cardSizeWidget);
|
||||||
|
|
@ -112,7 +113,7 @@ void DeckCardZoneDisplayWidget::addCardGroupIfItDoesNotExist()
|
||||||
connect(this, &DeckCardZoneDisplayWidget::activeSortCriteriaChanged, display_widget,
|
connect(this, &DeckCardZoneDisplayWidget::activeSortCriteriaChanged, display_widget,
|
||||||
&CardGroupDisplayWidget::onActiveSortCriteriaChanged);
|
&CardGroupDisplayWidget::onActiveSortCriteriaChanged);
|
||||||
cardGroupLayout->addWidget(display_widget);
|
cardGroupLayout->addWidget(display_widget);
|
||||||
} else if (displayType == "flat") {
|
} else if (displayType == DisplayType::Flat) {
|
||||||
auto *display_widget = new FlatCardGroupDisplayWidget(cardGroupContainer, deckListModel, zoneName,
|
auto *display_widget = new FlatCardGroupDisplayWidget(cardGroupContainer, deckListModel, zoneName,
|
||||||
cardGroup, activeGroupCriteria, activeSortCriteria,
|
cardGroup, activeGroupCriteria, activeSortCriteria,
|
||||||
subBannerOpacity, cardSizeWidget);
|
subBannerOpacity, cardSizeWidget);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include "../../../../game/cards/card_info.h"
|
#include "../../../../game/cards/card_info.h"
|
||||||
#include "../general/display/banner_widget.h"
|
#include "../general/display/banner_widget.h"
|
||||||
#include "../general/layout_containers/overlap_widget.h"
|
#include "../general/layout_containers/overlap_widget.h"
|
||||||
|
#include "../visual_deck_editor/visual_deck_editor_widget.h"
|
||||||
#include "card_info_picture_with_text_overlay_widget.h"
|
#include "card_info_picture_with_text_overlay_widget.h"
|
||||||
#include "card_size_widget.h"
|
#include "card_size_widget.h"
|
||||||
|
|
||||||
|
|
@ -21,6 +22,7 @@ public:
|
||||||
QString zoneName,
|
QString zoneName,
|
||||||
QString activeGroupCriteria,
|
QString activeGroupCriteria,
|
||||||
QStringList activeSortCriteria,
|
QStringList activeSortCriteria,
|
||||||
|
DisplayType displayType,
|
||||||
int bannerOpacity,
|
int bannerOpacity,
|
||||||
int subBannerOpacity,
|
int subBannerOpacity,
|
||||||
CardSizeWidget *_cardSizeWidget);
|
CardSizeWidget *_cardSizeWidget);
|
||||||
|
|
@ -33,7 +35,7 @@ public slots:
|
||||||
void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card);
|
void onClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *card);
|
||||||
void onHover(CardInfoPtr card);
|
void onHover(CardInfoPtr card);
|
||||||
void displayCards();
|
void displayCards();
|
||||||
void refreshDisplayType(const QString &displayType);
|
void refreshDisplayType(const DisplayType &displayType);
|
||||||
void addCardGroupIfItDoesNotExist();
|
void addCardGroupIfItDoesNotExist();
|
||||||
void deleteCardGroupIfItDoesNotExist();
|
void deleteCardGroupIfItDoesNotExist();
|
||||||
void onActiveGroupCriteriaChanged(QString activeGroupCriteria);
|
void onActiveGroupCriteriaChanged(QString activeGroupCriteria);
|
||||||
|
|
@ -48,6 +50,7 @@ signals:
|
||||||
private:
|
private:
|
||||||
QString activeGroupCriteria;
|
QString activeGroupCriteria;
|
||||||
QStringList activeSortCriteria;
|
QStringList activeSortCriteria;
|
||||||
|
DisplayType displayType = DisplayType::Overlap;
|
||||||
int bannerOpacity = 20;
|
int bannerOpacity = 20;
|
||||||
int subBannerOpacity = 10;
|
int subBannerOpacity = 10;
|
||||||
CardSizeWidget *cardSizeWidget;
|
CardSizeWidget *cardSizeWidget;
|
||||||
|
|
@ -55,7 +58,6 @@ private:
|
||||||
BannerWidget *banner;
|
BannerWidget *banner;
|
||||||
QWidget *cardGroupContainer;
|
QWidget *cardGroupContainer;
|
||||||
QVBoxLayout *cardGroupLayout;
|
QVBoxLayout *cardGroupLayout;
|
||||||
QString displayType = "flat";
|
|
||||||
OverlapWidget *overlapWidget;
|
OverlapWidget *overlapWidget;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,20 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
||||||
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckModel->getDeckList());
|
deckTagsDisplayWidget = new DeckPreviewDeckTagsDisplayWidget(this, deckModel->getDeckList());
|
||||||
deckTagsDisplayWidget->setHidden(!SettingsCache::instance().getDeckEditorTagsWidgetVisible());
|
deckTagsDisplayWidget->setHidden(!SettingsCache::instance().getDeckEditorTagsWidgetVisible());
|
||||||
|
|
||||||
|
activeGroupCriteriaLabel = new QLabel(this);
|
||||||
|
|
||||||
|
activeGroupCriteriaComboBox = new QComboBox(this);
|
||||||
|
activeGroupCriteriaComboBox->addItem(tr("Main Type"), DeckListModelGroupCriteria::MAIN_TYPE);
|
||||||
|
activeGroupCriteriaComboBox->addItem(tr("Mana Cost"), DeckListModelGroupCriteria::MANA_COST);
|
||||||
|
activeGroupCriteriaComboBox->addItem(tr("Colors"), DeckListModelGroupCriteria::COLOR);
|
||||||
|
connect(activeGroupCriteriaComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), [this]() {
|
||||||
|
deckModel->setActiveGroupCriteria(
|
||||||
|
static_cast<DeckListModelGroupCriteria>(activeGroupCriteriaComboBox->currentData(Qt::UserRole).toInt()));
|
||||||
|
deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder());
|
||||||
|
deckView->expandAll();
|
||||||
|
deckView->expandAll();
|
||||||
|
});
|
||||||
|
|
||||||
aIncrement = new QAction(QString(), this);
|
aIncrement = new QAction(QString(), this);
|
||||||
aIncrement->setIcon(QPixmap("theme:icons/increment"));
|
aIncrement->setIcon(QPixmap("theme:icons/increment"));
|
||||||
connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrement);
|
connect(aIncrement, &QAction::triggered, this, &DeckEditorDeckDockWidget::actIncrement);
|
||||||
|
|
@ -144,6 +158,9 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
||||||
|
|
||||||
upperLayout->addWidget(deckTagsDisplayWidget, 3, 1);
|
upperLayout->addWidget(deckTagsDisplayWidget, 3, 1);
|
||||||
|
|
||||||
|
upperLayout->addWidget(activeGroupCriteriaLabel, 4, 0);
|
||||||
|
upperLayout->addWidget(activeGroupCriteriaComboBox, 4, 1);
|
||||||
|
|
||||||
hashLabel1 = new QLabel();
|
hashLabel1 = new QLabel();
|
||||||
hashLabel1->setObjectName("hashLabel1");
|
hashLabel1->setObjectName("hashLabel1");
|
||||||
auto *hashSizePolicy = new QSizePolicy();
|
auto *hashSizePolicy = new QSizePolicy();
|
||||||
|
|
@ -286,11 +303,7 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox()
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const auto &pair : pairList) {
|
for (const auto &pair : pairList) {
|
||||||
QVariantMap dataMap;
|
bannerCardComboBox->addItem(pair.first, QVariant::fromValue(pair));
|
||||||
dataMap["name"] = pair.first;
|
|
||||||
dataMap["uuid"] = pair.second;
|
|
||||||
|
|
||||||
bannerCardComboBox->addItem(pair.first, dataMap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to restore the previous selection by finding the currentText
|
// Try to restore the previous selection by finding the currentText
|
||||||
|
|
@ -298,7 +311,7 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox()
|
||||||
if (restoredIndex != -1) {
|
if (restoredIndex != -1) {
|
||||||
bannerCardComboBox->setCurrentIndex(restoredIndex);
|
bannerCardComboBox->setCurrentIndex(restoredIndex);
|
||||||
if (deckModel->getDeckList()->getBannerCard().second !=
|
if (deckModel->getDeckList()->getBannerCard().second !=
|
||||||
bannerCardComboBox->itemData(bannerCardComboBox->currentIndex()).toMap()["uuid"].toString()) {
|
bannerCardComboBox->currentData().value<QPair<QString, QString>>().second) {
|
||||||
setBannerCard(restoredIndex);
|
setBannerCard(restoredIndex);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -318,9 +331,8 @@ void DeckEditorDeckDockWidget::updateBannerCardComboBox()
|
||||||
|
|
||||||
void DeckEditorDeckDockWidget::setBannerCard(int /* changedIndex */)
|
void DeckEditorDeckDockWidget::setBannerCard(int /* changedIndex */)
|
||||||
{
|
{
|
||||||
QVariantMap itemData = bannerCardComboBox->itemData(bannerCardComboBox->currentIndex()).toMap();
|
auto cardAndId = bannerCardComboBox->currentData().value<QPair<QString, QString>>();
|
||||||
deckModel->getDeckList()->setBannerCard(
|
deckModel->getDeckList()->setBannerCard(cardAndId);
|
||||||
QPair<QString, QString>(itemData["name"].toString(), itemData["uuid"].toString()));
|
|
||||||
emit deckModified();
|
emit deckModified();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -578,6 +590,7 @@ void DeckEditorDeckDockWidget::retranslateUi()
|
||||||
showBannerCardCheckBox->setText(tr("Show banner card selection menu"));
|
showBannerCardCheckBox->setText(tr("Show banner card selection menu"));
|
||||||
showTagsWidgetCheckBox->setText(tr("Show tags selection menu"));
|
showTagsWidgetCheckBox->setText(tr("Show tags selection menu"));
|
||||||
commentsLabel->setText(tr("&Comments:"));
|
commentsLabel->setText(tr("&Comments:"));
|
||||||
|
activeGroupCriteriaLabel->setText(tr("Group by:"));
|
||||||
hashLabel1->setText(tr("Hash:"));
|
hashLabel1->setText(tr("Hash:"));
|
||||||
|
|
||||||
aIncrement->setText(tr("&Increment number"));
|
aIncrement->setText(tr("&Increment number"));
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,8 @@ private:
|
||||||
DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget;
|
DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget;
|
||||||
QLabel *hashLabel1;
|
QLabel *hashLabel1;
|
||||||
LineEditUnfocusable *hashLabel;
|
LineEditUnfocusable *hashLabel;
|
||||||
|
QLabel *activeGroupCriteriaLabel;
|
||||||
|
QComboBox *activeGroupCriteriaComboBox;
|
||||||
|
|
||||||
QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;
|
QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,11 @@ void SettingsButtonWidget::addSettingsWidget(QWidget *toAdd) const
|
||||||
popup->addSettingsWidget(toAdd);
|
popup->addSettingsWidget(toAdd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SettingsButtonWidget::removeSettingsWidget(QWidget *toRemove) const
|
||||||
|
{
|
||||||
|
popup->removeSettingsWidget(toRemove);
|
||||||
|
}
|
||||||
|
|
||||||
void SettingsButtonWidget::setButtonIcon(QPixmap iconMap)
|
void SettingsButtonWidget::setButtonIcon(QPixmap iconMap)
|
||||||
{
|
{
|
||||||
button->setIcon(iconMap);
|
button->setIcon(iconMap);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ class SettingsButtonWidget : public QWidget
|
||||||
public:
|
public:
|
||||||
explicit SettingsButtonWidget(QWidget *parent = nullptr);
|
explicit SettingsButtonWidget(QWidget *parent = nullptr);
|
||||||
void addSettingsWidget(QWidget *toAdd) const;
|
void addSettingsWidget(QWidget *toAdd) const;
|
||||||
|
void removeSettingsWidget(QWidget *toRemove) const;
|
||||||
void setButtonIcon(QPixmap iconMap);
|
void setButtonIcon(QPixmap iconMap);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
@ -25,6 +26,8 @@ private slots:
|
||||||
private:
|
private:
|
||||||
QHBoxLayout *layout;
|
QHBoxLayout *layout;
|
||||||
QToolButton *button;
|
QToolButton *button;
|
||||||
|
|
||||||
|
public:
|
||||||
SettingsPopupWidget *popup;
|
SettingsPopupWidget *popup;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,12 @@ void SettingsPopupWidget::addSettingsWidget(QWidget *toAdd) const
|
||||||
containerLayout->addWidget(toAdd); // Add to containerWidget's layout
|
containerLayout->addWidget(toAdd); // Add to containerWidget's layout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SettingsPopupWidget::removeSettingsWidget(QWidget *toRemove) const
|
||||||
|
{
|
||||||
|
containerLayout->removeWidget(toRemove);
|
||||||
|
toRemove->deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
void SettingsPopupWidget::adjustSizeToFitScreen()
|
void SettingsPopupWidget::adjustSizeToFitScreen()
|
||||||
{
|
{
|
||||||
QScreen *screen = QApplication::screenAt(this->pos());
|
QScreen *screen = QApplication::screenAt(this->pos());
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ class SettingsPopupWidget : public QWidget
|
||||||
public:
|
public:
|
||||||
explicit SettingsPopupWidget(QWidget *parent = nullptr);
|
explicit SettingsPopupWidget(QWidget *parent = nullptr);
|
||||||
void addSettingsWidget(QWidget *toAdd) const;
|
void addSettingsWidget(QWidget *toAdd) const;
|
||||||
|
void removeSettingsWidget(QWidget *toRemove) const;
|
||||||
void adjustSizeToFitScreen();
|
void adjustSizeToFitScreen();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ void VisualDatabaseDisplayFilterSaveLoadWidget::refreshFilterList()
|
||||||
|
|
||||||
// Refresh the filter file list
|
// Refresh the filter file list
|
||||||
QDir dir(SettingsCache::instance().getFiltersPath());
|
QDir dir(SettingsCache::instance().getFiltersPath());
|
||||||
QStringList filterFiles = dir.entryList(QStringList() << "*.json", QDir::Files, QDir::Time);
|
QStringList filterFiles = dir.entryList(QStringList() << "*.json", QDir::Files, QDir::Name);
|
||||||
|
|
||||||
// Loop through the filter files and create widgets for them
|
// Loop through the filter files and create widgets for them
|
||||||
for (const QString &filename : filterFiles) {
|
for (const QString &filename : filterFiles) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "visual_database_display_name_filter_widget.h"
|
#include "visual_database_display_name_filter_widget.h"
|
||||||
|
|
||||||
|
#include "../../../../dialogs/dlg_load_deck_from_clipboard.h"
|
||||||
#include "../../../tabs/abstract_tab_deck_editor.h"
|
#include "../../../tabs/abstract_tab_deck_editor.h"
|
||||||
|
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
|
|
@ -35,6 +36,13 @@ VisualDatabaseDisplayNameFilterWidget::VisualDatabaseDisplayNameFilterWidget(QWi
|
||||||
layout->addWidget(loadFromDeckButton);
|
layout->addWidget(loadFromDeckButton);
|
||||||
|
|
||||||
connect(loadFromDeckButton, &QPushButton::clicked, this, &VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck);
|
connect(loadFromDeckButton, &QPushButton::clicked, this, &VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck);
|
||||||
|
|
||||||
|
loadFromClipboardButton = new QPushButton(this);
|
||||||
|
layout->addWidget(loadFromClipboardButton);
|
||||||
|
|
||||||
|
connect(loadFromClipboardButton, &QPushButton::clicked, this,
|
||||||
|
&VisualDatabaseDisplayNameFilterWidget::actLoadFromClipboard);
|
||||||
|
|
||||||
connect(filterModel, &FilterTreeModel::layoutChanged, this,
|
connect(filterModel, &FilterTreeModel::layoutChanged, this,
|
||||||
[this]() { QTimer::singleShot(100, this, &VisualDatabaseDisplayNameFilterWidget::syncWithFilterModel); });
|
[this]() { QTimer::singleShot(100, this, &VisualDatabaseDisplayNameFilterWidget::syncWithFilterModel); });
|
||||||
|
|
||||||
|
|
@ -46,6 +54,8 @@ void VisualDatabaseDisplayNameFilterWidget::retranslateUi()
|
||||||
searchBox->setPlaceholderText(tr("Filter by name..."));
|
searchBox->setPlaceholderText(tr("Filter by name..."));
|
||||||
loadFromDeckButton->setText(tr("Load from Deck"));
|
loadFromDeckButton->setText(tr("Load from Deck"));
|
||||||
loadFromDeckButton->setToolTip(tr("Apply all card names in currently loaded deck as exact match name filters"));
|
loadFromDeckButton->setToolTip(tr("Apply all card names in currently loaded deck as exact match name filters"));
|
||||||
|
loadFromClipboardButton->setText(tr("Load from Clipboard"));
|
||||||
|
loadFromClipboardButton->setToolTip(tr("Apply all card names in clipboard as exact match name filters"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck()
|
void VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck()
|
||||||
|
|
@ -75,6 +85,20 @@ void VisualDatabaseDisplayNameFilterWidget::actLoadFromDeck()
|
||||||
updateFilterModel();
|
updateFilterModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VisualDatabaseDisplayNameFilterWidget::actLoadFromClipboard()
|
||||||
|
{
|
||||||
|
DlgLoadDeckFromClipboard dlg(this);
|
||||||
|
if (!dlg.exec())
|
||||||
|
return;
|
||||||
|
|
||||||
|
QStringList cardsInClipboard = dlg.getDeckList()->getCardList();
|
||||||
|
for (QString cardName : cardsInClipboard) {
|
||||||
|
createNameFilter(cardName);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFilterModel();
|
||||||
|
}
|
||||||
|
|
||||||
void VisualDatabaseDisplayNameFilterWidget::createNameFilter(const QString &name)
|
void VisualDatabaseDisplayNameFilterWidget::createNameFilter(const QString &name)
|
||||||
{
|
{
|
||||||
if (activeFilters.contains(name))
|
if (activeFilters.contains(name))
|
||||||
|
|
@ -85,7 +109,11 @@ void VisualDatabaseDisplayNameFilterWidget::createNameFilter(const QString &name
|
||||||
button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }"
|
button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }"
|
||||||
"QPushButton:hover { background-color: red; color: white; }");
|
"QPushButton:hover { background-color: red; color: white; }");
|
||||||
|
|
||||||
connect(button, &QPushButton::clicked, this, [this, name]() { removeNameFilter(name); });
|
connect(button, &QPushButton::clicked, this, [this, name]() {
|
||||||
|
removeNameFilter(name);
|
||||||
|
QTimer::singleShot(0, this,
|
||||||
|
&VisualDatabaseDisplayNameFilterWidget::updateFilterModel); // Avoid concurrent modification
|
||||||
|
});
|
||||||
|
|
||||||
flowWidget->addWidget(button);
|
flowWidget->addWidget(button);
|
||||||
activeFilters[name] = button;
|
activeFilters[name] = button;
|
||||||
|
|
@ -96,9 +124,6 @@ void VisualDatabaseDisplayNameFilterWidget::removeNameFilter(const QString &name
|
||||||
if (activeFilters.contains(name)) {
|
if (activeFilters.contains(name)) {
|
||||||
activeFilters[name]->deleteLater(); // Safe deletion
|
activeFilters[name]->deleteLater(); // Safe deletion
|
||||||
activeFilters.remove(name);
|
activeFilters.remove(name);
|
||||||
|
|
||||||
QTimer::singleShot(0, this,
|
|
||||||
&VisualDatabaseDisplayNameFilterWidget::updateFilterModel); // Avoid concurrent modification
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,11 +35,13 @@ private:
|
||||||
QLineEdit *searchBox;
|
QLineEdit *searchBox;
|
||||||
FlowWidget *flowWidget;
|
FlowWidget *flowWidget;
|
||||||
QPushButton *loadFromDeckButton;
|
QPushButton *loadFromDeckButton;
|
||||||
|
QPushButton *loadFromClipboardButton;
|
||||||
|
|
||||||
QMap<QString, QPushButton *> activeFilters; // Store active name filter buttons
|
QMap<QString, QPushButton *> activeFilters; // Store active name filter buttons
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void actLoadFromDeck();
|
void actLoadFromDeck();
|
||||||
|
void actLoadFromClipboard();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // VISUAL_DATABASE_DISPLAY_NAME_FILTER_WIDGET_H
|
#endif // VISUAL_DATABASE_DISPLAY_NAME_FILTER_WIDGET_H
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,15 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
||||||
clearFilterWidget = new QToolButton();
|
clearFilterWidget = new QToolButton();
|
||||||
clearFilterWidget->setFixedSize(32, 32);
|
clearFilterWidget->setFixedSize(32, 32);
|
||||||
clearFilterWidget->setIcon(QPixmap("theme:icons/delete"));
|
clearFilterWidget->setIcon(QPixmap("theme:icons/delete"));
|
||||||
connect(clearFilterWidget, &QToolButton::clicked, this, [this] { filterModel->clear(); });
|
connect(clearFilterWidget, &QToolButton::clicked, this, [this] {
|
||||||
|
filterModel->blockSignals(true);
|
||||||
|
filterModel->filterTree()->blockSignals(true);
|
||||||
|
filterModel->clear();
|
||||||
|
filterModel->blockSignals(false);
|
||||||
|
filterModel->filterTree()->blockSignals(false);
|
||||||
|
emit filterModel->filterTree()->changed();
|
||||||
|
emit filterModel->layoutChanged();
|
||||||
|
});
|
||||||
|
|
||||||
quickFilterSaveLoadWidget = new SettingsButtonWidget(this);
|
quickFilterSaveLoadWidget = new SettingsButtonWidget(this);
|
||||||
quickFilterSaveLoadWidget->setButtonIcon(QPixmap("theme:icons/lock"));
|
quickFilterSaveLoadWidget->setButtonIcon(QPixmap("theme:icons/lock"));
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,7 @@ void VisualDeckEditorWidget::retranslateUi()
|
||||||
searchPushButton->setToolTip(tr("Search for closest match in the database (with auto-suggestions) and add "
|
searchPushButton->setToolTip(tr("Search for closest match in the database (with auto-suggestions) and add "
|
||||||
"preferred printing to the deck on pressing enter"));
|
"preferred printing to the deck on pressing enter"));
|
||||||
sortCriteriaButton->setToolTip(tr("Configure how cards are sorted within their groups"));
|
sortCriteriaButton->setToolTip(tr("Configure how cards are sorted within their groups"));
|
||||||
displayTypeButton->setText(tr("Flat Layout"));
|
displayTypeButton->setText(tr("Overlap Layout"));
|
||||||
displayTypeButton->setToolTip(
|
displayTypeButton->setToolTip(
|
||||||
tr("Change how cards are displayed within zones (i.e. overlapped or fully visible.)"));
|
tr("Change how cards are displayed within zones (i.e. overlapped or fully visible.)"));
|
||||||
}
|
}
|
||||||
|
|
@ -212,14 +212,13 @@ void VisualDeckEditorWidget::updateDisplayType()
|
||||||
// Update UI and emit signal
|
// Update UI and emit signal
|
||||||
switch (currentDisplayType) {
|
switch (currentDisplayType) {
|
||||||
case DisplayType::Flat:
|
case DisplayType::Flat:
|
||||||
emit displayTypeChanged("flat");
|
|
||||||
displayTypeButton->setText(tr("Flat Layout"));
|
displayTypeButton->setText(tr("Flat Layout"));
|
||||||
break;
|
break;
|
||||||
case DisplayType::Overlap:
|
case DisplayType::Overlap:
|
||||||
emit displayTypeChanged("overlap");
|
|
||||||
displayTypeButton->setText(tr("Overlap Layout"));
|
displayTypeButton->setText(tr("Overlap Layout"));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
emit displayTypeChanged(currentDisplayType);
|
||||||
}
|
}
|
||||||
|
|
||||||
void VisualDeckEditorWidget::addZoneIfDoesNotExist()
|
void VisualDeckEditorWidget::addZoneIfDoesNotExist()
|
||||||
|
|
@ -239,8 +238,9 @@ void VisualDeckEditorWidget::addZoneIfDoesNotExist()
|
||||||
if (found) {
|
if (found) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
DeckCardZoneDisplayWidget *zoneDisplayWidget = new DeckCardZoneDisplayWidget(
|
DeckCardZoneDisplayWidget *zoneDisplayWidget =
|
||||||
zoneContainer, deckListModel, zone, activeGroupCriteria, activeSortCriteria, 20, 10, cardSizeWidget);
|
new DeckCardZoneDisplayWidget(zoneContainer, deckListModel, zone, activeGroupCriteria, activeSortCriteria,
|
||||||
|
currentDisplayType, 20, 10, cardSizeWidget);
|
||||||
connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardHovered, this, &VisualDeckEditorWidget::onHover);
|
connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardHovered, this, &VisualDeckEditorWidget::onHover);
|
||||||
connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardClicked, this, &VisualDeckEditorWidget::onCardClick);
|
connect(zoneDisplayWidget, &DeckCardZoneDisplayWidget::cardClicked, this, &VisualDeckEditorWidget::onCardClick);
|
||||||
connect(this, &VisualDeckEditorWidget::activeSortCriteriaChanged, zoneDisplayWidget,
|
connect(this, &VisualDeckEditorWidget::activeSortCriteriaChanged, zoneDisplayWidget,
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ signals:
|
||||||
void activeSortCriteriaChanged(QStringList activeSortCriteria);
|
void activeSortCriteriaChanged(QStringList activeSortCriteria);
|
||||||
void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName);
|
void cardClicked(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName);
|
||||||
void cardAdditionRequested(CardInfoPtr card);
|
void cardAdditionRequested(CardInfoPtr card);
|
||||||
void displayTypeChanged(QString displayType);
|
void displayTypeChanged(DisplayType displayType);
|
||||||
|
|
||||||
protected slots:
|
protected slots:
|
||||||
void onHover(CardInfoPtr hoveredCard);
|
void onHover(CardInfoPtr hoveredCard);
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
#include <QDirIterator>
|
#include <QDirIterator>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QMessageBox>
|
||||||
|
|
||||||
DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckList *_deckList)
|
DeckPreviewDeckTagsDisplayWidget::DeckPreviewDeckTagsDisplayWidget(QWidget *_parent, DeckList *_deckList)
|
||||||
: QWidget(_parent), deckList(nullptr)
|
: QWidget(_parent), deckList(nullptr)
|
||||||
|
|
@ -77,6 +78,21 @@ static QStringList getAllFiles(const QString &filePath)
|
||||||
return allFiles;
|
return allFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
|
||||||
|
{
|
||||||
|
QFileInfo fileInfo(filePath);
|
||||||
|
QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod");
|
||||||
|
|
||||||
|
if (QFile::exists(newFileName)) {
|
||||||
|
QMessageBox::StandardButton reply =
|
||||||
|
QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"),
|
||||||
|
QObject::tr("A .cod version of this deck already exists. Overwrite it?"),
|
||||||
|
QMessageBox::Yes | QMessageBox::No);
|
||||||
|
return reply == QMessageBox::Yes;
|
||||||
|
}
|
||||||
|
return true; // Safe to proceed
|
||||||
|
}
|
||||||
|
|
||||||
void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
|
void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
|
||||||
{
|
{
|
||||||
if (qobject_cast<DeckPreviewWidget *>(parentWidget())) {
|
if (qobject_cast<DeckPreviewWidget *>(parentWidget())) {
|
||||||
|
|
@ -91,6 +107,10 @@ void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
|
||||||
// Retrieve saved preference if the prompt is disabled
|
// Retrieve saved preference if the prompt is disabled
|
||||||
if (!SettingsCache::instance().getVisualDeckStoragePromptForConversion()) {
|
if (!SettingsCache::instance().getVisualDeckStoragePromptForConversion()) {
|
||||||
if (SettingsCache::instance().getVisualDeckStorageAlwaysConvert()) {
|
if (SettingsCache::instance().getVisualDeckStorageAlwaysConvert()) {
|
||||||
|
|
||||||
|
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath))
|
||||||
|
return;
|
||||||
|
|
||||||
deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath);
|
deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath);
|
||||||
deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastFileName();
|
deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastFileName();
|
||||||
deckPreviewWidget->refreshBannerCardText();
|
deckPreviewWidget->refreshBannerCardText();
|
||||||
|
|
@ -100,6 +120,10 @@ void DeckPreviewDeckTagsDisplayWidget::openTagEditDlg()
|
||||||
// Show the dialog to the user
|
// Show the dialog to the user
|
||||||
DialogConvertDeckToCodFormat conversionDialog(parentWidget());
|
DialogConvertDeckToCodFormat conversionDialog(parentWidget());
|
||||||
if (conversionDialog.exec() == QDialog::Accepted) {
|
if (conversionDialog.exec() == QDialog::Accepted) {
|
||||||
|
|
||||||
|
if (!confirmOverwriteIfExists(this, deckPreviewWidget->filePath))
|
||||||
|
return;
|
||||||
|
|
||||||
deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath);
|
deckPreviewWidget->deckLoader->convertToCockatriceFormat(deckPreviewWidget->filePath);
|
||||||
deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastFileName();
|
deckPreviewWidget->filePath = deckPreviewWidget->deckLoader->getLastFileName();
|
||||||
deckPreviewWidget->refreshBannerCardText();
|
deckPreviewWidget->refreshBannerCardText();
|
||||||
|
|
|
||||||
|
|
@ -50,10 +50,10 @@ void DeckPreviewTagDisplayWidget::mousePressEvent(QMouseEvent *event)
|
||||||
{
|
{
|
||||||
switch (event->button()) {
|
switch (event->button()) {
|
||||||
case Qt::LeftButton:
|
case Qt::LeftButton:
|
||||||
setState(TagState::Selected);
|
setState(state != TagState::Selected ? TagState::Selected : TagState::NotSelected);
|
||||||
break;
|
break;
|
||||||
case Qt::RightButton:
|
case Qt::RightButton:
|
||||||
setState(TagState::Excluded);
|
setState(state != TagState::Excluded ? TagState::Excluded : TagState::NotSelected);
|
||||||
break;
|
break;
|
||||||
case Qt::MiddleButton:
|
case Qt::MiddleButton:
|
||||||
setState(TagState::NotSelected);
|
setState(TagState::NotSelected);
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent,
|
||||||
&DeckPreviewWidget::updateTagsVisibility);
|
&DeckPreviewWidget::updateTagsVisibility);
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::visualDeckStorageShowBannerCardComboBoxChanged, this,
|
connect(&SettingsCache::instance(), &SettingsCache::visualDeckStorageShowBannerCardComboBoxChanged, this,
|
||||||
&DeckPreviewWidget::updateBannerCardComboBoxVisibility);
|
&DeckPreviewWidget::updateBannerCardComboBoxVisibility);
|
||||||
|
connect(visualDeckStorageWidget->settings(), &VisualDeckStorageQuickSettingsWidget::deckPreviewTooltipChanged, this,
|
||||||
|
&DeckPreviewWidget::refreshBannerCardToolTip);
|
||||||
|
|
||||||
layout->addWidget(bannerCardDisplayWidget);
|
layout->addWidget(bannerCardDisplayWidget);
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +81,6 @@ void DeckPreviewWidget::initializeUi(const bool deckLoadSuccess)
|
||||||
|
|
||||||
bannerCardDisplayWidget->setCard(bannerCard);
|
bannerCardDisplayWidget->setCard(bannerCard);
|
||||||
bannerCardDisplayWidget->setFontSize(24);
|
bannerCardDisplayWidget->setFontSize(24);
|
||||||
refreshBannerCardText();
|
|
||||||
setFilePath(deckLoader->getLastFileName());
|
setFilePath(deckLoader->getLastFileName());
|
||||||
|
|
||||||
colorIdentityWidget = new ColorIdentityWidget(this, getColorIdentity());
|
colorIdentityWidget = new ColorIdentityWidget(this, getColorIdentity());
|
||||||
|
|
@ -104,6 +105,8 @@ void DeckPreviewWidget::initializeUi(const bool deckLoadSuccess)
|
||||||
layout->addWidget(bannerCardLabel);
|
layout->addWidget(bannerCardLabel);
|
||||||
layout->addWidget(bannerCardComboBox);
|
layout->addWidget(bannerCardComboBox);
|
||||||
|
|
||||||
|
refreshBannerCardText();
|
||||||
|
|
||||||
retranslateUi();
|
retranslateUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,15 +182,42 @@ QString DeckPreviewWidget::getColorIdentity()
|
||||||
return colorIdentity;
|
return colorIdentity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The display name is given by the deck name, or the filename if the deck name is not set.
|
||||||
|
*/
|
||||||
|
QString DeckPreviewWidget::getDisplayName() const
|
||||||
|
{
|
||||||
|
return deckLoader->getName().isEmpty() ? QFileInfo(deckLoader->getLastFileName()).fileName()
|
||||||
|
: deckLoader->getName();
|
||||||
|
}
|
||||||
|
|
||||||
void DeckPreviewWidget::setFilePath(const QString &_filePath)
|
void DeckPreviewWidget::setFilePath(const QString &_filePath)
|
||||||
{
|
{
|
||||||
filePath = _filePath;
|
filePath = _filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the banner card text.
|
||||||
|
* This also calls `refreshBannerCardToolTip`, since those two often need to be updated together.
|
||||||
|
*/
|
||||||
void DeckPreviewWidget::refreshBannerCardText()
|
void DeckPreviewWidget::refreshBannerCardText()
|
||||||
{
|
{
|
||||||
bannerCardDisplayWidget->setOverlayText(
|
bannerCardDisplayWidget->setOverlayText(getDisplayName());
|
||||||
deckLoader->getName().isEmpty() ? QFileInfo(deckLoader->getLastFileName()).fileName() : deckLoader->getName());
|
|
||||||
|
refreshBannerCardToolTip();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckPreviewWidget::refreshBannerCardToolTip()
|
||||||
|
{
|
||||||
|
auto type = visualDeckStorageWidget->settings()->getDeckPreviewTooltip();
|
||||||
|
switch (type) {
|
||||||
|
case VisualDeckStorageQuickSettingsWidget::TooltipType::None:
|
||||||
|
bannerCardDisplayWidget->setToolTip("");
|
||||||
|
break;
|
||||||
|
case VisualDeckStorageQuickSettingsWidget::TooltipType::Filepath:
|
||||||
|
bannerCardDisplayWidget->setToolTip(filePath);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeckPreviewWidget::updateBannerCardComboBox()
|
void DeckPreviewWidget::updateBannerCardComboBox()
|
||||||
|
|
@ -260,11 +290,11 @@ void DeckPreviewWidget::updateBannerCardComboBox()
|
||||||
|
|
||||||
void DeckPreviewWidget::setBannerCard(int /* changedIndex */)
|
void DeckPreviewWidget::setBannerCard(int /* changedIndex */)
|
||||||
{
|
{
|
||||||
QVariant itemData = bannerCardComboBox->itemData(bannerCardComboBox->currentIndex());
|
auto nameAndId = bannerCardComboBox->currentData().value<QPair<QString, QString>>();
|
||||||
deckLoader->setBannerCard(QPair<QString, QString>(bannerCardComboBox->currentText(), itemData.toString()));
|
deckLoader->setBannerCard(nameAndId);
|
||||||
deckLoader->saveToFile(filePath, DeckLoader::getFormatFromName(filePath));
|
deckLoader->saveToFile(filePath, DeckLoader::getFormatFromName(filePath));
|
||||||
bannerCardDisplayWidget->setCard(CardDatabaseManager::getInstance()->getCardByNameAndProviderId(
|
bannerCardDisplayWidget->setCard(
|
||||||
bannerCardComboBox->currentText(), itemData.toString()));
|
CardDatabaseManager::getInstance()->getCardByNameAndProviderId(nameAndId.first, nameAndId.second));
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeckPreviewWidget::imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance)
|
void DeckPreviewWidget::imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance)
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ public:
|
||||||
const QString &_filePath);
|
const QString &_filePath);
|
||||||
void retranslateUi();
|
void retranslateUi();
|
||||||
QString getColorIdentity();
|
QString getColorIdentity();
|
||||||
|
QString getDisplayName() const;
|
||||||
|
|
||||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||||
QVBoxLayout *layout;
|
QVBoxLayout *layout;
|
||||||
|
|
@ -49,6 +50,7 @@ signals:
|
||||||
public slots:
|
public slots:
|
||||||
void setFilePath(const QString &filePath);
|
void setFilePath(const QString &filePath);
|
||||||
void refreshBannerCardText();
|
void refreshBannerCardText();
|
||||||
|
void refreshBannerCardToolTip();
|
||||||
void updateBannerCardComboBox();
|
void updateBannerCardComboBox();
|
||||||
void setBannerCard(int);
|
void setBannerCard(int);
|
||||||
void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include "visual_deck_storage_widget.h"
|
#include "visual_deck_storage_widget.h"
|
||||||
|
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
|
#include <QComboBox>
|
||||||
#include <QSpinBox>
|
#include <QSpinBox>
|
||||||
|
|
||||||
VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidget *parent)
|
VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidget *parent)
|
||||||
|
|
@ -41,14 +42,6 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg
|
||||||
connect(showBannerCardComboBoxCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
|
connect(showBannerCardComboBoxCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
|
||||||
&SettingsCache::setVisualDeckStorageShowBannerCardComboBox);
|
&SettingsCache::setVisualDeckStorageShowBannerCardComboBox);
|
||||||
|
|
||||||
// search folder names checkbox
|
|
||||||
searchFolderNamesCheckBox = new QCheckBox(this);
|
|
||||||
searchFolderNamesCheckBox->setChecked(SettingsCache::instance().getVisualDeckStorageSearchFolderNames());
|
|
||||||
connect(searchFolderNamesCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
|
|
||||||
&VisualDeckStorageQuickSettingsWidget::searchFolderNamesChanged);
|
|
||||||
connect(searchFolderNamesCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(),
|
|
||||||
&SettingsCache::setVisualDeckStorageSearchFolderNames);
|
|
||||||
|
|
||||||
// draw unused color identities checkbox
|
// draw unused color identities checkbox
|
||||||
drawUnusedColorIdentitiesCheckBox = new QCheckBox(this);
|
drawUnusedColorIdentitiesCheckBox = new QCheckBox(this);
|
||||||
drawUnusedColorIdentitiesCheckBox->setChecked(
|
drawUnusedColorIdentitiesCheckBox->setChecked(
|
||||||
|
|
@ -80,6 +73,26 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg
|
||||||
unusedColorIdentityOpacityLayout->addWidget(unusedColorIdentitiesOpacityLabel);
|
unusedColorIdentityOpacityLayout->addWidget(unusedColorIdentitiesOpacityLabel);
|
||||||
unusedColorIdentityOpacityLayout->addWidget(unusedColorIdentitiesOpacitySpinBox);
|
unusedColorIdentityOpacityLayout->addWidget(unusedColorIdentitiesOpacitySpinBox);
|
||||||
|
|
||||||
|
// tooltip selector
|
||||||
|
auto deckPreviewTooltipWidget = new QWidget(this);
|
||||||
|
|
||||||
|
deckPreviewTooltipLabel = new QLabel(deckPreviewTooltipWidget);
|
||||||
|
deckPreviewTooltipComboBox = new QComboBox(deckPreviewTooltipWidget);
|
||||||
|
deckPreviewTooltipComboBox->setFocusPolicy(Qt::StrongFocus);
|
||||||
|
deckPreviewTooltipComboBox->addItem("", TooltipType::None);
|
||||||
|
deckPreviewTooltipComboBox->addItem("", TooltipType::Filepath);
|
||||||
|
|
||||||
|
deckPreviewTooltipComboBox->setCurrentIndex(SettingsCache::instance().getVisualDeckStorageTooltipType());
|
||||||
|
connect(deckPreviewTooltipComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||||
|
[this] { emit deckPreviewTooltipChanged(getDeckPreviewTooltip()); });
|
||||||
|
connect(deckPreviewTooltipComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), &SettingsCache::instance(),
|
||||||
|
&SettingsCache::setVisualDeckStorageTooltipType);
|
||||||
|
|
||||||
|
auto deckPreviewTooltipLayout = new QHBoxLayout(deckPreviewTooltipWidget);
|
||||||
|
deckPreviewTooltipLayout->setContentsMargins(11, 0, 11, 0);
|
||||||
|
deckPreviewTooltipLayout->addWidget(deckPreviewTooltipLabel);
|
||||||
|
deckPreviewTooltipLayout->addWidget(deckPreviewTooltipComboBox);
|
||||||
|
|
||||||
// card size slider
|
// card size slider
|
||||||
cardSizeWidget = new CardSizeWidget(this, nullptr, SettingsCache::instance().getVisualDeckStorageCardSize());
|
cardSizeWidget = new CardSizeWidget(this, nullptr, SettingsCache::instance().getVisualDeckStorageCardSize());
|
||||||
connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, this,
|
connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, this,
|
||||||
|
|
@ -92,9 +105,9 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg
|
||||||
this->addSettingsWidget(showTagFilterCheckBox);
|
this->addSettingsWidget(showTagFilterCheckBox);
|
||||||
this->addSettingsWidget(showTagsOnDeckPreviewsCheckBox);
|
this->addSettingsWidget(showTagsOnDeckPreviewsCheckBox);
|
||||||
this->addSettingsWidget(showBannerCardComboBoxCheckBox);
|
this->addSettingsWidget(showBannerCardComboBoxCheckBox);
|
||||||
this->addSettingsWidget(searchFolderNamesCheckBox);
|
|
||||||
this->addSettingsWidget(drawUnusedColorIdentitiesCheckBox);
|
this->addSettingsWidget(drawUnusedColorIdentitiesCheckBox);
|
||||||
this->addSettingsWidget(unusedColorIdentityOpacityWidget);
|
this->addSettingsWidget(unusedColorIdentityOpacityWidget);
|
||||||
|
this->addSettingsWidget(deckPreviewTooltipWidget);
|
||||||
this->addSettingsWidget(cardSizeWidget);
|
this->addSettingsWidget(cardSizeWidget);
|
||||||
|
|
||||||
connect(&SettingsCache::instance(), &SettingsCache::langChanged, this,
|
connect(&SettingsCache::instance(), &SettingsCache::langChanged, this,
|
||||||
|
|
@ -108,10 +121,13 @@ void VisualDeckStorageQuickSettingsWidget::retranslateUi()
|
||||||
showTagFilterCheckBox->setText(tr("Show Tag Filter"));
|
showTagFilterCheckBox->setText(tr("Show Tag Filter"));
|
||||||
showTagsOnDeckPreviewsCheckBox->setText(tr("Show Tags On Deck Previews"));
|
showTagsOnDeckPreviewsCheckBox->setText(tr("Show Tags On Deck Previews"));
|
||||||
showBannerCardComboBoxCheckBox->setText(tr("Show Banner Card Selection Option"));
|
showBannerCardComboBoxCheckBox->setText(tr("Show Banner Card Selection Option"));
|
||||||
searchFolderNamesCheckBox->setText(tr("Include Folder Names in Search"));
|
|
||||||
drawUnusedColorIdentitiesCheckBox->setText(tr("Draw unused Color Identities"));
|
drawUnusedColorIdentitiesCheckBox->setText(tr("Draw unused Color Identities"));
|
||||||
unusedColorIdentitiesOpacityLabel->setText(tr("Unused Color Identities Opacity"));
|
unusedColorIdentitiesOpacityLabel->setText(tr("Unused Color Identities Opacity"));
|
||||||
unusedColorIdentitiesOpacitySpinBox->setSuffix("%");
|
unusedColorIdentitiesOpacitySpinBox->setSuffix("%");
|
||||||
|
|
||||||
|
deckPreviewTooltipLabel->setText(tr("Deck tooltip:"));
|
||||||
|
deckPreviewTooltipComboBox->setItemText(0, tr("None"));
|
||||||
|
deckPreviewTooltipComboBox->setItemText(1, tr("Filepath"));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VisualDeckStorageQuickSettingsWidget::getShowFolders() const
|
bool VisualDeckStorageQuickSettingsWidget::getShowFolders() const
|
||||||
|
|
@ -139,16 +155,16 @@ bool VisualDeckStorageQuickSettingsWidget::getShowTagsOnDeckPreviews() const
|
||||||
return showTagsOnDeckPreviewsCheckBox->isChecked();
|
return showTagsOnDeckPreviewsCheckBox->isChecked();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VisualDeckStorageQuickSettingsWidget::getSearchFolderNames() const
|
|
||||||
{
|
|
||||||
return searchFolderNamesCheckBox->isChecked();
|
|
||||||
}
|
|
||||||
|
|
||||||
int VisualDeckStorageQuickSettingsWidget::getUnusedColorIdentitiesOpacity() const
|
int VisualDeckStorageQuickSettingsWidget::getUnusedColorIdentitiesOpacity() const
|
||||||
{
|
{
|
||||||
return unusedColorIdentitiesOpacitySpinBox->value();
|
return unusedColorIdentitiesOpacitySpinBox->value();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VisualDeckStorageQuickSettingsWidget::TooltipType VisualDeckStorageQuickSettingsWidget::getDeckPreviewTooltip() const
|
||||||
|
{
|
||||||
|
return deckPreviewTooltipComboBox->currentData().value<TooltipType>();
|
||||||
|
}
|
||||||
|
|
||||||
int VisualDeckStorageQuickSettingsWidget::getCardSize() const
|
int VisualDeckStorageQuickSettingsWidget::getCardSize() const
|
||||||
{
|
{
|
||||||
return cardSizeWidget->getSlider()->value();
|
return cardSizeWidget->getSlider()->value();
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ class CardSizeWidget;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
class QSpinBox;
|
class QSpinBox;
|
||||||
class QCheckBox;
|
class QCheckBox;
|
||||||
|
class QComboBox;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The VDS's quick settings menu.
|
* The VDS's quick settings menu.
|
||||||
|
|
@ -22,12 +23,23 @@ class VisualDeckStorageQuickSettingsWidget : public SettingsButtonWidget
|
||||||
QCheckBox *showBannerCardComboBoxCheckBox;
|
QCheckBox *showBannerCardComboBoxCheckBox;
|
||||||
QCheckBox *showTagFilterCheckBox;
|
QCheckBox *showTagFilterCheckBox;
|
||||||
QCheckBox *showTagsOnDeckPreviewsCheckBox;
|
QCheckBox *showTagsOnDeckPreviewsCheckBox;
|
||||||
QCheckBox *searchFolderNamesCheckBox;
|
|
||||||
QLabel *unusedColorIdentitiesOpacityLabel;
|
QLabel *unusedColorIdentitiesOpacityLabel;
|
||||||
QSpinBox *unusedColorIdentitiesOpacitySpinBox;
|
QSpinBox *unusedColorIdentitiesOpacitySpinBox;
|
||||||
|
QLabel *deckPreviewTooltipLabel;
|
||||||
|
QComboBox *deckPreviewTooltipComboBox;
|
||||||
CardSizeWidget *cardSizeWidget;
|
CardSizeWidget *cardSizeWidget;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
/**
|
||||||
|
* The info to display in the deck preview's banner card tooltip.
|
||||||
|
*/
|
||||||
|
enum TooltipType
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Filepath
|
||||||
|
};
|
||||||
|
Q_ENUM(TooltipType)
|
||||||
|
|
||||||
explicit VisualDeckStorageQuickSettingsWidget(QWidget *parent = nullptr);
|
explicit VisualDeckStorageQuickSettingsWidget(QWidget *parent = nullptr);
|
||||||
|
|
||||||
void retranslateUi();
|
void retranslateUi();
|
||||||
|
|
@ -37,8 +49,8 @@ public:
|
||||||
bool getShowBannerCardComboBox() const;
|
bool getShowBannerCardComboBox() const;
|
||||||
bool getShowTagFilter() const;
|
bool getShowTagFilter() const;
|
||||||
bool getShowTagsOnDeckPreviews() const;
|
bool getShowTagsOnDeckPreviews() const;
|
||||||
bool getSearchFolderNames() const;
|
|
||||||
int getUnusedColorIdentitiesOpacity() const;
|
int getUnusedColorIdentitiesOpacity() const;
|
||||||
|
TooltipType getDeckPreviewTooltip() const;
|
||||||
int getCardSize() const;
|
int getCardSize() const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
|
@ -47,8 +59,8 @@ signals:
|
||||||
void showBannerCardComboBoxChanged(bool enabled);
|
void showBannerCardComboBoxChanged(bool enabled);
|
||||||
void showTagFilterChanged(bool enabled);
|
void showTagFilterChanged(bool enabled);
|
||||||
void showTagsOnDeckPreviewsChanged(bool enabled);
|
void showTagsOnDeckPreviewsChanged(bool enabled);
|
||||||
void searchFolderNamesChanged(bool enabled);
|
|
||||||
void unusedColorIdentitiesOpacityChanged(int opacity);
|
void unusedColorIdentitiesOpacityChanged(int opacity);
|
||||||
|
void deckPreviewTooltipChanged(TooltipType tooltip);
|
||||||
void cardSizeChanged(int scale);
|
void cardSizeChanged(int scale);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
#include "visual_deck_storage_search_widget.h"
|
#include "visual_deck_storage_search_widget.h"
|
||||||
|
|
||||||
|
#include "../../../../game/filters/deck_filter_string.h"
|
||||||
|
#include "../../../../game/filters/syntax_help.h"
|
||||||
#include "../../../../settings/cache_settings.h"
|
#include "../../../../settings/cache_settings.h"
|
||||||
|
#include "../../pixel_map_generator.h"
|
||||||
|
|
||||||
|
#include <QAction>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Constructs a PrintingSelectorCardSearchWidget for searching cards by set name or set code.
|
* @brief Constructs a PrintingSelectorCardSearchWidget for searching cards by set name or set code.
|
||||||
|
|
@ -17,7 +22,13 @@ VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(VisualDeckStorageWi
|
||||||
setLayout(layout);
|
setLayout(layout);
|
||||||
|
|
||||||
searchBar = new QLineEdit(this);
|
searchBar = new QLineEdit(this);
|
||||||
searchBar->setPlaceholderText(tr("Search by filename"));
|
searchBar->setPlaceholderText(tr("Search by filename (or search expression)"));
|
||||||
|
searchBar->setClearButtonEnabled(true);
|
||||||
|
searchBar->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
|
||||||
|
|
||||||
|
auto help = searchBar->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition);
|
||||||
|
connect(help, &QAction::triggered, this, [this] { createDeckSearchSyntaxHelpWindow(searchBar); });
|
||||||
|
|
||||||
layout->addWidget(searchBar);
|
layout->addWidget(searchBar);
|
||||||
|
|
||||||
// Add a debounce timer for the search bar to limit frequent updates
|
// Add a debounce timer for the search bar to limit frequent updates
|
||||||
|
|
@ -41,37 +52,29 @@ QString VisualDeckStorageSearchWidget::getSearchText()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the filename used for the search.
|
* Converts the filepath into a relative filepath starting from the deck folder.
|
||||||
|
* If the file isn't in the deck folder, then this will just return the filename.
|
||||||
*
|
*
|
||||||
* if includeFolderName is true, then this returns the relative filepath starting from the deck folder.
|
* @param filePath The filepath to convert into a relative filepath
|
||||||
* If the file isn't in the deck folder, or includeFolderName is false, then this will just return the filename.
|
|
||||||
*
|
|
||||||
* @param filePath The filePath to convert into a search name
|
|
||||||
*/
|
*/
|
||||||
static QString getFileSearchName(const QString &filePath, bool includeFolderName)
|
static QString toRelativeFilepath(const QString &filePath)
|
||||||
{
|
{
|
||||||
QString deckPath = SettingsCache::instance().getDeckPath();
|
QString deckPath = SettingsCache::instance().getDeckPath();
|
||||||
if (includeFolderName && filePath.startsWith(deckPath)) {
|
if (filePath.startsWith(deckPath)) {
|
||||||
return filePath.mid(deckPath.length()).toLower();
|
return filePath.mid(deckPath.length());
|
||||||
}
|
}
|
||||||
|
|
||||||
QFileInfo fileInfo(filePath);
|
QFileInfo fileInfo(filePath);
|
||||||
QString fileName = fileInfo.fileName().toLower();
|
QString fileName = fileInfo.fileName();
|
||||||
return fileName;
|
return fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
void VisualDeckStorageSearchWidget::filterWidgets(QList<DeckPreviewWidget *> widgets,
|
void VisualDeckStorageSearchWidget::filterWidgets(QList<DeckPreviewWidget *> widgets, const QString &searchText)
|
||||||
const QString &searchText,
|
|
||||||
bool includeFolderName)
|
|
||||||
{
|
{
|
||||||
if (searchText.isEmpty() || searchText.isNull()) {
|
auto filterString = DeckFilterString(searchText);
|
||||||
for (auto widget : widgets) {
|
|
||||||
widget->filteredBySearch = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto file : widgets) {
|
for (auto widget : widgets) {
|
||||||
QString fileSearchName = getFileSearchName(file->filePath, includeFolderName);
|
QString relativeFilePath = toRelativeFilepath(widget->filePath);
|
||||||
file->filteredBySearch = !fileSearchName.contains(searchText.toLower());
|
widget->filteredBySearch = !filterString.check(widget, {relativeFilePath});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ class VisualDeckStorageSearchWidget : public QWidget
|
||||||
public:
|
public:
|
||||||
explicit VisualDeckStorageSearchWidget(VisualDeckStorageWidget *parent);
|
explicit VisualDeckStorageSearchWidget(VisualDeckStorageWidget *parent);
|
||||||
QString getSearchText();
|
QString getSearchText();
|
||||||
void filterWidgets(QList<DeckPreviewWidget *> widgets, const QString &searchText, bool includeFolderName);
|
void filterWidgets(QList<DeckPreviewWidget *> widgets, const QString &searchText);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QHBoxLayout *layout;
|
QHBoxLayout *layout;
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,6 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare
|
||||||
&VisualDeckStorageWidget::updateShowFolders);
|
&VisualDeckStorageWidget::updateShowFolders);
|
||||||
connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::showTagFilterChanged, this,
|
connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::showTagFilterChanged, this,
|
||||||
&VisualDeckStorageWidget::updateTagsVisibility);
|
&VisualDeckStorageWidget::updateTagsVisibility);
|
||||||
connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::searchFolderNamesChanged, this,
|
|
||||||
&VisualDeckStorageWidget::updateSearchFilter);
|
|
||||||
|
|
||||||
searchAndSortLayout->addWidget(deckPreviewColorIdentityFilterWidget);
|
searchAndSortLayout->addWidget(deckPreviewColorIdentityFilterWidget);
|
||||||
searchAndSortLayout->addWidget(sortWidget);
|
searchAndSortLayout->addWidget(sortWidget);
|
||||||
|
|
@ -196,8 +194,7 @@ void VisualDeckStorageWidget::updateColorFilter()
|
||||||
void VisualDeckStorageWidget::updateSearchFilter()
|
void VisualDeckStorageWidget::updateSearchFilter()
|
||||||
{
|
{
|
||||||
if (folderWidget) {
|
if (folderWidget) {
|
||||||
searchWidget->filterWidgets(folderWidget->findChildren<DeckPreviewWidget *>(), searchWidget->getSearchText(),
|
searchWidget->filterWidgets(folderWidget->findChildren<DeckPreviewWidget *>(), searchWidget->getSearchText());
|
||||||
quickSettingsWidget->getSearchFolderNames());
|
|
||||||
folderWidget->updateVisibility();
|
folderWidget->updateVisibility();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1099,7 +1099,7 @@ void MainWindow::cardDatabaseLoadingFailed()
|
||||||
|
|
||||||
void MainWindow::cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames)
|
void MainWindow::cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames)
|
||||||
{
|
{
|
||||||
QMessageBox msgBox;
|
QMessageBox msgBox(this);
|
||||||
msgBox.setWindowTitle(tr("New sets found"));
|
msgBox.setWindowTitle(tr("New sets found"));
|
||||||
msgBox.setIcon(QMessageBox::Question);
|
msgBox.setIcon(QMessageBox::Question);
|
||||||
msgBox.setText(tr("%n new set(s) found in the card database\n"
|
msgBox.setText(tr("%n new set(s) found in the card database\n"
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,24 @@ DeckListModel::~DeckListModel()
|
||||||
delete root;
|
delete root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString DeckListModel::getSortCriteriaForCard(CardInfoPtr info)
|
||||||
|
{
|
||||||
|
if (!info) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (activeGroupCriteria) {
|
||||||
|
case DeckListModelGroupCriteria::MAIN_TYPE:
|
||||||
|
return info->getMainCardType();
|
||||||
|
case DeckListModelGroupCriteria::MANA_COST:
|
||||||
|
return info->getCmc();
|
||||||
|
case DeckListModelGroupCriteria::COLOR:
|
||||||
|
return info->getColors() == "" ? "Colorless" : info->getColors();
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void DeckListModel::rebuildTree()
|
void DeckListModel::rebuildTree()
|
||||||
{
|
{
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
|
|
@ -49,7 +67,7 @@ void DeckListModel::rebuildTree()
|
||||||
}
|
}
|
||||||
|
|
||||||
CardInfoPtr info = CardDatabaseManager::getInstance()->getCard(currentCard->getName());
|
CardInfoPtr info = CardDatabaseManager::getInstance()->getCard(currentCard->getName());
|
||||||
QString cardType = info ? info->getMainCardType() : "unknown";
|
QString cardType = getSortCriteriaForCard(info);
|
||||||
|
|
||||||
auto *cardTypeNode = dynamic_cast<InnerDecklistNode *>(node->findChild(cardType));
|
auto *cardTypeNode = dynamic_cast<InnerDecklistNode *>(node->findChild(cardType));
|
||||||
|
|
||||||
|
|
@ -467,6 +485,12 @@ void DeckListModel::sort(int column, Qt::SortOrder order)
|
||||||
emit layoutChanged();
|
emit layoutChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DeckListModel::setActiveGroupCriteria(DeckListModelGroupCriteria newCriteria)
|
||||||
|
{
|
||||||
|
activeGroupCriteria = newCriteria;
|
||||||
|
rebuildTree();
|
||||||
|
}
|
||||||
|
|
||||||
void DeckListModel::cleanList()
|
void DeckListModel::cleanList()
|
||||||
{
|
{
|
||||||
setDeckList(new DeckLoader);
|
setDeckList(new DeckLoader);
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,13 @@ class CardDatabase;
|
||||||
class QPrinter;
|
class QPrinter;
|
||||||
class QTextCursor;
|
class QTextCursor;
|
||||||
|
|
||||||
|
enum DeckListModelGroupCriteria
|
||||||
|
{
|
||||||
|
MAIN_TYPE,
|
||||||
|
MANA_COST,
|
||||||
|
COLOR
|
||||||
|
};
|
||||||
|
|
||||||
class DecklistModelCardNode : public AbstractDecklistCardNode
|
class DecklistModelCardNode : public AbstractDecklistCardNode
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
|
|
@ -85,6 +92,7 @@ signals:
|
||||||
public:
|
public:
|
||||||
explicit DeckListModel(QObject *parent = nullptr);
|
explicit DeckListModel(QObject *parent = nullptr);
|
||||||
~DeckListModel() override;
|
~DeckListModel() override;
|
||||||
|
QString getSortCriteriaForCard(CardInfoPtr info);
|
||||||
int rowCount(const QModelIndex &parent) const override;
|
int rowCount(const QModelIndex &parent) const override;
|
||||||
int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override;
|
int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override;
|
||||||
QVariant data(const QModelIndex &index, int role) const override;
|
QVariant data(const QModelIndex &index, int role) const override;
|
||||||
|
|
@ -113,10 +121,12 @@ public:
|
||||||
QList<CardInfoPtr> getCardsAsCardInfoPtrs() const;
|
QList<CardInfoPtr> getCardsAsCardInfoPtrs() const;
|
||||||
QList<CardInfoPtr> getCardsAsCardInfoPtrsForZone(QString zoneName) const;
|
QList<CardInfoPtr> getCardsAsCardInfoPtrsForZone(QString zoneName) const;
|
||||||
QList<QString> *getZones() const;
|
QList<QString> *getZones() const;
|
||||||
|
void setActiveGroupCriteria(DeckListModelGroupCriteria newCriteria);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
DeckLoader *deckList;
|
DeckLoader *deckList;
|
||||||
InnerDecklistNode *root;
|
InnerDecklistNode *root;
|
||||||
|
DeckListModelGroupCriteria activeGroupCriteria = DeckListModelGroupCriteria::MAIN_TYPE;
|
||||||
int lastKnownColumn;
|
int lastKnownColumn;
|
||||||
Qt::SortOrder lastKnownOrder;
|
Qt::SortOrder lastKnownOrder;
|
||||||
InnerDecklistNode *createNodeIfNeeded(const QString &name, InnerDecklistNode *parent);
|
InnerDecklistNode *createNodeIfNeeded(const QString &name, InnerDecklistNode *parent);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include "../../client/ui/picture_loader/picture_loader.h"
|
#include "../../client/ui/picture_loader/picture_loader.h"
|
||||||
#include "../../deck/deck_loader.h"
|
#include "../../deck/deck_loader.h"
|
||||||
#include "../../dialogs/dlg_load_deck.h"
|
#include "../../dialogs/dlg_load_deck.h"
|
||||||
|
#include "../../dialogs/dlg_load_deck_from_clipboard.h"
|
||||||
#include "../../dialogs/dlg_load_remote_deck.h"
|
#include "../../dialogs/dlg_load_remote_deck.h"
|
||||||
#include "../../server/pending_command.h"
|
#include "../../server/pending_command.h"
|
||||||
#include "../../settings/cache_settings.h"
|
#include "../../settings/cache_settings.h"
|
||||||
|
|
@ -52,6 +53,7 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
||||||
{
|
{
|
||||||
loadLocalButton = new QPushButton;
|
loadLocalButton = new QPushButton;
|
||||||
loadRemoteButton = new QPushButton;
|
loadRemoteButton = new QPushButton;
|
||||||
|
loadFromClipboardButton = new QPushButton;
|
||||||
unloadDeckButton = new QPushButton;
|
unloadDeckButton = new QPushButton;
|
||||||
readyStartButton = new ToggleButton;
|
readyStartButton = new ToggleButton;
|
||||||
forceStartGameButton = new QPushButton;
|
forceStartGameButton = new QPushButton;
|
||||||
|
|
@ -59,6 +61,7 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
||||||
|
|
||||||
connect(loadLocalButton, &QPushButton::clicked, this, &DeckViewContainer::loadLocalDeck);
|
connect(loadLocalButton, &QPushButton::clicked, this, &DeckViewContainer::loadLocalDeck);
|
||||||
connect(loadRemoteButton, &QPushButton::clicked, this, &DeckViewContainer::loadRemoteDeck);
|
connect(loadRemoteButton, &QPushButton::clicked, this, &DeckViewContainer::loadRemoteDeck);
|
||||||
|
connect(loadFromClipboardButton, &QPushButton::clicked, this, &DeckViewContainer::loadFromClipboard);
|
||||||
connect(readyStartButton, &QPushButton::clicked, this, &DeckViewContainer::readyStart);
|
connect(readyStartButton, &QPushButton::clicked, this, &DeckViewContainer::readyStart);
|
||||||
connect(unloadDeckButton, &QPushButton::clicked, this, &DeckViewContainer::unloadDeck);
|
connect(unloadDeckButton, &QPushButton::clicked, this, &DeckViewContainer::unloadDeck);
|
||||||
connect(forceStartGameButton, &QPushButton::clicked, this, &DeckViewContainer::forceStart);
|
connect(forceStartGameButton, &QPushButton::clicked, this, &DeckViewContainer::forceStart);
|
||||||
|
|
@ -68,6 +71,7 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
||||||
auto *buttonHBox = new QHBoxLayout;
|
auto *buttonHBox = new QHBoxLayout;
|
||||||
buttonHBox->addWidget(loadLocalButton);
|
buttonHBox->addWidget(loadLocalButton);
|
||||||
buttonHBox->addWidget(loadRemoteButton);
|
buttonHBox->addWidget(loadRemoteButton);
|
||||||
|
buttonHBox->addWidget(loadFromClipboardButton);
|
||||||
buttonHBox->addWidget(unloadDeckButton);
|
buttonHBox->addWidget(unloadDeckButton);
|
||||||
buttonHBox->addWidget(readyStartButton);
|
buttonHBox->addWidget(readyStartButton);
|
||||||
buttonHBox->addWidget(sideboardLockButton);
|
buttonHBox->addWidget(sideboardLockButton);
|
||||||
|
|
@ -118,6 +122,7 @@ void DeckViewContainer::retranslateUi()
|
||||||
{
|
{
|
||||||
loadLocalButton->setText(tr("Load deck..."));
|
loadLocalButton->setText(tr("Load deck..."));
|
||||||
loadRemoteButton->setText(tr("Load remote deck..."));
|
loadRemoteButton->setText(tr("Load remote deck..."));
|
||||||
|
loadFromClipboardButton->setText(tr("Load from clipboard..."));
|
||||||
unloadDeckButton->setText(tr("Unload deck"));
|
unloadDeckButton->setText(tr("Unload deck"));
|
||||||
readyStartButton->setText(tr("Ready to start"));
|
readyStartButton->setText(tr("Ready to start"));
|
||||||
forceStartGameButton->setText(tr("Force start"));
|
forceStartGameButton->setText(tr("Force start"));
|
||||||
|
|
@ -148,6 +153,7 @@ void DeckViewContainer::switchToDeckSelectView()
|
||||||
|
|
||||||
setVisibility(loadLocalButton, true);
|
setVisibility(loadLocalButton, true);
|
||||||
setVisibility(loadRemoteButton, !parentGame->getIsLocalGame());
|
setVisibility(loadRemoteButton, !parentGame->getIsLocalGame());
|
||||||
|
setVisibility(loadFromClipboardButton, true);
|
||||||
setVisibility(unloadDeckButton, false);
|
setVisibility(unloadDeckButton, false);
|
||||||
setVisibility(readyStartButton, false);
|
setVisibility(readyStartButton, false);
|
||||||
setVisibility(sideboardLockButton, false);
|
setVisibility(sideboardLockButton, false);
|
||||||
|
|
@ -172,6 +178,7 @@ void DeckViewContainer::switchToDeckLoadedView()
|
||||||
|
|
||||||
setVisibility(loadLocalButton, false);
|
setVisibility(loadLocalButton, false);
|
||||||
setVisibility(loadRemoteButton, false);
|
setVisibility(loadRemoteButton, false);
|
||||||
|
setVisibility(loadFromClipboardButton, false);
|
||||||
setVisibility(unloadDeckButton, true);
|
setVisibility(unloadDeckButton, true);
|
||||||
setVisibility(readyStartButton, true);
|
setVisibility(readyStartButton, true);
|
||||||
setVisibility(sideboardLockButton, true);
|
setVisibility(sideboardLockButton, true);
|
||||||
|
|
@ -198,8 +205,11 @@ void DeckViewContainer::refreshShortcuts()
|
||||||
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
|
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
|
||||||
loadLocalButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadLocalButton"));
|
loadLocalButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadLocalButton"));
|
||||||
loadRemoteButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadRemoteButton"));
|
loadRemoteButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadRemoteButton"));
|
||||||
|
loadFromClipboardButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadFromClipboardButton"));
|
||||||
|
unloadDeckButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/unloadDeckButton"));
|
||||||
readyStartButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/readyStartButton"));
|
readyStartButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/readyStartButton"));
|
||||||
sideboardLockButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/sideboardLockButton"));
|
sideboardLockButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/sideboardLockButton"));
|
||||||
|
forceStartGameButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/forceStartGameButton"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -247,19 +257,27 @@ void DeckViewContainer::loadLocalDeck()
|
||||||
void DeckViewContainer::loadDeckFromFile(const QString &filePath)
|
void DeckViewContainer::loadDeckFromFile(const QString &filePath)
|
||||||
{
|
{
|
||||||
DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(filePath);
|
DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(filePath);
|
||||||
QString deckString;
|
|
||||||
DeckLoader deck;
|
DeckLoader deck;
|
||||||
|
|
||||||
bool error = !deck.loadFromFile(filePath, fmt, true);
|
bool success = deck.loadFromFile(filePath, fmt, true);
|
||||||
if (!error) {
|
|
||||||
deckString = deck.writeToString_Native();
|
if (!success) {
|
||||||
error = deckString.length() > MAX_FILE_LENGTH;
|
|
||||||
}
|
|
||||||
if (error) {
|
|
||||||
QMessageBox::critical(this, tr("Error"), tr("The selected file could not be loaded."));
|
QMessageBox::critical(this, tr("Error"), tr("The selected file could not be loaded."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadDeckFromDeckLoader(&deck);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckViewContainer::loadDeckFromDeckLoader(const DeckLoader *deck)
|
||||||
|
{
|
||||||
|
QString deckString = deck->writeToString_Native();
|
||||||
|
|
||||||
|
if (deckString.length() > MAX_FILE_LENGTH) {
|
||||||
|
QMessageBox::critical(this, tr("Error"), tr("Deck is greater than maximum file size."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Command_DeckSelect cmd;
|
Command_DeckSelect cmd;
|
||||||
cmd.set_deck(deckString.toStdString());
|
cmd.set_deck(deckString.toStdString());
|
||||||
PendingCommand *pend = parentGame->prepareGameCommand(cmd);
|
PendingCommand *pend = parentGame->prepareGameCommand(cmd);
|
||||||
|
|
@ -279,6 +297,18 @@ void DeckViewContainer::loadRemoteDeck()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DeckViewContainer::loadFromClipboard()
|
||||||
|
{
|
||||||
|
auto dlg = DlgLoadDeckFromClipboard(this);
|
||||||
|
|
||||||
|
if (!dlg.exec()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeckLoader *deck = dlg.getDeckList();
|
||||||
|
loadDeckFromDeckLoader(deck);
|
||||||
|
}
|
||||||
|
|
||||||
void DeckViewContainer::deckSelectFinished(const Response &r)
|
void DeckViewContainer::deckSelectFinished(const Response &r)
|
||||||
{
|
{
|
||||||
const Response_DeckDownload &resp = r.GetExtension(Response_DeckDownload::ext);
|
const Response_DeckDownload &resp = r.GetExtension(Response_DeckDownload::ext);
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,8 @@ class DeckViewContainer : public QWidget
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
private:
|
private:
|
||||||
QVBoxLayout *deckViewLayout;
|
QVBoxLayout *deckViewLayout;
|
||||||
QPushButton *loadLocalButton, *loadRemoteButton, *unloadDeckButton, *forceStartGameButton;
|
QPushButton *loadLocalButton, *loadRemoteButton, *loadFromClipboardButton;
|
||||||
|
QPushButton *unloadDeckButton, *forceStartGameButton;
|
||||||
ToggleButton *readyStartButton, *sideboardLockButton;
|
ToggleButton *readyStartButton, *sideboardLockButton;
|
||||||
DeckView *deckView;
|
DeckView *deckView;
|
||||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||||
|
|
@ -58,6 +59,7 @@ private slots:
|
||||||
void switchToDeckLoadedView();
|
void switchToDeckLoadedView();
|
||||||
void loadLocalDeck();
|
void loadLocalDeck();
|
||||||
void loadRemoteDeck();
|
void loadRemoteDeck();
|
||||||
|
void loadFromClipboard();
|
||||||
void unloadDeck();
|
void unloadDeck();
|
||||||
void readyStart();
|
void readyStart();
|
||||||
void forceStart();
|
void forceStart();
|
||||||
|
|
@ -81,6 +83,7 @@ public:
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void loadDeckFromFile(const QString &filePath);
|
void loadDeckFromFile(const QString &filePath);
|
||||||
|
void loadDeckFromDeckLoader(const DeckLoader *deck);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // DECK_VIEW_CONTAINER_H
|
#endif // DECK_VIEW_CONTAINER_H
|
||||||
|
|
|
||||||
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 <QByteArray>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QRegularExpression>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
peg::parser search(R"(
|
static peg::parser search(R"(
|
||||||
Start <- QueryPartList
|
Start <- QueryPartList
|
||||||
~ws <- [ ]+
|
~ws <- [ ]+
|
||||||
QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws*
|
QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws*
|
||||||
|
|
@ -20,7 +21,7 @@ QueryPart <- NotQuery / SetQuery / RarityQuery / CMCQuery / FormatQuery / PowerQ
|
||||||
|
|
||||||
NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart
|
NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart
|
||||||
SetQuery <- ('e'/'set') [:] FlexStringValue
|
SetQuery <- ('e'/'set') [:] FlexStringValue
|
||||||
OracleQuery <- 'o' [:] RegexString
|
OracleQuery <- 'o' [:] MatcherString
|
||||||
|
|
||||||
|
|
||||||
CMCQuery <- ('cmc'/'mv') ws? NumericExpression
|
CMCQuery <- ('cmc'/'mv') ws? NumericExpression
|
||||||
|
|
@ -42,7 +43,7 @@ ColorEx <- Color / [mc]
|
||||||
|
|
||||||
ColorQuery <- [cC] 'olor'? <[iI]?> <[:!]> ColorEx*
|
ColorQuery <- [cC] 'olor'? <[iI]?> <[:!]> ColorEx*
|
||||||
|
|
||||||
FieldQuery <- String [:] RegexString / String ws? NumericExpression
|
FieldQuery <- String [:] MatcherString / String ws? NumericExpression
|
||||||
|
|
||||||
NonDoubleQuoteUnlessEscaped <- '\\\"'. / !["].
|
NonDoubleQuoteUnlessEscaped <- '\\\"'. / !["].
|
||||||
NonSingleQuoteUnlessEscaped <- "\\\'". / !['].
|
NonSingleQuoteUnlessEscaped <- "\\\'". / !['].
|
||||||
|
|
@ -52,8 +53,14 @@ String <- SingleApostropheString / UnescapedStringListPart+ / ["] <NonDoubleQuot
|
||||||
StringValue <- String / [(] StringList [)]
|
StringValue <- String / [(] StringList [)]
|
||||||
StringList <- StringListString (ws? [,] ws? StringListString)*
|
StringList <- StringListString (ws? [,] ws? StringListString)*
|
||||||
StringListString <- UnescapedStringListPart+
|
StringListString <- UnescapedStringListPart+
|
||||||
GenericQuery <- RegexString
|
GenericQuery <- MatcherString
|
||||||
RegexString <- String
|
|
||||||
|
# A String that can either be a normal string or a regex search string
|
||||||
|
MatcherString <- RegexMatcher / NormalMatcher
|
||||||
|
|
||||||
|
NormalMatcher <- String
|
||||||
|
RegexMatcher <- '/' RegexMatcherString '/'
|
||||||
|
RegexMatcherString <- ('\\/' / !'/' .)+
|
||||||
|
|
||||||
FlexStringValue <- CompactStringSet / String / [(] StringList [)]
|
FlexStringValue <- CompactStringSet / String / [(] StringList [)]
|
||||||
CompactStringSet <- StringListString ([,+] StringListString)+
|
CompactStringSet <- StringListString ([,+] StringListString)+
|
||||||
|
|
@ -63,7 +70,7 @@ NumericOperator <- [=:] / <[><!][=]?>
|
||||||
NumericValue <- [0-9]+
|
NumericValue <- [0-9]+
|
||||||
)");
|
)");
|
||||||
|
|
||||||
std::once_flag init;
|
static std::once_flag init;
|
||||||
|
|
||||||
static void setupParserRules()
|
static void setupParserRules()
|
||||||
{
|
{
|
||||||
|
|
@ -261,14 +268,22 @@ static void setupParserRules()
|
||||||
return QString::fromStdString(std::string(sv.sv()));
|
return QString::fromStdString(std::string(sv.sv()));
|
||||||
};
|
};
|
||||||
|
|
||||||
search["RegexString"] = [](const peg::SemanticValues &sv) -> StringMatcher {
|
search["NormalMatcher"] = [](const peg::SemanticValues &sv) -> StringMatcher {
|
||||||
auto target = std::any_cast<QString>(sv[0]);
|
auto target = std::any_cast<QString>(sv[0]);
|
||||||
return [=](const QString &s) {
|
|
||||||
auto sanitizedTarget = QString(target);
|
auto sanitizedTarget = QString(target);
|
||||||
sanitizedTarget.replace("\\\"", "\"");
|
sanitizedTarget.replace("\\\"", "\"");
|
||||||
sanitizedTarget.replace("\\'", "'");
|
sanitizedTarget.replace("\\'", "'");
|
||||||
return s.contains(sanitizedTarget, Qt::CaseInsensitive);
|
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 {
|
search["OracleQuery"] = [](const peg::SemanticValues &sv) -> Filter {
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,12 @@
|
||||||
*
|
*
|
||||||
* @return the QTextBrowser
|
* @return the QTextBrowser
|
||||||
*/
|
*/
|
||||||
static QTextBrowser *createBrowser()
|
static QTextBrowser *createBrowser(const QString &helpFile)
|
||||||
{
|
{
|
||||||
QFile file("theme:help/search.md");
|
QFile file(helpFile);
|
||||||
|
|
||||||
if (!file.open(QFile::ReadOnly | QFile::Text)) {
|
if (!file.open(QFile::ReadOnly | QFile::Text)) {
|
||||||
|
qCWarning(SyntaxHelpLog) << "Could not open syntax help file: " << helpFile;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,9 +55,29 @@ static QTextBrowser *createBrowser()
|
||||||
*/
|
*/
|
||||||
QTextBrowser *createSearchSyntaxHelpWindow(QLineEdit *lineEdit)
|
QTextBrowser *createSearchSyntaxHelpWindow(QLineEdit *lineEdit)
|
||||||
{
|
{
|
||||||
auto browser = createBrowser();
|
auto browser = createBrowser("theme:help/search.md");
|
||||||
QObject::connect(browser, &QTextBrowser::anchorClicked,
|
QObject::connect(browser, &QTextBrowser::anchorClicked,
|
||||||
[lineEdit](const QUrl &link) { lineEdit->setText(link.fragment()); });
|
[lineEdit](const QUrl &link) { lineEdit->setText(link.fragment()); });
|
||||||
QObject::connect(lineEdit, &QObject::destroyed, browser, &QTextBrowser::close);
|
QObject::connect(lineEdit, &QObject::destroyed, browser, &QTextBrowser::close);
|
||||||
return browser;
|
return browser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the deck search syntax help window and connects its anchorClicked signal to the given QLineEdit.
|
||||||
|
* The window will automatically close when the QLineEdit is destroyed.
|
||||||
|
*
|
||||||
|
* @return the QTextBrowser
|
||||||
|
*/
|
||||||
|
QTextBrowser *createDeckSearchSyntaxHelpWindow(QLineEdit *lineEdit)
|
||||||
|
{
|
||||||
|
auto browser = createBrowser("theme:help/deck_search.md");
|
||||||
|
QObject::connect(browser, &QTextBrowser::anchorClicked, [lineEdit](const QUrl &link) {
|
||||||
|
if (link.fragment() == "cardSearchSyntaxHelp") {
|
||||||
|
createSearchSyntaxHelpWindow(lineEdit);
|
||||||
|
} else {
|
||||||
|
lineEdit->setText(link.fragment());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
QObject::connect(lineEdit, &QObject::destroyed, browser, &QTextBrowser::close);
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
|
@ -2,8 +2,13 @@
|
||||||
#define SEARCH_SYNTAX_HELP_H
|
#define SEARCH_SYNTAX_HELP_H
|
||||||
|
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
|
#include <QLoggingCategory>
|
||||||
#include <QTextBrowser>
|
#include <QTextBrowser>
|
||||||
|
|
||||||
|
inline Q_LOGGING_CATEGORY(SyntaxHelpLog, "syntax_help");
|
||||||
|
|
||||||
QTextBrowser *createSearchSyntaxHelpWindow(QLineEdit *lineEdit);
|
QTextBrowser *createSearchSyntaxHelpWindow(QLineEdit *lineEdit);
|
||||||
|
|
||||||
|
QTextBrowser *createDeckSearchSyntaxHelpWindow(QLineEdit *lineEdit);
|
||||||
|
|
||||||
#endif // SEARCH_SYNTAX_HELP_H
|
#endif // SEARCH_SYNTAX_HELP_H
|
||||||
|
|
|
||||||
|
|
@ -275,7 +275,6 @@ SettingsCache::SettingsCache()
|
||||||
visualDeckStorageSortingOrder = settings->value("interface/visualdeckstoragesortingorder", 0).toInt();
|
visualDeckStorageSortingOrder = settings->value("interface/visualdeckstoragesortingorder", 0).toInt();
|
||||||
visualDeckStorageShowFolders = settings->value("interface/visualdeckstorageshowfolders", true).toBool();
|
visualDeckStorageShowFolders = settings->value("interface/visualdeckstorageshowfolders", true).toBool();
|
||||||
visualDeckStorageShowTagFilter = settings->value("interface/visualdeckstorageshowtagfilter", true).toBool();
|
visualDeckStorageShowTagFilter = settings->value("interface/visualdeckstorageshowtagfilter", true).toBool();
|
||||||
visualDeckStorageSearchFolderNames = settings->value("interface/visualdeckstoragesearchfoldernames", true).toBool();
|
|
||||||
visualDeckStorageShowBannerCardComboBox =
|
visualDeckStorageShowBannerCardComboBox =
|
||||||
settings->value("interface/visualdeckstorageshowbannercardcombobox", true).toBool();
|
settings->value("interface/visualdeckstorageshowbannercardcombobox", true).toBool();
|
||||||
visualDeckStorageShowTagsOnDeckPreviews =
|
visualDeckStorageShowTagsOnDeckPreviews =
|
||||||
|
|
@ -284,6 +283,7 @@ SettingsCache::SettingsCache()
|
||||||
settings->value("interface/visualdeckstoragedrawunusedcoloridentities", true).toBool();
|
settings->value("interface/visualdeckstoragedrawunusedcoloridentities", true).toBool();
|
||||||
visualDeckStorageUnusedColorIdentitiesOpacity =
|
visualDeckStorageUnusedColorIdentitiesOpacity =
|
||||||
settings->value("interface/visualdeckstorageunusedcoloridentitiesopacity", 15).toInt();
|
settings->value("interface/visualdeckstorageunusedcoloridentitiesopacity", 15).toInt();
|
||||||
|
visualDeckStorageTooltipType = settings->value("interface/visualdeckstoragetooltiptype", 0).toInt();
|
||||||
visualDeckStoragePromptForConversion =
|
visualDeckStoragePromptForConversion =
|
||||||
settings->value("interface/visualdeckstoragepromptforconversion", true).toBool();
|
settings->value("interface/visualdeckstoragepromptforconversion", true).toBool();
|
||||||
visualDeckStorageAlwaysConvert = settings->value("interface/visualdeckstoragealwaysconvert", false).toBool();
|
visualDeckStorageAlwaysConvert = settings->value("interface/visualdeckstoragealwaysconvert", false).toBool();
|
||||||
|
|
@ -734,12 +734,6 @@ void SettingsCache::setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T _showTa
|
||||||
emit visualDeckStorageShowTagFilterChanged(visualDeckStorageShowTagFilter);
|
emit visualDeckStorageShowTagFilterChanged(visualDeckStorageShowTagFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SettingsCache::setVisualDeckStorageSearchFolderNames(QT_STATE_CHANGED_T value)
|
|
||||||
{
|
|
||||||
visualDeckStorageSearchFolderNames = value;
|
|
||||||
settings->setValue("interface/visualdeckstoragesearchfoldernames", visualDeckStorageSearchFolderNames);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SettingsCache::setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T _showBannerCardComboBox)
|
void SettingsCache::setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T _showBannerCardComboBox)
|
||||||
{
|
{
|
||||||
visualDeckStorageShowBannerCardComboBox = _showBannerCardComboBox;
|
visualDeckStorageShowBannerCardComboBox = _showBannerCardComboBox;
|
||||||
|
|
@ -778,6 +772,12 @@ void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity(int _visual
|
||||||
emit visualDeckStorageUnusedColorIdentitiesOpacityChanged(visualDeckStorageUnusedColorIdentitiesOpacity);
|
emit visualDeckStorageUnusedColorIdentitiesOpacityChanged(visualDeckStorageUnusedColorIdentitiesOpacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SettingsCache::setVisualDeckStorageTooltipType(int value)
|
||||||
|
{
|
||||||
|
visualDeckStorageTooltipType = value;
|
||||||
|
settings->setValue("interface/visualdeckstoragetooltiptype", visualDeckStorageTooltipType);
|
||||||
|
}
|
||||||
|
|
||||||
void SettingsCache::setVisualDeckStoragePromptForConversion(bool _visualDeckStoragePromptForConversion)
|
void SettingsCache::setVisualDeckStoragePromptForConversion(bool _visualDeckStoragePromptForConversion)
|
||||||
{
|
{
|
||||||
visualDeckStoragePromptForConversion = _visualDeckStoragePromptForConversion;
|
visualDeckStoragePromptForConversion = _visualDeckStoragePromptForConversion;
|
||||||
|
|
|
||||||
|
|
@ -148,10 +148,10 @@ private:
|
||||||
bool visualDeckStorageShowBannerCardComboBox;
|
bool visualDeckStorageShowBannerCardComboBox;
|
||||||
bool visualDeckStorageShowTagsOnDeckPreviews;
|
bool visualDeckStorageShowTagsOnDeckPreviews;
|
||||||
bool visualDeckStorageShowTagFilter;
|
bool visualDeckStorageShowTagFilter;
|
||||||
bool visualDeckStorageSearchFolderNames;
|
|
||||||
int visualDeckStorageCardSize;
|
int visualDeckStorageCardSize;
|
||||||
bool visualDeckStorageDrawUnusedColorIdentities;
|
bool visualDeckStorageDrawUnusedColorIdentities;
|
||||||
int visualDeckStorageUnusedColorIdentitiesOpacity;
|
int visualDeckStorageUnusedColorIdentitiesOpacity;
|
||||||
|
int visualDeckStorageTooltipType;
|
||||||
bool visualDeckStoragePromptForConversion;
|
bool visualDeckStoragePromptForConversion;
|
||||||
bool visualDeckStorageAlwaysConvert;
|
bool visualDeckStorageAlwaysConvert;
|
||||||
bool visualDeckStorageInGame;
|
bool visualDeckStorageInGame;
|
||||||
|
|
@ -454,10 +454,6 @@ public:
|
||||||
{
|
{
|
||||||
return visualDeckStorageShowTagFilter;
|
return visualDeckStorageShowTagFilter;
|
||||||
}
|
}
|
||||||
bool getVisualDeckStorageSearchFolderNames() const
|
|
||||||
{
|
|
||||||
return visualDeckStorageSearchFolderNames;
|
|
||||||
}
|
|
||||||
bool getVisualDeckStorageShowBannerCardComboBox() const
|
bool getVisualDeckStorageShowBannerCardComboBox() const
|
||||||
{
|
{
|
||||||
return visualDeckStorageShowBannerCardComboBox;
|
return visualDeckStorageShowBannerCardComboBox;
|
||||||
|
|
@ -478,6 +474,10 @@ public:
|
||||||
{
|
{
|
||||||
return visualDeckStorageUnusedColorIdentitiesOpacity;
|
return visualDeckStorageUnusedColorIdentitiesOpacity;
|
||||||
}
|
}
|
||||||
|
int getVisualDeckStorageTooltipType() const
|
||||||
|
{
|
||||||
|
return visualDeckStorageTooltipType;
|
||||||
|
}
|
||||||
bool getVisualDeckStoragePromptForConversion() const
|
bool getVisualDeckStoragePromptForConversion() const
|
||||||
{
|
{
|
||||||
return visualDeckStoragePromptForConversion;
|
return visualDeckStoragePromptForConversion;
|
||||||
|
|
@ -850,12 +850,12 @@ public slots:
|
||||||
void setVisualDeckStorageSortingOrder(int _visualDeckStorageSortingOrder);
|
void setVisualDeckStorageSortingOrder(int _visualDeckStorageSortingOrder);
|
||||||
void setVisualDeckStorageShowFolders(QT_STATE_CHANGED_T value);
|
void setVisualDeckStorageShowFolders(QT_STATE_CHANGED_T value);
|
||||||
void setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T _showTags);
|
void setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T _showTags);
|
||||||
void setVisualDeckStorageSearchFolderNames(QT_STATE_CHANGED_T value);
|
|
||||||
void setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T _showBannerCardComboBox);
|
void setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T _showBannerCardComboBox);
|
||||||
void setVisualDeckStorageShowTagsOnDeckPreviews(QT_STATE_CHANGED_T _showTags);
|
void setVisualDeckStorageShowTagsOnDeckPreviews(QT_STATE_CHANGED_T _showTags);
|
||||||
void setVisualDeckStorageCardSize(int _visualDeckStorageCardSize);
|
void setVisualDeckStorageCardSize(int _visualDeckStorageCardSize);
|
||||||
void setVisualDeckStorageDrawUnusedColorIdentities(QT_STATE_CHANGED_T _visualDeckStorageDrawUnusedColorIdentities);
|
void setVisualDeckStorageDrawUnusedColorIdentities(QT_STATE_CHANGED_T _visualDeckStorageDrawUnusedColorIdentities);
|
||||||
void setVisualDeckStorageUnusedColorIdentitiesOpacity(int _visualDeckStorageUnusedColorIdentitiesOpacity);
|
void setVisualDeckStorageUnusedColorIdentitiesOpacity(int _visualDeckStorageUnusedColorIdentitiesOpacity);
|
||||||
|
void setVisualDeckStorageTooltipType(int value);
|
||||||
void setVisualDeckStoragePromptForConversion(bool _visualDeckStoragePromptForConversion);
|
void setVisualDeckStoragePromptForConversion(bool _visualDeckStoragePromptForConversion);
|
||||||
void setVisualDeckStorageAlwaysConvert(bool _visualDeckStorageAlwaysConvert);
|
void setVisualDeckStorageAlwaysConvert(bool _visualDeckStorageAlwaysConvert);
|
||||||
void setVisualDeckStorageInGame(QT_STATE_CHANGED_T value);
|
void setVisualDeckStorageInGame(QT_STATE_CHANGED_T value);
|
||||||
|
|
|
||||||
|
|
@ -267,6 +267,13 @@ private:
|
||||||
{"DeckViewContainer/loadRemoteButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Remote Deck..."),
|
{"DeckViewContainer/loadRemoteButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Remote Deck..."),
|
||||||
parseSequenceString("Ctrl+Alt+O"),
|
parseSequenceString("Ctrl+Alt+O"),
|
||||||
ShortcutGroup::Game_Lobby)},
|
ShortcutGroup::Game_Lobby)},
|
||||||
|
{"DeckViewContainer/loadFromClipboardButton",
|
||||||
|
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Load Deck from Clipboard..."),
|
||||||
|
parseSequenceString("Ctrl+Shift+V"),
|
||||||
|
ShortcutGroup::Game_Lobby)},
|
||||||
|
{"DeckViewContainer/unloadDeckButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Unload Deck"),
|
||||||
|
parseSequenceString("Ctrl+Alt+U"),
|
||||||
|
ShortcutGroup::Game_Lobby)},
|
||||||
{"DeckViewContainer/readyStartButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Set Ready to Start"),
|
{"DeckViewContainer/readyStartButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Set Ready to Start"),
|
||||||
parseSequenceString("Ctrl+Shift+S"),
|
parseSequenceString("Ctrl+Shift+S"),
|
||||||
ShortcutGroup::Game_Lobby)},
|
ShortcutGroup::Game_Lobby)},
|
||||||
|
|
@ -274,6 +281,9 @@ private:
|
||||||
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Toggle Sideboard Lock"),
|
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Toggle Sideboard Lock"),
|
||||||
parseSequenceString("Ctrl+Shift+B"),
|
parseSequenceString("Ctrl+Shift+B"),
|
||||||
ShortcutGroup::Game_Lobby)},
|
ShortcutGroup::Game_Lobby)},
|
||||||
|
{"DeckViewContainer/forceStartGameButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Force Start"),
|
||||||
|
parseSequenceString(""),
|
||||||
|
ShortcutGroup::Game_Lobby)},
|
||||||
{"Player/aCCMagenta", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Add Card Counter (F)"),
|
{"Player/aCCMagenta", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Add Card Counter (F)"),
|
||||||
parseSequenceString(""),
|
parseSequenceString(""),
|
||||||
ShortcutGroup::Card_Counters)},
|
ShortcutGroup::Card_Counters)},
|
||||||
|
|
|
||||||
|
|
@ -441,7 +441,7 @@ bool DeckList::readElement(QXmlStreamReader *xml)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeckList::write(QXmlStreamWriter *xml)
|
void DeckList::write(QXmlStreamWriter *xml) const
|
||||||
{
|
{
|
||||||
xml->writeStartElement("cockatrice_deck");
|
xml->writeStartElement("cockatrice_deck");
|
||||||
xml->writeAttribute("version", "1");
|
xml->writeAttribute("version", "1");
|
||||||
|
|
@ -508,7 +508,7 @@ bool DeckList::loadFromString_Native(const QString &nativeString)
|
||||||
return loadFromXml(&xml);
|
return loadFromXml(&xml);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString DeckList::writeToString_Native()
|
QString DeckList::writeToString_Native() const
|
||||||
{
|
{
|
||||||
QString result;
|
QString result;
|
||||||
QXmlStreamWriter xml(&result);
|
QXmlStreamWriter xml(&result);
|
||||||
|
|
|
||||||
|
|
@ -345,10 +345,10 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
bool readElement(QXmlStreamReader *xml);
|
bool readElement(QXmlStreamReader *xml);
|
||||||
void write(QXmlStreamWriter *xml);
|
void write(QXmlStreamWriter *xml) const;
|
||||||
bool loadFromXml(QXmlStreamReader *xml);
|
bool loadFromXml(QXmlStreamReader *xml);
|
||||||
bool loadFromString_Native(const QString &nativeString);
|
bool loadFromString_Native(const QString &nativeString);
|
||||||
QString writeToString_Native();
|
QString writeToString_Native() const;
|
||||||
bool loadFromFile_Native(QIODevice *device);
|
bool loadFromFile_Native(QIODevice *device);
|
||||||
bool saveToFile_Native(QIODevice *device);
|
bool saveToFile_Native(QIODevice *device);
|
||||||
bool loadFromStream_Plain(QTextStream &stream, bool preserveMetadata);
|
bool loadFromStream_Plain(QTextStream &stream, bool preserveMetadata);
|
||||||
|
|
|
||||||
|
|
@ -920,6 +920,8 @@ Server_Player::cmdConcede(const Command_Concede & /*cmd*/, ResponseContainer & /
|
||||||
game->removeArrowsRelatedToPlayer(ges, this);
|
game->removeArrowsRelatedToPlayer(ges, this);
|
||||||
game->unattachCards(ges, this);
|
game->unattachCards(ges, this);
|
||||||
|
|
||||||
|
playerMutex.lock();
|
||||||
|
|
||||||
// Return cards to their rightful owners before conceding the game
|
// Return cards to their rightful owners before conceding the game
|
||||||
static const QRegularExpression ownerRegex{"Owner: ?([^\n]+)"};
|
static const QRegularExpression ownerRegex{"Owner: ?([^\n]+)"};
|
||||||
for (const auto &card : zones.value("table")->getCards()) {
|
for (const auto &card : zones.value("table")->getCards()) {
|
||||||
|
|
@ -959,6 +961,8 @@ Server_Player::cmdConcede(const Command_Concede & /*cmd*/, ResponseContainer & /
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playerMutex.unlock();
|
||||||
|
|
||||||
// All borrowed cards have been returned, can now continue cleanup process
|
// All borrowed cards have been returned, can now continue cleanup process
|
||||||
clearZones();
|
clearZones();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -227,9 +227,6 @@ void SettingsCache::setVisualDeckStorageShowFolders(QT_STATE_CHANGED_T /* value
|
||||||
void SettingsCache::setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T /* _showTags */)
|
void SettingsCache::setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T /* _showTags */)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
void SettingsCache::setVisualDeckStorageSearchFolderNames(QT_STATE_CHANGED_T /* value */)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
void SettingsCache::setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T /* _showBannerCardComboBox */)
|
void SettingsCache::setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T /* _showBannerCardComboBox */)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
@ -247,6 +244,9 @@ void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity(
|
||||||
int /* _visualDeckStorageUnusedColorIdentitiesOpacity */)
|
int /* _visualDeckStorageUnusedColorIdentitiesOpacity */)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
void SettingsCache::setVisualDeckStorageTooltipType(int /* value */)
|
||||||
|
{
|
||||||
|
}
|
||||||
void SettingsCache::setVisualDeckStoragePromptForConversion(bool /* _visualDeckStoragePromptForConversion */)
|
void SettingsCache::setVisualDeckStoragePromptForConversion(bool /* _visualDeckStoragePromptForConversion */)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@
|
||||||
<xs:element type="xs:integer" name="tablerow" minOccurs="0" maxOccurs="1" />
|
<xs:element type="xs:integer" name="tablerow" minOccurs="0" maxOccurs="1" />
|
||||||
<xs:element type="xs:boolean" name="cipt" minOccurs="0" maxOccurs="1" />
|
<xs:element type="xs:boolean" name="cipt" minOccurs="0" maxOccurs="1" />
|
||||||
<xs:element type="xs:boolean" name="upsidedown" minOccurs="0" maxOccurs="1" />
|
<xs:element type="xs:boolean" name="upsidedown" minOccurs="0" maxOccurs="1" />
|
||||||
|
<xs:element type="xs:boolean" name="landscapeOrientation" minOccurs="0" maxOccurs="1" />
|
||||||
</xs:sequence>
|
</xs:sequence>
|
||||||
</xs:complexType>
|
</xs:complexType>
|
||||||
<xs:element name="cockatrice_carddatabase">
|
<xs:element name="cockatrice_carddatabase">
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,13 @@ set(oracle_SOURCES
|
||||||
../cockatrice/src/game/cards/card_database_manager.cpp
|
../cockatrice/src/game/cards/card_database_manager.cpp
|
||||||
../cockatrice/src/game/cards/card_info.cpp
|
../cockatrice/src/game/cards/card_info.cpp
|
||||||
../cockatrice/src/client/ui/picture_loader/picture_loader.cpp
|
../cockatrice/src/client/ui/picture_loader/picture_loader.cpp
|
||||||
|
../cockatrice/src/client/ui/picture_loader/picture_loader_request_status_display_widget.cpp
|
||||||
|
../cockatrice/src/client/ui/picture_loader/picture_loader_status_bar.cpp
|
||||||
../cockatrice/src/client/ui/picture_loader/picture_loader_worker.cpp
|
../cockatrice/src/client/ui/picture_loader/picture_loader_worker.cpp
|
||||||
|
../cockatrice/src/client/ui/picture_loader/picture_loader_worker_work.cpp
|
||||||
../cockatrice/src/client/ui/picture_loader/picture_to_load.cpp
|
../cockatrice/src/client/ui/picture_loader/picture_to_load.cpp
|
||||||
|
../cockatrice/src/client/ui/widgets/quick_settings/settings_button_widget.cpp
|
||||||
|
../cockatrice/src/client/ui/widgets/quick_settings/settings_popup_widget.cpp
|
||||||
../cockatrice/src/game/cards/card_database_parser/card_database_parser.cpp
|
../cockatrice/src/game/cards/card_database_parser/card_database_parser.cpp
|
||||||
../cockatrice/src/game/cards/card_database_parser/cockatrice_xml_3.cpp
|
../cockatrice/src/game/cards/card_database_parser/cockatrice_xml_3.cpp
|
||||||
../cockatrice/src/game/cards/card_database_parser/cockatrice_xml_4.cpp
|
../cockatrice/src/game/cards/card_database_parser/cockatrice_xml_4.cpp
|
||||||
|
|
|
||||||
|
|
@ -231,9 +231,6 @@ void SettingsCache::setVisualDeckStorageShowFolders(QT_STATE_CHANGED_T /* value
|
||||||
void SettingsCache::setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T /* _showTags */)
|
void SettingsCache::setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T /* _showTags */)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
void SettingsCache::setVisualDeckStorageSearchFolderNames(QT_STATE_CHANGED_T /* value */)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
void SettingsCache::setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T /* _showBannerCardComboBox */)
|
void SettingsCache::setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T /* _showBannerCardComboBox */)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
@ -251,6 +248,9 @@ void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity(
|
||||||
int /* _visualDeckStorageUnusedColorIdentitiesOpacity */)
|
int /* _visualDeckStorageUnusedColorIdentitiesOpacity */)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
void SettingsCache::setVisualDeckStorageTooltipType(int /* value */)
|
||||||
|
{
|
||||||
|
}
|
||||||
void SettingsCache::setVisualDeckStoragePromptForConversion(bool /* _visualDeckStoragePromptForConversion */)
|
void SettingsCache::setVisualDeckStoragePromptForConversion(bool /* _visualDeckStoragePromptForConversion */)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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