Compare commits
250 Commits
2024-12-20
...
2025-02-02
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1de09deb59 | ||
|
|
a0b52ce450 | ||
|
|
95cea0f191 | ||
|
|
0fc05e15cd | ||
|
|
26c0cdc072 | ||
|
|
b1b48d50f3 | ||
|
|
349c18aa6a | ||
|
|
b956fd4bac | ||
|
|
34e0130b90 | ||
|
|
33d8edeb9a | ||
|
|
5d1e905255 | ||
|
|
51c542aa04 | ||
|
|
4d791f4d7a | ||
|
|
aee68f8b00 | ||
|
|
b911ea6e28 | ||
|
|
a41e7c75c1 | ||
|
|
9f729bf636 | ||
|
|
42e4c14a82 | ||
|
|
37a0c00b3f | ||
|
|
f6c31bf901 | ||
|
|
b48fe8b99c | ||
|
|
19b758591b | ||
|
|
ec6a23de56 | ||
|
|
ce416df3fb | ||
|
|
4e96157091 | ||
|
|
f428148f64 | ||
|
|
e8b1e3ef0c | ||
|
|
085f0dd26c | ||
|
|
1d2ab8d3d3 | ||
|
|
66e2e7a473 | ||
|
|
af161f00b7 | ||
|
|
420cca2402 | ||
|
|
97fdf11c8f | ||
|
|
aeb1b9fb4f | ||
|
|
b004e91aa4 | ||
|
|
090cc8c144 | ||
|
|
0467fae51b | ||
|
|
aa24502129 | ||
|
|
e752578d15 | ||
|
|
724db755af | ||
|
|
ec0caaf421 | ||
|
|
55b490ade0 | ||
|
|
1392bdd258 | ||
|
|
648c96ac3d | ||
|
|
d3a1538af3 | ||
|
|
2bc71095dd | ||
|
|
92a903b035 | ||
|
|
cd373edf3d | ||
|
|
ca2d438cda | ||
|
|
c148c8df7f | ||
|
|
0cbad25385 | ||
|
|
7b94d5d501 | ||
|
|
ee938342f3 | ||
|
|
cb64a5eea0 | ||
|
|
80165c28a9 | ||
|
|
315c224f24 | ||
|
|
55f624b634 | ||
|
|
82b257b589 | ||
|
|
a51ca9f9cb | ||
|
|
7e19b52926 | ||
|
|
2d02955f8b | ||
|
|
3a740f0bde | ||
|
|
455d68f9ea | ||
|
|
2def02e140 | ||
|
|
23bd18a04c | ||
|
|
d09b9eb533 | ||
|
|
25caae6d0f | ||
|
|
a717e715b6 | ||
|
|
c079715c46 | ||
|
|
f6c1253e84 | ||
|
|
8462b6e906 | ||
|
|
cca82f59eb | ||
|
|
81662b7fec | ||
|
|
d2c2128e9b | ||
|
|
686645c1e4 | ||
|
|
9df71fe1e8 | ||
|
|
6309e7e318 | ||
|
|
767e83c879 | ||
|
|
78d54b0ef2 | ||
|
|
497e4f1be0 | ||
|
|
6072df3522 | ||
|
|
ba89495dc0 | ||
|
|
a417b049da | ||
|
|
883f1a5c11 | ||
|
|
dd8ac14f99 | ||
|
|
9bd024d39f | ||
|
|
e4611a8616 | ||
|
|
3f41e5dd77 | ||
|
|
a6fc88c79a | ||
|
|
3a4ec1062b | ||
|
|
7347ba88ac | ||
|
|
3b544a36a8 | ||
|
|
2851d0c7e6 | ||
|
|
2b296badea | ||
|
|
a12c4ee909 | ||
|
|
7db9c9115e | ||
|
|
503985a080 | ||
|
|
9f466162b0 | ||
|
|
8bea3f8997 | ||
|
|
1a3df84f0a | ||
|
|
2b3c47148e | ||
|
|
59ca4397e2 | ||
|
|
98266b0739 | ||
|
|
5a82ff106d | ||
|
|
2194430019 | ||
|
|
1f11015a2f | ||
|
|
c3421669d5 | ||
|
|
6e8adddc6d | ||
|
|
22a6ded4f0 | ||
|
|
0d7669db2c | ||
|
|
9526bca168 | ||
|
|
0683431f35 | ||
|
|
70790264b8 | ||
|
|
c8a68c83e3 | ||
|
|
23171f79d0 | ||
|
|
b7f05a12a3 | ||
|
|
6078dd092a | ||
|
|
81b85e97df | ||
|
|
cc16b8779c | ||
|
|
62f7c7f9ce | ||
|
|
7496e79e8c | ||
|
|
b8cf3e2cab | ||
|
|
93fab3d78f | ||
|
|
9c38c9ed1b | ||
|
|
38e99f2e87 | ||
|
|
68226786a2 | ||
|
|
455cd9717a | ||
|
|
fa79c5c36a | ||
|
|
0402d4b853 | ||
|
|
8a427955e7 | ||
|
|
bb4214e28a | ||
|
|
f924b04efd | ||
|
|
62f60867a9 | ||
|
|
b5844f1244 | ||
|
|
8c0093d453 | ||
|
|
34df4cd060 | ||
|
|
99eea3a662 | ||
|
|
6e1047032d | ||
|
|
b2a8748bc6 | ||
|
|
ded6d5b8eb | ||
|
|
832842c20c | ||
|
|
b43e4ae469 | ||
|
|
026afeb885 | ||
|
|
b6793a5e01 | ||
|
|
d231264a16 | ||
|
|
6e02bdec2e | ||
|
|
cfaadc40b1 | ||
|
|
93475b43a5 | ||
|
|
3348e051a1 | ||
|
|
dad1aea128 | ||
|
|
dec001114a | ||
|
|
1ce7b9f7de | ||
|
|
2ff99f12d8 | ||
|
|
6679705254 | ||
|
|
7eafac5b1a | ||
|
|
ac3aa949ad | ||
|
|
b4036c8671 | ||
|
|
4e0de1c066 | ||
|
|
f32890916d | ||
|
|
24a0dac420 | ||
|
|
716bc00533 | ||
|
|
32dd18998d | ||
|
|
5e62069444 | ||
|
|
bf63dc4ab7 | ||
|
|
7679546e30 | ||
|
|
45b11dc984 | ||
|
|
25d21a3da6 | ||
|
|
c8d49b5bf9 | ||
|
|
f737d9a794 | ||
|
|
df9c5ae53c | ||
|
|
e0829a75d2 | ||
|
|
1f58f7e93d | ||
|
|
14807ba036 | ||
|
|
75fb3894a6 | ||
|
|
18119bd11b | ||
|
|
4c7796537f | ||
|
|
3452cb01d0 | ||
|
|
6a151ef97a | ||
|
|
e3d651668c | ||
|
|
7a5704beaa | ||
|
|
37b78a9a4c | ||
|
|
8bc5a9d581 | ||
|
|
57ed162b79 | ||
|
|
3524231500 | ||
|
|
5cfe2b4762 | ||
|
|
a8bac1e468 | ||
|
|
4f798286af | ||
|
|
8a04b2d69d | ||
|
|
17893d9747 | ||
|
|
8af49406cd | ||
|
|
3b068b79fe | ||
|
|
ce14e83e78 | ||
|
|
f213d6fda7 | ||
|
|
83db00d7a3 | ||
|
|
7e9bd88eb4 | ||
|
|
ea716ca440 | ||
|
|
3cd7a04002 | ||
|
|
914002f846 | ||
|
|
17b82a186f | ||
|
|
7a8e957476 | ||
|
|
6dfd354973 | ||
|
|
956c12eb32 | ||
|
|
d5ae4eed26 | ||
|
|
ca486e5ed9 | ||
|
|
de63066b0b | ||
|
|
c7ca55ceb5 | ||
|
|
024bef7ded | ||
|
|
34d3d60f95 | ||
|
|
ed907d7c6f | ||
|
|
9d7fd66546 | ||
|
|
9934841950 | ||
|
|
432fe1100b | ||
|
|
d987628935 | ||
|
|
4c3ceae0e4 | ||
|
|
2b9d7538bf | ||
|
|
4ca1fc083d | ||
|
|
e7585271fb | ||
|
|
6e6824117d | ||
|
|
3e5f2fd8b2 | ||
|
|
6e470d788e | ||
|
|
a40d8092ce | ||
|
|
0234a70bfd | ||
|
|
705b1e0c2b | ||
|
|
69379334f9 | ||
|
|
12e50a1f2f | ||
|
|
ec17a477be | ||
|
|
205e1c7a59 | ||
|
|
ffb60c06cb | ||
|
|
2280f59ee6 | ||
|
|
0d4dd63edc | ||
|
|
69f1f4c1a5 | ||
|
|
d930d9c237 | ||
|
|
9c782d130f | ||
|
|
f12053f39d | ||
|
|
bcf6ca4f87 | ||
|
|
46619bb425 | ||
|
|
cdd870a129 | ||
|
|
7a1b7b9438 | ||
|
|
2183ada1f2 | ||
|
|
1d9e64ec73 | ||
|
|
5339be318e | ||
|
|
e1ba39c437 | ||
|
|
07ee271478 | ||
|
|
4823cce622 | ||
|
|
23099f7e8b | ||
|
|
5bdbd51fa8 | ||
|
|
a0e5871c6e | ||
|
|
3cf0904651 | ||
|
|
2bd06ff0fd | ||
|
|
6ea333d0f1 |
17
.ci/macos.entitlements
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<false/>
|
||||
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
|
||||
<key>com.apple.security.cs.disable-executable-page-protection</key>
|
||||
<true/>
|
||||
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
86
.github/workflows/desktop-build.yml
vendored
@@ -217,10 +217,18 @@ jobs:
|
||||
core_count: 3
|
||||
make_package: 1
|
||||
|
||||
- target: 14
|
||||
- target: 15
|
||||
soc: Apple
|
||||
os: macos-14
|
||||
xcode: "15.4"
|
||||
os: macos-15
|
||||
xcode: "16.2"
|
||||
type: Release
|
||||
core_count: 3
|
||||
make_package: 1
|
||||
|
||||
- target: 15
|
||||
soc: Apple
|
||||
os: macos-15
|
||||
xcode: "16.2"
|
||||
type: Debug
|
||||
core_count: 3
|
||||
|
||||
@@ -246,7 +254,7 @@ jobs:
|
||||
brew update
|
||||
brew install protobuf qt --force-bottle
|
||||
|
||||
- name: Build on Xcode ${{matrix.xcode}}
|
||||
- name: Build & Sign on Xcode ${{matrix.xcode}}
|
||||
shell: bash
|
||||
id: build
|
||||
env:
|
||||
@@ -254,10 +262,69 @@ jobs:
|
||||
MAKE_TEST: 1
|
||||
MAKE_PACKAGE: '${{matrix.make_package}}'
|
||||
PACKAGE_SUFFIX: '-macOS${{matrix.target}}_${{matrix.soc}}'
|
||||
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
|
||||
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
|
||||
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
|
||||
# macOS runner have 3 cores usually - only the macos-13 image has 4:
|
||||
# https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories
|
||||
# https://github.com/actions/runner-images?tab=readme-ov-file#available-images
|
||||
run: .ci/compile.sh --server --parallel ${{matrix.core_count}}
|
||||
run: |
|
||||
if [[ -n "$MACOS_CERTIFICATE_NAME" ]]
|
||||
then
|
||||
echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12
|
||||
security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security set-keychain-settings -t 3600 -l build.keychain
|
||||
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
|
||||
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain
|
||||
fi
|
||||
.ci/compile.sh --server --parallel ${{matrix.core_count}}
|
||||
|
||||
- name: Sign app bundle
|
||||
if: matrix.make_package
|
||||
env:
|
||||
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
|
||||
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
|
||||
run: |
|
||||
if [[ -n "$MACOS_CERTIFICATE_NAME" ]]
|
||||
then
|
||||
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
|
||||
/usr/bin/codesign --sign="$MACOS_CERTIFICATE_NAME" --entitlements=".ci/macos.entitlements" --options=runtime --force --deep --timestamp --verbose ${{steps.build.outputs.path}}
|
||||
fi
|
||||
|
||||
- name: Notarize app bundle
|
||||
if: matrix.make_package
|
||||
env:
|
||||
MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
|
||||
MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
|
||||
MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}
|
||||
run: |
|
||||
if [[ -n "$MACOS_NOTARIZATION_APPLE_ID" ]]
|
||||
then
|
||||
# Store the notarization credentials so that we can prevent a UI password dialog from blocking the CI
|
||||
echo "Create keychain profile"
|
||||
xcrun notarytool store-credentials "notarytool-profile" --apple-id "$MACOS_NOTARIZATION_APPLE_ID" --team-id "$MACOS_NOTARIZATION_TEAM_ID" --password "$MACOS_NOTARIZATION_PWD"
|
||||
|
||||
# We can't notarize an app bundle directly, but we need to compress it as an archive.
|
||||
# Therefore, we create a zip file containing our app bundle, so that we can send it to the
|
||||
# notarization service
|
||||
echo "Creating temp notarization archive"
|
||||
ditto -c -k --keepParent ${{steps.build.outputs.path}} "notarization.zip"
|
||||
|
||||
# Here we send the notarization request to the Apple's Notarization service, waiting for the result.
|
||||
# This typically takes a few seconds inside a CI environment, but it might take more depending on the App
|
||||
# characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if
|
||||
# you're curious
|
||||
echo "Notarize app"
|
||||
xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait
|
||||
|
||||
# Finally, we need to "attach the staple" to our executable, which will allow our app to be
|
||||
# validated by macOS even when an internet connection is not available.
|
||||
echo "Attach staple"
|
||||
xcrun stapler staple ${{steps.build.outputs.path}}
|
||||
fi
|
||||
|
||||
- name: Upload artifact
|
||||
if: matrix.make_package
|
||||
@@ -347,6 +414,15 @@ jobs:
|
||||
path: ${{steps.build.outputs.path}}
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload pdb database
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Windows${{matrix.target}}-debug-pdbs
|
||||
path: |
|
||||
build/cockatrice/Release/*.pdb
|
||||
build/servatrice/Release/*.pdb
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload to release
|
||||
if: matrix.package != 'skip' && needs.configure.outputs.tag != null
|
||||
shell: bash
|
||||
|
||||
3
.gitignore
vendored
@@ -6,10 +6,11 @@ mysql.cnf
|
||||
.DS_Store
|
||||
.idea/
|
||||
*.aps
|
||||
cmake-build-debug*
|
||||
cmake-build*
|
||||
preferences
|
||||
compile_commands.json
|
||||
.vs/
|
||||
.vscode/
|
||||
.cache
|
||||
.gdb_history
|
||||
cockatrice/resources/config/qtlogging.ini
|
||||
|
||||
@@ -140,12 +140,17 @@ endif()
|
||||
|
||||
# Define proper compilation flags
|
||||
if(MSVC)
|
||||
# Visual Studio: Disable Warning C4251, C++20 compatibility, Multi-threaded Builds, Warn Detection, Unwind Semantics
|
||||
set(CMAKE_CXX_FLAGS "/wd4251 /Zc:__cplusplus /std:c++20 /permissive- /W4 /MP /EHsc")
|
||||
# Disable Warning C4251, C++20 compatibility, Multi-threaded Builds, Warn Detection, Unwind Semantics, Debug Symbols
|
||||
set(CMAKE_CXX_FLAGS "/wd4251 /Zc:__cplusplus /std:c++20 /permissive- /W4 /MP /EHsc /Zi")
|
||||
# Visual Studio: Maximum Optimization, Multi-threaded DLL
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "/Ox /MD")
|
||||
# Visual Studio: No Optimization, Multi-threaded Debug DLL, Debug Symbols
|
||||
set(CMAKE_CXX_FLAGS_DEBUG "/Od /MDd /Zi")
|
||||
# Visual Studio: No Optimization, Multi-threaded Debug DLL
|
||||
set(CMAKE_CXX_FLAGS_DEBUG "/Od /MDd")
|
||||
|
||||
# Generate PDB, even when in release (So developers can better analyze crash logs)
|
||||
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /DEBUG /OPT:REF /OPT:ICF")
|
||||
|
||||
add_compile_definitions(_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING)
|
||||
elseif(CMAKE_COMPILER_IS_GNUCXX)
|
||||
# linux/gcc, bsd/gcc, windows/mingw
|
||||
include(CheckCXXCompilerFlag)
|
||||
@@ -266,6 +271,7 @@ if(UNIX)
|
||||
set(CPACK_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/cockatrice/resources/appicon.icns")
|
||||
set(CPACK_DMG_DS_STORE_SETUP_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/CMakeDMGSetup.script")
|
||||
set(CPACK_DMG_BACKGROUND_IMAGE "${CMAKE_CURRENT_SOURCE_DIR}/cmake/dmgBackground.tif")
|
||||
set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/SignMacApplications.cmake")
|
||||
else()
|
||||
# linux
|
||||
if(CPACK_GENERATOR STREQUAL "RPM")
|
||||
|
||||
32
Dockerfile
@@ -1,19 +1,20 @@
|
||||
FROM ubuntu:bionic
|
||||
MAINTAINER Zach Halpern <zahalpern+github@gmail.com>
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get install -y\
|
||||
build-essential\
|
||||
cmake\
|
||||
git\
|
||||
libprotobuf-dev\
|
||||
libqt5sql5-mysql\
|
||||
libmysqlclient-dev\
|
||||
libqt5websockets5-dev\
|
||||
protobuf-compiler\
|
||||
qt5-default\
|
||||
qtbase5-dev\
|
||||
qttools5-dev-tools\
|
||||
qttools5-dev
|
||||
build-essential \
|
||||
cmake \
|
||||
file \
|
||||
g++ \
|
||||
git \
|
||||
libmariadb-dev-compat \
|
||||
libprotobuf-dev \
|
||||
libqt6sql6-mysql \
|
||||
qt6-websockets-dev \
|
||||
protobuf-compiler \
|
||||
qt6-tools-dev \
|
||||
qt6-tools-dev-tools
|
||||
|
||||
COPY . /home/servatrice/code/
|
||||
WORKDIR /home/servatrice/code
|
||||
@@ -25,7 +26,6 @@ RUN cmake .. -DWITH_SERVER=1 -DWITH_CLIENT=0 -DWITH_ORACLE=0 -DWITH_DBCONVERTER=
|
||||
|
||||
WORKDIR /home/servatrice
|
||||
|
||||
EXPOSE 4747
|
||||
EXPOSE 4747 4748
|
||||
|
||||
ENTRYPOINT [ "servatrice", "--log-to-console" ]
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ if(WITH_CLIENT)
|
||||
Svg
|
||||
WebSockets
|
||||
Widgets
|
||||
Xml
|
||||
)
|
||||
endif()
|
||||
if(WITH_ORACLE)
|
||||
|
||||
@@ -5,7 +5,7 @@ OutFile "@CPACK_TOPLEVEL_DIRECTORY@/@CPACK_OUTPUT_FILE_NAME@"
|
||||
|
||||
!define INST_DIR "@CPACK_TEMPORARY_DIRECTORY@"
|
||||
|
||||
RequestExecutionlevel highest
|
||||
RequestExecutionlevel admin
|
||||
SetCompressor LZMA
|
||||
|
||||
Var NormalDestDir
|
||||
@@ -235,6 +235,13 @@ ${If} $PortableMode = 0
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
||||
IntFmt $0 "0x%08X" $0
|
||||
|
||||
; Enable Windows User-Mode Dumps
|
||||
; https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps
|
||||
WriteRegExpandStr HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe" "DumpFolder" "%LOCALAPPDATA%\CrashDumps\Cockatrice"
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe" "DumpCount" "5"
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe" "DumpType" "2"
|
||||
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayIcon" "$INSTDIR\cockatrice.exe"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayName" "Cockatrice"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayVersion" "@CPACK_PACKAGE_VERSION_MAJOR@.@CPACK_PACKAGE_VERSION_MINOR@.@CPACK_PACKAGE_VERSION_PATCH@"
|
||||
|
||||
27
cmake/SignMacApplications.cmake
Normal file
@@ -0,0 +1,27 @@
|
||||
# This script re-signs all apps after CPack packages them. This is necessary because CPack modifies
|
||||
# the library references used by Cockatrice to App relative paths, invalidating the code signature.
|
||||
string(LENGTH "$ENV{MACOS_CERTIFICATE_NAME}" MACOS_CERTIFICATE_NAME_LEN)
|
||||
|
||||
if(APPLE AND MACOS_CERTIFICATE_NAME_LEN GREATER 0)
|
||||
set(APPLICATIONS "cockatrice" "servatrice" "oracle" "dbconverter")
|
||||
foreach(app_name IN LISTS APPLICATIONS)
|
||||
set(FULL_APP_PATH "${CPACK_TEMPORARY_INSTALL_DIRECTORY}/${app_name}.app")
|
||||
|
||||
message(STATUS "Signing Interior Dynamically Loaded Libraries for ${app_name}.app")
|
||||
execute_process(COMMAND "find" "${FULL_APP_PATH}" "-name" "*.dylib" OUTPUT_VARIABLE INTERIOR_DLLS)
|
||||
string(REPLACE "\n" ";" INTERIOR_DLLS_LIST ${INTERIOR_DLLS})
|
||||
|
||||
foreach(INTERIOR_DLL IN LISTS INTERIOR_DLLS_LIST)
|
||||
execute_process(
|
||||
COMMAND "codesign" "--sign" "$ENV{MACOS_CERTIFICATE_NAME}" "--entitlements" "../.ci/macos.entitlements"
|
||||
"--options" "runtime" "--force" "--deep" "--timestamp" "--verbose" "${INTERIOR_DLL}"
|
||||
)
|
||||
endforeach()
|
||||
|
||||
message(STATUS "Signing Exterior Applications ${app_name}.app")
|
||||
execute_process(
|
||||
COMMAND "codesign" "--sign" "$ENV{MACOS_CERTIFICATE_NAME}" "--entitlements" "../.ci/macos.entitlements"
|
||||
"--options" "runtime" "--force" "--deep" "--timestamp" "--verbose" "${FULL_APP_PATH}"
|
||||
)
|
||||
endforeach()
|
||||
endif()
|
||||
@@ -34,8 +34,8 @@ set(cockatrice_SOURCES
|
||||
src/deck/deck_loader.cpp
|
||||
src/deck/deck_list_model.cpp
|
||||
src/deck/deck_stats_interface.cpp
|
||||
src/deck/deck_view.cpp
|
||||
src/dialogs/dlg_connect.cpp
|
||||
src/dialogs/dlg_convert_deck_to_cod_format.cpp
|
||||
src/dialogs/dlg_create_token.cpp
|
||||
src/dialogs/dlg_create_game.cpp
|
||||
src/dialogs/dlg_edit_avatar.cpp
|
||||
@@ -56,13 +56,14 @@ set(cockatrice_SOURCES
|
||||
src/dialogs/dlg_tip_of_the_day.cpp
|
||||
src/dialogs/dlg_update.cpp
|
||||
src/dialogs/dlg_view_log.cpp
|
||||
src/dialogs/dlg_load_deck.cpp
|
||||
src/game/deckview/deck_view.cpp
|
||||
src/game/deckview/deck_view_container.cpp
|
||||
src/game/filters/filter_string.cpp
|
||||
src/game/filters/filter_builder.cpp
|
||||
src/game/filters/filter_tree.cpp
|
||||
src/game/filters/filter_tree_model.cpp
|
||||
src/client/ui/layouts/flow_layout.cpp
|
||||
src/client/ui/layouts/horizontal_flow_layout.cpp
|
||||
src/client/ui/layouts/vertical_flow_layout.cpp
|
||||
src/client/ui/widgets/general/layout_containers/flow_widget.cpp
|
||||
src/game/game_scene.cpp
|
||||
src/game/game_selector.cpp
|
||||
@@ -80,6 +81,7 @@ set(cockatrice_SOURCES
|
||||
src/utility/logger.cpp
|
||||
src/client/ui/widgets/cards/card_info_picture_enlarged_widget.cpp
|
||||
src/client/ui/widgets/cards/card_info_picture_with_text_overlay_widget.cpp
|
||||
src/client/ui/widgets/general/display/banner_widget.cpp
|
||||
src/client/ui/widgets/general/display/labeled_input.cpp
|
||||
src/client/ui/widgets/general/display/dynamic_font_size_label.cpp
|
||||
src/client/ui/widgets/general/display/dynamic_font_size_push_button.cpp
|
||||
@@ -92,7 +94,9 @@ set(cockatrice_SOURCES
|
||||
src/server/pending_command.cpp
|
||||
src/game/phase.cpp
|
||||
src/client/ui/phases_toolbar.cpp
|
||||
src/client/ui/picture_loader.cpp
|
||||
src/client/ui/picture_loader/picture_loader.cpp
|
||||
src/client/ui/picture_loader/picture_loader_worker.cpp
|
||||
src/client/ui/picture_loader/picture_to_load.cpp
|
||||
src/game/zones/pile_zone.cpp
|
||||
src/client/ui/pixel_map_generator.cpp
|
||||
src/game/player/player.cpp
|
||||
@@ -110,6 +114,7 @@ set(cockatrice_SOURCES
|
||||
src/client/ui/widgets/printing_selector/printing_selector_view_options_widget.cpp
|
||||
src/client/ui/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp
|
||||
src/client/network/release_channel.cpp
|
||||
src/client/network/client_update_checker.cpp
|
||||
src/server/remote/remote_client.cpp
|
||||
src/server/remote/remote_decklist_tree_widget.cpp
|
||||
src/server/remote/remote_replay_list_tree_widget.cpp
|
||||
@@ -122,10 +127,14 @@ set(cockatrice_SOURCES
|
||||
src/settings/game_filters_settings.cpp
|
||||
src/settings/layouts_settings.cpp
|
||||
src/settings/message_settings.cpp
|
||||
src/settings/recents_settings.cpp
|
||||
src/settings/servers_settings.cpp
|
||||
src/settings/settings_manager.cpp
|
||||
src/settings/cache_settings.cpp
|
||||
src/settings/shortcuts_settings.cpp
|
||||
src/settings/shortcut_treeview.cpp
|
||||
src/settings/card_override_settings.cpp
|
||||
src/settings/debug_settings.cpp
|
||||
src/client/sound_engine.cpp
|
||||
src/client/network/spoiler_background_updater.cpp
|
||||
src/game/zones/stack_zone.cpp
|
||||
@@ -141,6 +150,19 @@ set(cockatrice_SOURCES
|
||||
src/client/tabs/tab_room.cpp
|
||||
src/client/tabs/tab_server.cpp
|
||||
src/client/tabs/tab_supervisor.cpp
|
||||
src/client/tabs/api/edhrec/tab_edhrec.cpp
|
||||
src/client/tabs/api/edhrec/edhrec_commander_api_response_display_widget.cpp
|
||||
src/client/tabs/api/edhrec/edhrec_commander_api_response_card_details_display_widget.cpp
|
||||
src/client/tabs/api/edhrec/edhrec_commander_api_response_card_list_display_widget.cpp
|
||||
src/client/tabs/api/edhrec/edhrec_commander_api_response_commander_details_display_widget.cpp
|
||||
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_archidekt_links.cpp
|
||||
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_average_deck_statistics.cpp
|
||||
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_details.cpp
|
||||
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_list.cpp
|
||||
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_container.cpp
|
||||
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_prices.cpp
|
||||
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_commander_details.cpp
|
||||
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response.cpp
|
||||
src/game/zones/table_zone.cpp
|
||||
src/client/tapped_out_interface.cpp
|
||||
src/client/ui/theme_manager.cpp
|
||||
@@ -150,15 +172,34 @@ set(cockatrice_SOURCES
|
||||
src/server/user/user_context_menu.cpp
|
||||
src/server/user/user_info_connection.cpp
|
||||
src/server/user/user_info_box.cpp
|
||||
src/server/user/user_list.cpp
|
||||
src/server/user/user_list_manager.cpp
|
||||
src/server/user/user_list_widget.cpp
|
||||
src/client/ui/window_main.cpp
|
||||
src/game/zones/view_zone_widget.cpp
|
||||
src/game/zones/view_zone.cpp
|
||||
src/client/tabs/visual_deck_storage/tab_deck_storage_visual.cpp
|
||||
src/client/ui/widgets/cards/deck_preview_card_picture_widget.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_widget.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_dialog.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/visual_deck_storage_widget.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp
|
||||
src/client/ui/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp
|
||||
${VERSION_STRING_CPP}
|
||||
)
|
||||
|
||||
add_subdirectory(sounds)
|
||||
add_subdirectory(themes)
|
||||
configure_file(
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/resources/config/qtlogging.ini ${CMAKE_BINARY_DIR}/cockatrice/qtlogging.ini COPYONLY
|
||||
)
|
||||
|
||||
set(cockatrice_RESOURCES cockatrice.qrc)
|
||||
|
||||
@@ -279,7 +320,7 @@ if(APPLE)
|
||||
set(plugin_dest_dir cockatrice.app/Contents/Plugins)
|
||||
set(qtconf_dest_dir cockatrice.app/Contents/Resources)
|
||||
|
||||
# Qt plugins: audio (Qt5), iconengines, imageformats, platforms, printsupport (Qt5), styles, tls (Qt6)
|
||||
# Qt plugins: audio (Qt5), iconengines, imageformats, multimedia (Qt6), platforms, printsupport (Qt5), styles, tls (Qt6)
|
||||
install(
|
||||
DIRECTORY "${QT_PLUGINS_DIR}/"
|
||||
DESTINATION ${plugin_dest_dir}
|
||||
@@ -290,6 +331,7 @@ if(APPLE)
|
||||
PATTERN "audio/*.dylib"
|
||||
PATTERN "iconengines/*.dylib"
|
||||
PATTERN "imageformats/*.dylib"
|
||||
PATTERN "multimedia/*.dylib"
|
||||
PATTERN "platforms/*.dylib"
|
||||
PATTERN "printsupport/*.dylib"
|
||||
PATTERN "styles/*.dylib"
|
||||
@@ -330,7 +372,7 @@ if(WIN32)
|
||||
PATTERN "*.dll"
|
||||
)
|
||||
|
||||
# Qt plugins: audio (Qt5), iconengines, imageformats, platforms, printsupport (Qt5), styles, tls (Qt6)
|
||||
# Qt plugins: audio (Qt5), iconengines, imageformats, multimedia (Qt6) platforms, printsupport (Qt5), styles, tls (Qt6)
|
||||
install(
|
||||
DIRECTORY "${QT_PLUGINS_DIR}/"
|
||||
DESTINATION ${plugin_dest_dir}
|
||||
@@ -342,6 +384,7 @@ if(WIN32)
|
||||
PATTERN "imageformats/*.dll"
|
||||
PATTERN "mediaservice/dsengine.dll"
|
||||
PATTERN "mediaservice/wmfengine.dll"
|
||||
PATTERN "multimedia/*.dll"
|
||||
PATTERN "platforms/qdirect2d.dll"
|
||||
PATTERN "platforms/qminimal.dll"
|
||||
PATTERN "platforms/qoffscreen.dll"
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<file>resources/icons/search.svg</file>
|
||||
<file>resources/icons/settings.svg</file>
|
||||
<file>resources/icons/spectator.svg</file>
|
||||
<file>resources/icons/swap.svg</file>
|
||||
<file>resources/icons/sync.svg</file>
|
||||
<file>resources/icons/tab_changed.svg</file>
|
||||
<file>resources/icons/update.png</file>
|
||||
@@ -41,6 +42,8 @@
|
||||
<file>resources/config/deckeditor.svg</file>
|
||||
<file>resources/config/shorcuts.svg</file>
|
||||
<file>resources/config/sound.svg</file>
|
||||
<file>resources/config/debug.ini</file>
|
||||
<file>resources/config/qtlogging.ini</file>
|
||||
|
||||
<file>resources/counters/w.svg</file>
|
||||
<file>resources/counters/w_highlight.svg</file>
|
||||
@@ -326,6 +329,13 @@
|
||||
<file>resources/replay/fastforward.svg</file>
|
||||
<file>resources/replay/pause.svg</file>
|
||||
|
||||
<file>resources/usericons/pawn_single.svg</file>
|
||||
<file>resources/usericons/pawn_double.svg</file>
|
||||
<file>resources/usericons/pawn_vip_single.svg</file>
|
||||
<file>resources/usericons/pawn_vip_double.svg</file>
|
||||
<file>resources/usericons/star_single.svg</file>
|
||||
<file>resources/usericons/star_double.svg</file>
|
||||
|
||||
<file>resources/userlevels/normal.svg</file>
|
||||
<file>resources/userlevels/registered.svg</file>
|
||||
<file>resources/userlevels/registered_buddy.svg</file>
|
||||
|
||||
11
cockatrice/resources/config/debug.ini
Normal file
@@ -0,0 +1,11 @@
|
||||
[debug]
|
||||
showCardId=false
|
||||
|
||||
[localgame]
|
||||
onStartup=false
|
||||
playerCount=1
|
||||
;deck\Player 1=path/to/deck
|
||||
;deck\Player 2=path/to/deck
|
||||
|
||||
; Fun Fact: You can assign a deck to your username and it will auto load and ready when you join a server game
|
||||
;deck\Your Username Here=path/to/deck
|
||||
49
cockatrice/resources/config/qtlogging.ini
Normal file
@@ -0,0 +1,49 @@
|
||||
[Rules]
|
||||
# Uncomment a rule to disable logging for that category
|
||||
|
||||
# main = false
|
||||
# qt_translator = false
|
||||
# window_main.* = false
|
||||
# release_channel = false
|
||||
# spoiler_background_updater = false
|
||||
# theme_manager = false
|
||||
# sound_engine = false
|
||||
# tapped_out_interface = false
|
||||
|
||||
# tab_game = false
|
||||
# tab_message = false
|
||||
# tab_supervisor = false
|
||||
|
||||
# dlg_edit_avatar = false
|
||||
# dlg_settings = false
|
||||
# dlg_tip_of_the_day = false
|
||||
# dlg_update = false
|
||||
|
||||
# settings_cache = false
|
||||
# servers_settings = false
|
||||
# shortcuts_settings = false
|
||||
|
||||
# player = false
|
||||
# game_scene = false
|
||||
# game_scene.player_addition_removal = false
|
||||
# card_zone = false
|
||||
# view_zone = false
|
||||
|
||||
# user_info_connection = false
|
||||
|
||||
# picture_loader = false
|
||||
# picture_loader.worker = false
|
||||
# picture_loader.card_back_cache_fail = false
|
||||
# picture_loader.picture_to_load = false
|
||||
# deck_loader = false
|
||||
# card_database = false
|
||||
# card_database.loading = false
|
||||
# card_database.loading.success_or_failure = false
|
||||
# cockatrice_xml.* = false
|
||||
# cockatrice_xml.xml_3_parser = false
|
||||
# cockatrice_xml.xml_4_parser = false
|
||||
# card_list = false
|
||||
|
||||
# pixel_map_generator = false
|
||||
|
||||
# filter_string = false
|
||||
12
cockatrice/resources/icons/swap.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000" version="1.1" id="Capa_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="800px" height="800px" viewBox="0 0 71.753 71.753"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<path d="M39.798,20.736H28.172v20.738L11.625,41.47V20.736H0L19.899,0.839L39.798,20.736z M51.855,70.914l19.897-19.896H60.129
|
||||
V30.282l-16.547-0.004v20.74H31.957L51.855,70.914z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 648 B |
268
cockatrice/resources/usericons/pawn_double.svg
Normal file
@@ -0,0 +1,268 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="100"
|
||||
height="100"
|
||||
id="svg5322"
|
||||
version="1.1"
|
||||
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
|
||||
sodipodi:docname="pawn_double.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs3">
|
||||
<inkscape:perspective
|
||||
sodipodi:type="inkscape:persp3d"
|
||||
inkscape:vp_x="0 : 526.18109 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_z="744.09448 : 526.18109 : 1"
|
||||
inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
|
||||
id="perspective5328" />
|
||||
<inkscape:perspective
|
||||
id="perspective5305"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<linearGradient
|
||||
id="linearGradient5181">
|
||||
<stop
|
||||
style="stop-color:#0fbb00;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5183" />
|
||||
<stop
|
||||
style="stop-color:#064400;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5185" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600-2"
|
||||
id="radialGradient3606-7"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
|
||||
<linearGradient
|
||||
id="linearGradient3600-2">
|
||||
<stop
|
||||
style="stop-color:#ffc33d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602-4" />
|
||||
<stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604-9" />
|
||||
</linearGradient>
|
||||
<inkscape:perspective
|
||||
id="perspective5478"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<linearGradient
|
||||
id="linearGradient5189">
|
||||
<stop
|
||||
style="stop-color:#000ec9;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5191" />
|
||||
<stop
|
||||
style="stop-color:#000657;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5193" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600-4"
|
||||
id="radialGradient3606-1"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
|
||||
<linearGradient
|
||||
id="linearGradient3600-4">
|
||||
<stop
|
||||
style="stop-color:#ffc33d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602-3" />
|
||||
<stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604-5" />
|
||||
</linearGradient>
|
||||
<inkscape:perspective
|
||||
id="perspective5559"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient5173"
|
||||
id="linearGradient5179"
|
||||
x1="167.33386"
|
||||
y1="178.83276"
|
||||
x2="244.78181"
|
||||
y2="178.83276"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
id="linearGradient5173">
|
||||
<stop
|
||||
style="stop-color:#f50000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5175" />
|
||||
<stop
|
||||
style="stop-color:#950000;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5177" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600"
|
||||
id="radialGradient5169"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276" />
|
||||
<linearGradient
|
||||
id="linearGradient3600">
|
||||
<stop
|
||||
style="stop-color:#ffc13d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602" />
|
||||
<stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
r="25.501276"
|
||||
fy="131.40274"
|
||||
fx="324.32715"
|
||||
cy="131.40274"
|
||||
cx="324.32715"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="radialGradient5574"
|
||||
xlink:href="#linearGradient3600"
|
||||
inkscape:collect="always" />
|
||||
<inkscape:perspective
|
||||
id="perspective5663"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600-7"
|
||||
id="radialGradient3606-8"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
|
||||
<linearGradient
|
||||
id="linearGradient3600-7">
|
||||
<stop
|
||||
style="stop-color:#ffc13d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602-7" />
|
||||
<stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604-6" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
r="25.501276"
|
||||
fy="131.40274"
|
||||
fx="324.32715"
|
||||
cy="131.40274"
|
||||
cx="324.32715"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="radialGradient5676"
|
||||
xlink:href="#linearGradient3600-7"
|
||||
inkscape:collect="always" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
inkscape:document-units="mm"
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="5.6"
|
||||
inkscape:cx="63.214286"
|
||||
inkscape:cy="46.160714"
|
||||
inkscape:current-layer="g5249"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1147"
|
||||
inkscape:window-height="1211"
|
||||
inkscape:window-x="2842"
|
||||
inkscape:window-y="58"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1" />
|
||||
<metadata
|
||||
id="metadata4">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-952.36218)">
|
||||
<g
|
||||
id="g5249"
|
||||
transform="translate(0.53874115,0.90502985)">
|
||||
<path
|
||||
style="stroke:#000000;stroke-width:4.45809746000000030;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;fill-opacity:1"
|
||||
d="m 49.582319,954.34642 c -12.850034,0 -23.284789,10.43476 -23.284789,23.28479 0,7.32135 3.403263,13.81811 8.689724,18.08319 -10.278401,5.8502 -16.663073,17.8469 -20.19443,31.0259 -5.1178053,19.1 15.207096,22.0401 34.269334,22.0915 l 0,0.031 c 0.290839,0 0.566498,0.031 0.856734,0.031 19.210152,0 39.855802,-3.1837 34.789494,-22.0914 -3.636192,-13.5705 -10.027831,-25.4711 -20.378015,-31.17899 5.208701,-4.26694 8.506139,-10.73278 8.506139,-17.9914 0,-12.85003 -10.404159,-23.28479 -23.254191,-23.28479 z"
|
||||
id="left"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
fill="none"
|
||||
style="stroke:#000000;stroke-width:1.97203517px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 50.522358,952.70715 0,95.71425"
|
||||
id="center"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="opacity:1;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.71966;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 51.054254,1001.4773 v -45.77683 l 1.097241,0.005 c 3.642211,0.0172 9.170661,2.46935 12.395732,5.49816 4.897489,4.59945 7.421654,10.97001 6.981907,17.62114 -0.389167,5.88609 -2.631878,10.66609 -6.951818,14.81672 l -2.05562,1.97506 2.959813,2.0746 c 3.467097,2.43015 7.403677,6.55065 9.666109,10.11765 3.325898,5.2437 6.79289,13.8355 8.153827,20.2065 2.584451,12.0989 -5.997953,18.2384 -26.592174,19.0232 l -5.655017,0.2154 v -45.7768 z"
|
||||
id="right"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.2 KiB |
253
cockatrice/resources/usericons/pawn_single.svg
Normal file
@@ -0,0 +1,253 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="100"
|
||||
height="100"
|
||||
id="svg5322"
|
||||
version="1.1"
|
||||
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
|
||||
sodipodi:docname="pawn_single.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs3">
|
||||
<inkscape:perspective
|
||||
sodipodi:type="inkscape:persp3d"
|
||||
inkscape:vp_x="0 : 526.18109 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_z="744.09448 : 526.18109 : 1"
|
||||
inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
|
||||
id="perspective5328" />
|
||||
<inkscape:perspective
|
||||
id="perspective5305"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<linearGradient
|
||||
id="linearGradient5181">
|
||||
<stop
|
||||
style="stop-color:#0fbb00;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5183" />
|
||||
<stop
|
||||
style="stop-color:#064400;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5185" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600-2"
|
||||
id="radialGradient3606-7"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
|
||||
<linearGradient
|
||||
id="linearGradient3600-2">
|
||||
<stop
|
||||
style="stop-color:#ffc33d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602-4" />
|
||||
<stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604-9" />
|
||||
</linearGradient>
|
||||
<inkscape:perspective
|
||||
id="perspective5478"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<linearGradient
|
||||
id="linearGradient5189">
|
||||
<stop
|
||||
style="stop-color:#000ec9;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5191" />
|
||||
<stop
|
||||
style="stop-color:#000657;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5193" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600-4"
|
||||
id="radialGradient3606-1"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
|
||||
<linearGradient
|
||||
id="linearGradient3600-4">
|
||||
<stop
|
||||
style="stop-color:#ffc33d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602-3" />
|
||||
<stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604-5" />
|
||||
</linearGradient>
|
||||
<inkscape:perspective
|
||||
id="perspective5559"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient5173"
|
||||
id="linearGradient5179"
|
||||
x1="167.33386"
|
||||
y1="178.83276"
|
||||
x2="244.78181"
|
||||
y2="178.83276"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
id="linearGradient5173">
|
||||
<stop
|
||||
style="stop-color:#f50000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5175" />
|
||||
<stop
|
||||
style="stop-color:#950000;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5177" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600"
|
||||
id="radialGradient5169"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276" />
|
||||
<linearGradient
|
||||
id="linearGradient3600">
|
||||
<stop
|
||||
style="stop-color:#ffc13d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602" />
|
||||
<stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
r="25.501276"
|
||||
fy="131.40274"
|
||||
fx="324.32715"
|
||||
cy="131.40274"
|
||||
cx="324.32715"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="radialGradient5574"
|
||||
xlink:href="#linearGradient3600"
|
||||
inkscape:collect="always" />
|
||||
<inkscape:perspective
|
||||
id="perspective5663"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600-7"
|
||||
id="radialGradient3606-8"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
|
||||
<linearGradient
|
||||
id="linearGradient3600-7">
|
||||
<stop
|
||||
style="stop-color:#ffc13d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602-7" />
|
||||
<stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604-6" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
r="25.501276"
|
||||
fy="131.40274"
|
||||
fx="324.32715"
|
||||
cy="131.40274"
|
||||
cx="324.32715"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="radialGradient5676"
|
||||
xlink:href="#linearGradient3600-7"
|
||||
inkscape:collect="always" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
inkscape:document-units="mm"
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="5.6"
|
||||
inkscape:cx="63.214286"
|
||||
inkscape:cy="46.160714"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1147"
|
||||
inkscape:window-height="1211"
|
||||
inkscape:window-x="3185"
|
||||
inkscape:window-y="44"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1" />
|
||||
<metadata
|
||||
id="metadata4">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-952.36218)">
|
||||
<path
|
||||
style="fill-opacity:1;stroke:black;stroke-width:2.78220296;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;opacity:1"
|
||||
d="M 49.84375 1.71875 C 36.719738 1.71875 26.0625 12.375988 26.0625 25.5 C 26.0625 32.977454 29.538325 39.612734 34.9375 43.96875 C 24.439951 49.943698 17.919149 62.196126 14.3125 75.65625 C 9.0380874 95.34065 30.224013 98.21875 49.84375 98.21875 C 69.463486 98.21875 90.549327 94.96715 85.375 75.65625 C 81.693381 61.916246 75.224585 49.827177 64.8125 43.9375 C 70.181573 39.580662 73.59375 32.953205 73.59375 25.5 C 73.59375 12.375988 62.967762 1.71875 49.84375 1.71875 z "
|
||||
id="left"
|
||||
transform="translate(0,952.36218)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.1 KiB |
368
cockatrice/resources/usericons/pawn_vip_double.svg
Normal file
@@ -0,0 +1,368 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="100"
|
||||
height="100"
|
||||
id="svg5322"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="moderator_vip.svg">
|
||||
<defs
|
||||
id="defs3">
|
||||
<inkscape:perspective
|
||||
sodipodi:type="inkscape:persp3d"
|
||||
inkscape:vp_x="0 : 526.18109 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_z="744.09448 : 526.18109 : 1"
|
||||
inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
|
||||
id="perspective5328" />
|
||||
<inkscape:perspective
|
||||
id="perspective5305"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<linearGradient
|
||||
id="linearGradient5181">
|
||||
<stop
|
||||
style="stop-color:#0fbb00;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5183" />
|
||||
<stop
|
||||
style="stop-color:#064400;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5185" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600-2"
|
||||
id="radialGradient3606-7"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
|
||||
<linearGradient
|
||||
id="linearGradient3600-2">
|
||||
<stop
|
||||
style="stop-color:#ffc33d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602-4" />
|
||||
<stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604-9" />
|
||||
</linearGradient>
|
||||
<inkscape:perspective
|
||||
id="perspective5478"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<linearGradient
|
||||
id="linearGradient5189">
|
||||
<stop
|
||||
style="stop-color:#000ec9;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5191" />
|
||||
<stop
|
||||
style="stop-color:#000657;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5193" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600-4"
|
||||
id="radialGradient3606-1"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
|
||||
<linearGradient
|
||||
id="linearGradient3600-4">
|
||||
<stop
|
||||
style="stop-color:#ffc33d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602-3" />
|
||||
<stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604-5" />
|
||||
</linearGradient>
|
||||
<inkscape:perspective
|
||||
id="perspective5559"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient5173"
|
||||
id="linearGradient5179"
|
||||
x1="167.33386"
|
||||
y1="178.83276"
|
||||
x2="244.78181"
|
||||
y2="178.83276"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
id="linearGradient5173">
|
||||
<stop
|
||||
style="stop-color:#f50000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5175" />
|
||||
<stop
|
||||
style="stop-color:#950000;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5177" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600"
|
||||
id="radialGradient5169"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276" />
|
||||
<linearGradient
|
||||
id="linearGradient3600">
|
||||
<stop
|
||||
style="stop-color:#ffc13d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602" />
|
||||
<stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
r="25.501276"
|
||||
fy="131.40274"
|
||||
fx="324.32715"
|
||||
cy="131.40274"
|
||||
cx="324.32715"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="radialGradient5574"
|
||||
xlink:href="#linearGradient3600"
|
||||
inkscape:collect="always" />
|
||||
<inkscape:perspective
|
||||
id="perspective5663"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600-7"
|
||||
id="radialGradient3606-8"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
|
||||
<linearGradient
|
||||
id="linearGradient3600-7">
|
||||
<stop
|
||||
style="stop-color:#ffc13d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602-7" />
|
||||
<stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604-6" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600-7"
|
||||
id="radialGradient5254"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient5189-1"
|
||||
id="linearGradient5394"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="385.03503"
|
||||
y1="180.09546"
|
||||
x2="462.48297"
|
||||
y2="180.09546"
|
||||
gradientTransform="matrix(0.96839241,0,0,0.96839241,-360.365,847.52359)" />
|
||||
<linearGradient
|
||||
id="linearGradient5189-1">
|
||||
<stop
|
||||
style="stop-color:#000ec9;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5191-0" />
|
||||
<stop
|
||||
style="stop-color:#000657;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5193-4" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient5173-1"
|
||||
id="linearGradient5581"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="167.33386"
|
||||
y1="178.83276"
|
||||
x2="244.78181"
|
||||
y2="178.83276"
|
||||
gradientTransform="matrix(0.96839241,0,0,0.96839241,-149.54484,848.74636)" />
|
||||
<linearGradient
|
||||
id="linearGradient5173-1">
|
||||
<stop
|
||||
style="stop-color:#f50000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5175-5" />
|
||||
<stop
|
||||
style="stop-color:#950000;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5177-3" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient5181-9"
|
||||
id="linearGradient5782"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="282.50455"
|
||||
y1="181.61069"
|
||||
x2="359.95248"
|
||||
y2="181.61069"
|
||||
gradientTransform="matrix(0.96839241,0,0,0.96839241,-261.07526,846.05625)" />
|
||||
<linearGradient
|
||||
id="linearGradient5181-9">
|
||||
<stop
|
||||
style="stop-color:#80d600;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5183-3" />
|
||||
<stop
|
||||
style="stop-color:#80d600;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5185-0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
y2="181.61069"
|
||||
x2="359.95248"
|
||||
y1="181.61069"
|
||||
x1="282.50455"
|
||||
gradientTransform="matrix(0.96839241,0,0,0.96839241,-175.71812,893.2775)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="linearGradient5799"
|
||||
xlink:href="#linearGradient5181-9"
|
||||
inkscape:collect="always" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient3419">
|
||||
<stop
|
||||
style="stop-color:#ffec79;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop3421" />
|
||||
<stop
|
||||
style="stop-color:#f2c15b;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3423" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3419"
|
||||
id="linearGradient3425-5"
|
||||
x1="-126.90256"
|
||||
y1="941.52618"
|
||||
x2="-125.73831"
|
||||
y2="984.13751"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(154.2532,0.90556908)" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
inkscape:document-units="mm"
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="4.2946338"
|
||||
inkscape:cx="34.155294"
|
||||
inkscape:cy="65.175571"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1440"
|
||||
inkscape:window-height="792"
|
||||
inkscape:window-x="11"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata4">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-952.36218)">
|
||||
<path
|
||||
style="fill-opacity:1;stroke:black;stroke-width:2.78220296;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
d="M 49.84375 1.71875 C 36.719738 1.71875 26.0625 12.375988 26.0625 25.5 C 26.0625 32.977454 29.538325 39.612734 34.9375 43.96875 C 24.439951 49.943698 17.919149 62.196126 14.3125 75.65625 C 9.0380874 95.34065 30.224013 98.21875 49.84375 98.21875 C 69.463486 98.21875 90.549327 94.96715 85.375 75.65625 C 81.693381 61.916246 75.224585 49.827177 64.8125 43.9375 C 70.181573 39.580662 73.59375 32.953205 73.59375 25.5 C 73.59375 12.375988 62.967762 1.71875 49.84375 1.71875 z "
|
||||
transform="translate(0,952.36218)"
|
||||
id="right" />
|
||||
<path
|
||||
style="opacity:1;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.73577702;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 51.28696,1001.834 0,-46.98372 1.434151,0.16768 c 5.155008,0.60274 9.462857,2.72154 12.938257,6.36366 4.74393,4.9715 6.87913,11.35611 6.16464,18.43328 -0.53702,5.31935 -3.09008,10.59498 -6.83833,14.13074 l -1.94072,1.83069 3.04083,2.20427 c 3.58084,2.5957 7.18975,6.4912 9.55296,10.3116 4.89572,7.9144 9.23593,21.4918 8.50487,26.6055 -0.81312,5.6877 -5.43872,9.6977 -13.62216,11.8093 -3.80822,0.9826 -7.68056,1.4713 -14.763321,1.8633 l -4.471177,0.2474 0,-46.9837 z"
|
||||
id="left"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:url(#linearGradient3425-5);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="star"
|
||||
sodipodi:sides="5"
|
||||
sodipodi:cx="27.80283"
|
||||
sodipodi:cy="970.9433"
|
||||
sodipodi:r1="17.189852"
|
||||
sodipodi:r2="8.5949249"
|
||||
sodipodi:arg1="0.9349579"
|
||||
sodipodi:arg2="1.5632764"
|
||||
inkscape:flatsided="false"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 38.011063,984.77381 -10.143601,-5.23583 -10.063711,5.38779 1.845023,-11.2651 -8.233948,-7.90624 11.283888,-1.72639 4.974851,-10.27411 5.128803,10.19813 11.308575,1.55649 -8.114112,8.02918 z"
|
||||
inkscape:transform-center-x="0.094945927"
|
||||
inkscape:transform-center-y="-3.9764964"
|
||||
transform="matrix(2.3768784,0,0,2.4799382,-15.920285,-1400.1716)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
363
cockatrice/resources/usericons/pawn_vip_single.svg
Normal file
@@ -0,0 +1,363 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="100"
|
||||
height="100"
|
||||
id="svg5322"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="admin_vip.svg">
|
||||
<defs
|
||||
id="defs3">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient3419">
|
||||
<stop
|
||||
style="stop-color:#ffec79;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop3421" />
|
||||
<stop
|
||||
style="stop-color:#f2c15b;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3423" />
|
||||
</linearGradient>
|
||||
<inkscape:perspective
|
||||
sodipodi:type="inkscape:persp3d"
|
||||
inkscape:vp_x="0 : 526.18109 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_z="744.09448 : 526.18109 : 1"
|
||||
inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
|
||||
id="perspective5328" />
|
||||
<inkscape:perspective
|
||||
id="perspective5305"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<linearGradient
|
||||
id="linearGradient5181">
|
||||
<stop
|
||||
style="stop-color:#0fbb00;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5183" />
|
||||
<stop
|
||||
style="stop-color:#064400;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5185" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600-2"
|
||||
id="radialGradient3606-7"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
|
||||
<linearGradient
|
||||
id="linearGradient3600-2">
|
||||
<stop
|
||||
style="stop-color:#ffc33d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602-4" />
|
||||
<stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604-9" />
|
||||
</linearGradient>
|
||||
<inkscape:perspective
|
||||
id="perspective5478"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<linearGradient
|
||||
id="linearGradient5189">
|
||||
<stop
|
||||
style="stop-color:#000ec9;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5191" />
|
||||
<stop
|
||||
style="stop-color:#000657;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5193" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600-4"
|
||||
id="radialGradient3606-1"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
|
||||
<linearGradient
|
||||
id="linearGradient3600-4">
|
||||
<stop
|
||||
style="stop-color:#ffc33d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602-3" />
|
||||
<stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604-5" />
|
||||
</linearGradient>
|
||||
<inkscape:perspective
|
||||
id="perspective5559"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient5173"
|
||||
id="linearGradient5179"
|
||||
x1="167.33386"
|
||||
y1="178.83276"
|
||||
x2="244.78181"
|
||||
y2="178.83276"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
id="linearGradient5173">
|
||||
<stop
|
||||
style="stop-color:#f50000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5175" />
|
||||
<stop
|
||||
style="stop-color:#950000;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5177" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600"
|
||||
id="radialGradient5169"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276" />
|
||||
<linearGradient
|
||||
id="linearGradient3600">
|
||||
<stop
|
||||
style="stop-color:#ffc13d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602" />
|
||||
<stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
r="25.501276"
|
||||
fy="131.40274"
|
||||
fx="324.32715"
|
||||
cy="131.40274"
|
||||
cx="324.32715"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="radialGradient5574"
|
||||
xlink:href="#linearGradient3600"
|
||||
inkscape:collect="always" />
|
||||
<inkscape:perspective
|
||||
id="perspective5663"
|
||||
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
|
||||
inkscape:vp_z="1 : 0.5 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_x="0 : 0.5 : 1"
|
||||
sodipodi:type="inkscape:persp3d" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600-7"
|
||||
id="radialGradient3606-8"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
|
||||
<linearGradient
|
||||
id="linearGradient3600-7">
|
||||
<stop
|
||||
style="stop-color:#ffc13d;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3602-7" />
|
||||
<stop
|
||||
style="stop-color:#e09900;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3604-6" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3600-7"
|
||||
id="radialGradient5254"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
|
||||
cx="324.32715"
|
||||
cy="131.40274"
|
||||
fx="324.32715"
|
||||
fy="131.40274"
|
||||
r="25.501276" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient5189-1"
|
||||
id="linearGradient5394"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="385.03503"
|
||||
y1="180.09546"
|
||||
x2="462.48297"
|
||||
y2="180.09546"
|
||||
gradientTransform="matrix(0.96839241,0,0,0.96839241,-360.365,847.52359)" />
|
||||
<linearGradient
|
||||
id="linearGradient5189-1">
|
||||
<stop
|
||||
style="stop-color:#000ec9;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5191-0" />
|
||||
<stop
|
||||
style="stop-color:#000657;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5193-4" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient5173-1"
|
||||
id="linearGradient5581"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="167.33386"
|
||||
y1="178.83276"
|
||||
x2="244.78181"
|
||||
y2="178.83276"
|
||||
gradientTransform="matrix(0.96839241,0,0,0.96839241,-149.54484,848.74636)" />
|
||||
<linearGradient
|
||||
id="linearGradient5173-1">
|
||||
<stop
|
||||
style="stop-color:#f50000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5175-5" />
|
||||
<stop
|
||||
style="stop-color:#950000;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5177-3" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient5181-9"
|
||||
id="linearGradient5782"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="282.50455"
|
||||
y1="181.61069"
|
||||
x2="359.95248"
|
||||
y2="181.61069"
|
||||
gradientTransform="matrix(0.96839241,0,0,0.96839241,-261.07526,846.05625)" />
|
||||
<linearGradient
|
||||
id="linearGradient5181-9">
|
||||
<stop
|
||||
style="stop-color:#80d600;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5183-3" />
|
||||
<stop
|
||||
style="stop-color:#80d600;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop5185-0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
y2="181.61069"
|
||||
x2="359.95248"
|
||||
y1="181.61069"
|
||||
x1="282.50455"
|
||||
gradientTransform="matrix(0.96839241,0,0,0.96839241,-175.71812,893.2775)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="linearGradient5799"
|
||||
xlink:href="#linearGradient5181-9"
|
||||
inkscape:collect="always" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3419"
|
||||
id="linearGradient3425"
|
||||
x1="-126.90256"
|
||||
y1="941.52618"
|
||||
x2="-125.73831"
|
||||
y2="984.13751"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(154.2532,0.90556908)" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
inkscape:document-units="mm"
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="4.2946338"
|
||||
inkscape:cx="0.39222903"
|
||||
inkscape:cy="65.175571"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1440"
|
||||
inkscape:window-height="792"
|
||||
inkscape:window-x="11"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata4">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-952.36218)">
|
||||
<path
|
||||
style="fill-opacity:1;stroke:black;stroke-width:2.78220296;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
d="M 49.84375 1.71875 C 36.719738 1.71875 26.0625 12.375988 26.0625 25.5 C 26.0625 32.977454 29.538325 39.612734 34.9375 43.96875 C 24.439951 49.943698 17.919149 62.196126 14.3125 75.65625 C 9.0380874 95.34065 30.224013 98.21875 49.84375 98.21875 C 69.463486 98.21875 90.549327 94.96715 85.375 75.65625 C 81.693381 61.916246 75.224585 49.827177 64.8125 43.9375 C 70.181573 39.580662 73.59375 32.953205 73.59375 25.5 C 73.59375 12.375988 62.967762 1.71875 49.84375 1.71875 z "
|
||||
transform="translate(0,952.36218)"
|
||||
id="left" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:url(#linearGradient3425);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="star"
|
||||
sodipodi:sides="5"
|
||||
sodipodi:cx="27.80283"
|
||||
sodipodi:cy="970.9433"
|
||||
sodipodi:r1="17.189852"
|
||||
sodipodi:r2="8.5949249"
|
||||
sodipodi:arg1="0.9349579"
|
||||
sodipodi:arg2="1.5632764"
|
||||
inkscape:flatsided="false"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 38.011063,984.77381 -10.143601,-5.23583 -10.063711,5.38779 1.845023,-11.2651 -8.233948,-7.90624 11.283888,-1.72639 4.974851,-10.27411 5.128803,10.19813 11.308575,1.55649 -8.114112,8.02918 z"
|
||||
inkscape:transform-center-x="0.094945927"
|
||||
inkscape:transform-center-y="-3.9764964"
|
||||
transform="matrix(2.3768784,0,0,2.4799382,-16.393468,-1400.3733)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
131
cockatrice/resources/usericons/star_double.svg
Normal file
@@ -0,0 +1,131 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
viewBox="0 0 64 64"
|
||||
enable-background="new 0 0 64 64"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.5 r10040"
|
||||
width="100%"
|
||||
height="100%"
|
||||
sodipodi:docname="moderator_buddy.svg">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs10">
|
||||
<linearGradient
|
||||
id="linearGradient5225">
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5227" />
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop5229" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient5219"
|
||||
osb:paint="solid">
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop5221" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3758">
|
||||
<stop
|
||||
id="stop3760"
|
||||
offset="0"
|
||||
style="stop-color:#0fbb00;stop-opacity:1;" />
|
||||
<stop
|
||||
id="stop3762"
|
||||
offset="1"
|
||||
style="stop-color:#064400;stop-opacity:1;" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3750">
|
||||
<stop
|
||||
style="stop-color:#ece400;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3752" />
|
||||
<stop
|
||||
style="stop-color:#ece400;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop3754" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3758-7"
|
||||
id="linearGradient3756-1"
|
||||
x1="1.960216"
|
||||
y1="31.261461"
|
||||
x2="60.456024"
|
||||
y2="31.261461"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
id="linearGradient3758-7">
|
||||
<stop
|
||||
id="stop3760-4"
|
||||
offset="0"
|
||||
style="stop-color:#ece400;stop-opacity:1;" />
|
||||
<stop
|
||||
id="stop3762-0"
|
||||
offset="1"
|
||||
style="stop-color:#ec8b00;stop-opacity:1;" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1028"
|
||||
id="namedview8"
|
||||
showgrid="false"
|
||||
inkscape:zoom="5.2149125"
|
||||
inkscape:cx="-26.445493"
|
||||
inkscape:cy="31.598459"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" />
|
||||
<path
|
||||
d="M 61.442826,23.621762 38.532402,23.515555 31.556101,0.331013 24.578789,23.515555 1.6673502,23.621765 20.267785,38.422001 10.863888,63.668987 31.556101,47.626631 52.258426,63.668987 42.843404,38.422001 z"
|
||||
id="outline"
|
||||
style="fill:#000000;fill-opacity:1"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="M 55.041981,25.814432 36.921945,25.730432 31.404334,7.3935963 25.885923,25.730432 7.7650846,25.814434 22.476316,37.520057 15.0387,57.488097 31.404334,44.800071 47.777965,57.488097 40.331551,37.520057 z"
|
||||
id="left"
|
||||
style="fill-opacity:1;fill-rule:nonzero"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="M 56.276895,25.211993 37.3433,24.856806 31.486705,5.7742084 c 0.04705,37.4359336 -0.01851,2.6744908 -0.0678,40.1841446 L 48.19932,58.580578 40.956295,37.527792 z"
|
||||
id="right"
|
||||
style="fill-opacity:1;fill-rule:nonzero"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccc" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
115
cockatrice/resources/usericons/star_single.svg
Normal file
@@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
viewBox="0 0 64 64"
|
||||
enable-background="new 0 0 64 64"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.5 r10040"
|
||||
width="100%"
|
||||
height="100%"
|
||||
sodipodi:docname="registered_buddy.svg">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs10">
|
||||
<linearGradient
|
||||
id="linearGradient3758">
|
||||
<stop
|
||||
id="stop3760"
|
||||
offset="0"
|
||||
style="stop-color:#80d600;stop-opacity:1;" />
|
||||
<stop
|
||||
id="stop3762"
|
||||
offset="1"
|
||||
style="stop-color:#80d600;stop-opacity:1;" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3750">
|
||||
<stop
|
||||
style="stop-color:#ece400;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3752" />
|
||||
<stop
|
||||
style="stop-color:#ece400;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop3754" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3758"
|
||||
id="linearGradient3756"
|
||||
x1="1.960216"
|
||||
y1="31.261461"
|
||||
x2="60.456024"
|
||||
y2="31.261461"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.87626222,0,0,0.87626222,4.174756,4.8555263)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3758-7"
|
||||
id="linearGradient3756-1"
|
||||
x1="1.960216"
|
||||
y1="31.261461"
|
||||
x2="60.456024"
|
||||
y2="31.261461"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
id="linearGradient3758-7">
|
||||
<stop
|
||||
id="stop3760-4"
|
||||
offset="0"
|
||||
style="stop-color:#ece400;stop-opacity:1;" />
|
||||
<stop
|
||||
id="stop3762-0"
|
||||
offset="1"
|
||||
style="stop-color:#ec8b00;stop-opacity:1;" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1600"
|
||||
inkscape:window-height="1178"
|
||||
id="namedview8"
|
||||
showgrid="false"
|
||||
inkscape:zoom="7.375"
|
||||
inkscape:cx="-11.596718"
|
||||
inkscape:cy="34.975297"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" />
|
||||
<path
|
||||
d="M 61.442826,23.621762 38.532402,23.515555 31.556101,0.331013 24.578789,23.515555 1.6673502,23.621765 20.267785,38.422001 10.863888,63.668987 31.556101,47.626631 52.258426,63.668987 42.843404,38.422001 z"
|
||||
id="path4-9"
|
||||
style="fill:#000000;fill-opacity:1"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="M 57.150089,25.064396 37.504323,24.973324 31.522122,5.092503 25.539054,24.973324 5.8924192,25.064399 21.842354,37.75565 13.778482,59.405024 31.522122,45.64865 49.274434,59.405024 41.201022,37.75565 z"
|
||||
id="left"
|
||||
style="fill-opacity:1"
|
||||
inkscape:connector-curvature="0" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -52,7 +52,7 @@ AbstractClient::AbstractClient(QObject *parent)
|
||||
FeatureSet features;
|
||||
features.initalizeFeatureList(clientFeatures);
|
||||
|
||||
connect(this, SIGNAL(sigQueuePendingCommand(PendingCommand *)), this, SLOT(queuePendingCommand(PendingCommand *)));
|
||||
connect(this, &AbstractClient::sigQueuePendingCommand, this, &AbstractClient::queuePendingCommand);
|
||||
}
|
||||
|
||||
AbstractClient::~AbstractClient()
|
||||
|
||||
@@ -48,6 +48,7 @@ class AbstractClient : public QObject
|
||||
Q_OBJECT
|
||||
signals:
|
||||
void statusChanged(ClientStatus _status);
|
||||
void maxPingTime(int seconds, int maxSeconds);
|
||||
|
||||
// Room events
|
||||
void roomEventReceived(const RoomEvent &event);
|
||||
@@ -97,8 +98,8 @@ protected:
|
||||
virtual void sendCommandContainer(const CommandContainer &cont) = 0;
|
||||
|
||||
public:
|
||||
AbstractClient(QObject *parent = nullptr);
|
||||
~AbstractClient();
|
||||
explicit AbstractClient(QObject *parent = nullptr);
|
||||
~AbstractClient() override;
|
||||
|
||||
ClientStatus getStatus() const
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ signals:
|
||||
void onCtrlC();
|
||||
|
||||
protected:
|
||||
virtual bool eventFilter(QObject *, QEvent *event);
|
||||
bool eventFilter(QObject *, QEvent *event) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
35
cockatrice/src/client/network/client_update_checker.cpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#include "client_update_checker.h"
|
||||
|
||||
#include "../../settings/cache_settings.h"
|
||||
#include "release_channel.h"
|
||||
|
||||
ClientUpdateChecker::ClientUpdateChecker(QObject *parent) : QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void ClientUpdateChecker::check()
|
||||
{
|
||||
auto releaseChannel = SettingsCache::instance().getUpdateReleaseChannel();
|
||||
|
||||
finishedCheckConnection =
|
||||
connect(releaseChannel, &ReleaseChannel::finishedCheck, this, &ClientUpdateChecker::actFinishedCheck);
|
||||
errorConnection = connect(releaseChannel, &ReleaseChannel::error, this, &ClientUpdateChecker::actError);
|
||||
|
||||
releaseChannel->checkForUpdates();
|
||||
}
|
||||
|
||||
void ClientUpdateChecker::actFinishedCheck(bool needToUpdate, bool isCompatible, Release *release)
|
||||
{
|
||||
disconnect(finishedCheckConnection);
|
||||
disconnect(errorConnection);
|
||||
|
||||
emit finishedCheck(needToUpdate, isCompatible, release);
|
||||
}
|
||||
|
||||
void ClientUpdateChecker::actError(const QString &errorString)
|
||||
{
|
||||
disconnect(finishedCheckConnection);
|
||||
disconnect(errorConnection);
|
||||
|
||||
emit error(errorString);
|
||||
}
|
||||
45
cockatrice/src/client/network/client_update_checker.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#ifndef CLIENT_UPDATE_CHECKER_H
|
||||
#define CLIENT_UPDATE_CHECKER_H
|
||||
#include <QObject>
|
||||
|
||||
class Release;
|
||||
|
||||
/**
|
||||
* We use a singleton instance of UpdateChannel, which can cause interference and feedback loops when multiple objects
|
||||
* connect to it.
|
||||
*
|
||||
* This class encapsulates the usage of that UpdateChannel to ensure that the check only happens once per connection and
|
||||
* the connection is destroyed after it's been used.
|
||||
*/
|
||||
class ClientUpdateChecker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QMetaObject::Connection finishedCheckConnection;
|
||||
QMetaObject::Connection errorConnection;
|
||||
|
||||
void actFinishedCheck(bool needToUpdate, bool isCompatible, Release *release);
|
||||
void actError(const QString &errorString);
|
||||
|
||||
public:
|
||||
explicit ClientUpdateChecker(QObject *parent = nullptr);
|
||||
/**
|
||||
* Actually performs the check, using the currently selected update channel in the settings.
|
||||
* Any resulting signals will only be sent once.
|
||||
* This method should only be called ONCE per instance.
|
||||
*/
|
||||
void check();
|
||||
|
||||
signals:
|
||||
/**
|
||||
* Forwarded from UpdateChannel::finishedCheck
|
||||
*/
|
||||
void finishedCheck(bool needToUpdate, bool isCompatible, Release *release);
|
||||
|
||||
/**
|
||||
* Forwarded from UpdateChannel::error
|
||||
*/
|
||||
void error(const QString &errorString);
|
||||
};
|
||||
|
||||
#endif // CLIENT_UPDATE_CHECKER_H
|
||||
@@ -21,11 +21,8 @@
|
||||
|
||||
#define GIT_SHORT_HASH_LEN 7
|
||||
|
||||
int ReleaseChannel::sharedIndex = 0;
|
||||
|
||||
ReleaseChannel::ReleaseChannel() : netMan(new QNetworkAccessManager(this)), response(nullptr), lastRelease(nullptr)
|
||||
{
|
||||
index = sharedIndex++;
|
||||
}
|
||||
|
||||
ReleaseChannel::~ReleaseChannel()
|
||||
@@ -36,9 +33,9 @@ ReleaseChannel::~ReleaseChannel()
|
||||
void ReleaseChannel::checkForUpdates()
|
||||
{
|
||||
QString releaseChannelUrl = getReleaseChannelUrl();
|
||||
qDebug() << "Searching for updates on the channel: " << releaseChannelUrl;
|
||||
qCDebug(ReleaseChannelLog) << "Searching for updates on the channel: " << releaseChannelUrl;
|
||||
response = netMan->get(QNetworkRequest(releaseChannelUrl));
|
||||
connect(response, SIGNAL(finished()), this, SLOT(releaseListFinished()));
|
||||
connect(response, &QNetworkReply::finished, this, &ReleaseChannel::releaseListFinished);
|
||||
}
|
||||
|
||||
// Different release channel checking functions for different operating systems
|
||||
@@ -89,7 +86,7 @@ QString StableReleaseChannel::getManualDownloadUrl() const
|
||||
|
||||
QString StableReleaseChannel::getName() const
|
||||
{
|
||||
return tr("Stable Releases");
|
||||
return tr("Default");
|
||||
}
|
||||
|
||||
QString StableReleaseChannel::getReleaseChannelUrl() const
|
||||
@@ -112,7 +109,7 @@ void StableReleaseChannel::releaseListFinished()
|
||||
QVariantMap resultMap = jsonResponse.toVariant().toMap();
|
||||
if (!(resultMap.contains("name") && resultMap.contains("html_url") && resultMap.contains("tag_name") &&
|
||||
resultMap.contains("published_at"))) {
|
||||
qWarning() << "Invalid received from the release update server.";
|
||||
qWarning() << "Invalid received from the release update server:" << resultMap;
|
||||
emit error(tr("Invalid reply received from the release update server."));
|
||||
return;
|
||||
}
|
||||
@@ -148,17 +145,17 @@ void StableReleaseChannel::releaseListFinished()
|
||||
|
||||
QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN);
|
||||
QString myHash = QString(VERSION_COMMIT);
|
||||
qDebug() << "Current hash=" << myHash << "update hash=" << shortHash;
|
||||
qCDebug(ReleaseChannelLog) << "Current hash=" << myHash << "update hash=" << shortHash;
|
||||
|
||||
qDebug() << "Got reply from release server, name=" << lastRelease->getName()
|
||||
<< "desc=" << lastRelease->getDescriptionUrl() << "date=" << lastRelease->getPublishDate()
|
||||
<< "url=" << lastRelease->getDownloadUrl();
|
||||
qCDebug(ReleaseChannelLog) << "Got reply from release server, name=" << lastRelease->getName()
|
||||
<< "desc=" << lastRelease->getDescriptionUrl()
|
||||
<< "date=" << lastRelease->getPublishDate() << "url=" << lastRelease->getDownloadUrl();
|
||||
|
||||
const QString &tagName = resultMap["tag_name"].toString();
|
||||
QString url = QString(STABLETAG_URL) + tagName;
|
||||
qDebug() << "Searching for commit hash corresponding to stable channel tag: " << tagName;
|
||||
qCDebug(ReleaseChannelLog) << "Searching for commit hash corresponding to stable channel tag: " << tagName;
|
||||
response = netMan->get(QNetworkRequest(url));
|
||||
connect(response, SIGNAL(finished()), this, SLOT(tagListFinished()));
|
||||
connect(response, &QNetworkReply::finished, this, &StableReleaseChannel::tagListFinished);
|
||||
}
|
||||
|
||||
void StableReleaseChannel::tagListFinished()
|
||||
@@ -181,11 +178,11 @@ void StableReleaseChannel::tagListFinished()
|
||||
}
|
||||
|
||||
lastRelease->setCommitHash(resultMap["object"].toMap()["sha"].toString());
|
||||
qDebug() << "Got reply from tag server, commit=" << lastRelease->getCommitHash();
|
||||
qCDebug(ReleaseChannelLog) << "Got reply from tag server, commit=" << lastRelease->getCommitHash();
|
||||
|
||||
QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN);
|
||||
QString myHash = QString(VERSION_COMMIT);
|
||||
qDebug() << "Current hash=" << myHash << "update hash=" << shortHash;
|
||||
qCDebug(ReleaseChannelLog) << "Current hash=" << myHash << "update hash=" << shortHash;
|
||||
const bool needToUpdate = (QString::compare(shortHash, myHash, Qt::CaseInsensitive) != 0);
|
||||
|
||||
emit finishedCheck(needToUpdate, lastRelease->isCompatibleVersionFound(), lastRelease);
|
||||
@@ -203,7 +200,7 @@ QString BetaReleaseChannel::getManualDownloadUrl() const
|
||||
|
||||
QString BetaReleaseChannel::getName() const
|
||||
{
|
||||
return tr("Beta Releases");
|
||||
return tr("Beta");
|
||||
}
|
||||
|
||||
QString BetaReleaseChannel::getReleaseChannelUrl() const
|
||||
@@ -252,15 +249,15 @@ void BetaReleaseChannel::releaseListFinished()
|
||||
lastRelease->setName(QString("%1 (%2)").arg(resultMap["tag_name"].toString()).arg(shortHash));
|
||||
lastRelease->setDescriptionUrl(QString(BETARELEASE_CHANGESURL).arg(VERSION_COMMIT, shortHash));
|
||||
|
||||
qDebug() << "Got reply from release server, size=" << resultMap.size() << "name=" << lastRelease->getName()
|
||||
<< "desc=" << lastRelease->getDescriptionUrl() << "commit=" << lastRelease->getCommitHash()
|
||||
<< "date=" << lastRelease->getPublishDate();
|
||||
qCDebug(ReleaseChannelLog) << "Got reply from release server, size=" << resultMap.size()
|
||||
<< "name=" << lastRelease->getName() << "desc=" << lastRelease->getDescriptionUrl()
|
||||
<< "commit=" << lastRelease->getCommitHash() << "date=" << lastRelease->getPublishDate();
|
||||
|
||||
QString betaBuildDownloadUrl = resultMap["assets_url"].toString();
|
||||
|
||||
qDebug() << "Searching for a corresponding file on the beta channel: " << betaBuildDownloadUrl;
|
||||
qCDebug(ReleaseChannelLog) << "Searching for a corresponding file on the beta channel: " << betaBuildDownloadUrl;
|
||||
response = netMan->get(QNetworkRequest(betaBuildDownloadUrl));
|
||||
connect(response, SIGNAL(finished()), this, SLOT(fileListFinished()));
|
||||
connect(response, &QNetworkReply::finished, this, &BetaReleaseChannel::fileListFinished);
|
||||
}
|
||||
|
||||
void BetaReleaseChannel::fileListFinished()
|
||||
@@ -278,7 +275,7 @@ void BetaReleaseChannel::fileListFinished()
|
||||
QVariantList resultList = jsonResponse.toVariant().toList();
|
||||
QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN);
|
||||
QString myHash = QString(VERSION_COMMIT);
|
||||
qDebug() << "Current hash=" << myHash << "update hash=" << shortHash;
|
||||
qCDebug(ReleaseChannelLog) << "Current hash=" << myHash << "update hash=" << shortHash;
|
||||
|
||||
bool needToUpdate = (QString::compare(shortHash, myHash, Qt::CaseInsensitive) != 0);
|
||||
bool compatibleVersion = false;
|
||||
@@ -295,7 +292,7 @@ void BetaReleaseChannel::fileListFinished()
|
||||
if (downloadMatchesCurrentOS(*url)) {
|
||||
compatibleVersion = true;
|
||||
lastRelease->setDownloadUrl(*url);
|
||||
qDebug() << "Found compatible version url=" << *url;
|
||||
qCDebug(ReleaseChannelLog) << "Found compatible version url=" << *url;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
#define RELEASECHANNEL_H
|
||||
|
||||
#include <QDate>
|
||||
#include <QLoggingCategory>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVariantMap>
|
||||
#include <utility>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(ReleaseChannelLog, "release_channel");
|
||||
|
||||
class QNetworkReply;
|
||||
class QNetworkAccessManager;
|
||||
|
||||
@@ -82,9 +85,6 @@ public:
|
||||
~ReleaseChannel() override;
|
||||
|
||||
protected:
|
||||
// shared by all instances
|
||||
static int sharedIndex;
|
||||
int index;
|
||||
QNetworkAccessManager *netMan;
|
||||
QNetworkReply *response;
|
||||
Release *lastRelease;
|
||||
@@ -94,10 +94,6 @@ protected:
|
||||
virtual QString getReleaseChannelUrl() const = 0;
|
||||
|
||||
public:
|
||||
int getIndex() const
|
||||
{
|
||||
return index;
|
||||
}
|
||||
Release *getLastRelease()
|
||||
{
|
||||
return lastRelease;
|
||||
|
||||
@@ -10,7 +10,7 @@ ReplayTimelineWidget::ReplayTimelineWidget(QWidget *parent)
|
||||
currentEvent(0)
|
||||
{
|
||||
replayTimer = new QTimer(this);
|
||||
connect(replayTimer, SIGNAL(timeout()), this, SLOT(replayTimerTimeout()));
|
||||
connect(replayTimer, &QTimer::timeout, this, &ReplayTimelineWidget::replayTimerTimeout);
|
||||
|
||||
rewindBufferingTimer = new QTimer(this);
|
||||
rewindBufferingTimer->setSingleShot(true);
|
||||
|
||||
@@ -57,28 +57,29 @@ public:
|
||||
SortRole = Qt::UserRole
|
||||
};
|
||||
|
||||
SetsModel(CardDatabase *_db, QObject *parent = nullptr);
|
||||
~SetsModel();
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const;
|
||||
int columnCount(const QModelIndex &parent = QModelIndex()) const
|
||||
explicit SetsModel(CardDatabase *_db, QObject *parent = nullptr);
|
||||
~SetsModel() override;
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return NUM_COLS;
|
||||
}
|
||||
QVariant data(const QModelIndex &index, int role) const;
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role);
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
|
||||
Qt::ItemFlags flags(const QModelIndex &index) const;
|
||||
Qt::DropActions supportedDropActions() const;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||
Qt::ItemFlags flags(const QModelIndex &index) const override;
|
||||
Qt::DropActions supportedDropActions() const override;
|
||||
|
||||
QMimeData *mimeData(const QModelIndexList &indexes) const;
|
||||
bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent);
|
||||
QStringList mimeTypes() const;
|
||||
QMimeData *mimeData(const QModelIndexList &indexes) const override;
|
||||
bool
|
||||
dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
|
||||
QStringList mimeTypes() const override;
|
||||
void swapRows(int oldRow, int newRow);
|
||||
void toggleRow(int row, bool enable);
|
||||
void toggleRow(int row);
|
||||
void toggleAll(bool);
|
||||
void sort(int column, Qt::SortOrder order = Qt::AscendingOrder);
|
||||
void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override;
|
||||
void save(CardDatabase *db);
|
||||
void restore(CardDatabase *db);
|
||||
void restoreOriginalOrder();
|
||||
@@ -88,7 +89,7 @@ class SetsDisplayModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
SetsDisplayModel(QObject *parent = NULL);
|
||||
explicit SetsDisplayModel(QObject *parent = nullptr);
|
||||
|
||||
protected:
|
||||
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
|
||||
|
||||
@@ -28,7 +28,7 @@ SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *apParent) : QObject(
|
||||
// File exists means we're in spoiler season
|
||||
startSpoilerDownloadProcess(SPOILERS_STATUS_URL, false);
|
||||
} else {
|
||||
qDebug() << "Spoilers Disabled";
|
||||
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoilers Disabled";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +45,10 @@ void SpoilerBackgroundUpdater::downloadFromURL(QUrl url, bool saveResults)
|
||||
|
||||
if (saveResults) {
|
||||
// This will write out to the file (used for spoiler.xml)
|
||||
connect(reply, SIGNAL(finished()), this, SLOT(actDownloadFinishedSpoilersFile()));
|
||||
connect(reply, &QNetworkReply::finished, this, &SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile);
|
||||
} else {
|
||||
// This will check the status (used to see if we're in spoiler season or not)
|
||||
connect(reply, SIGNAL(finished()), this, SLOT(actCheckIfSpoilerSeasonEnabled()));
|
||||
connect(reply, &QNetworkReply::finished, this, &SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ void SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile()
|
||||
reply->deleteLater();
|
||||
emit spoilerCheckerDone();
|
||||
} else {
|
||||
qDebug() << "Error downloading spoilers file" << errorCode;
|
||||
qCDebug(SpoilerBackgroundUpdaterLog) << "Error downloading spoilers file" << errorCode;
|
||||
emit spoilerCheckerDone();
|
||||
}
|
||||
}
|
||||
@@ -81,11 +81,11 @@ bool SpoilerBackgroundUpdater::deleteSpoilerFile()
|
||||
|
||||
// Delete the spoiler.xml file
|
||||
if (file.exists() && file.remove()) {
|
||||
qDebug() << "Deleting spoiler.xml";
|
||||
qCDebug(SpoilerBackgroundUpdaterLog) << "Deleting spoiler.xml";
|
||||
return true;
|
||||
}
|
||||
|
||||
qDebug() << "Error: Spoiler.xml not found or not deleted";
|
||||
qCDebug(SpoilerBackgroundUpdaterLog) << "Error: Spoiler.xml not found or not deleted";
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -101,24 +101,24 @@ void SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled()
|
||||
trayIcon->showMessage(tr("Spoilers season has ended"), tr("Deleting spoiler.xml. Please run Oracle"));
|
||||
}
|
||||
|
||||
qDebug() << "Spoiler Season Offline";
|
||||
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoiler Season Offline";
|
||||
emit spoilerCheckerDone();
|
||||
} else if (errorCode == QNetworkReply::NoError) {
|
||||
qDebug() << "Spoiler Service Online";
|
||||
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoiler Service Online";
|
||||
startSpoilerDownloadProcess(SPOILERS_URL, true);
|
||||
} else if (errorCode == QNetworkReply::HostNotFoundError) {
|
||||
if (trayIcon) {
|
||||
trayIcon->showMessage(tr("Spoilers download failed"), tr("No internet connection"));
|
||||
}
|
||||
|
||||
qDebug() << "Spoiler download failed due to no internet connection";
|
||||
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoiler download failed due to no internet connection";
|
||||
emit spoilerCheckerDone();
|
||||
} else {
|
||||
if (trayIcon) {
|
||||
trayIcon->showMessage(tr("Spoilers download failed"), tr("Error") + " " + (short)errorCode);
|
||||
}
|
||||
|
||||
qDebug() << "Spoiler download failed with reason" << errorCode;
|
||||
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoiler download failed with reason" << errorCode;
|
||||
emit spoilerCheckerDone();
|
||||
}
|
||||
}
|
||||
@@ -139,19 +139,19 @@ bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
|
||||
trayIcon->showMessage(tr("Spoilers already up to date"), tr("No new spoilers added"));
|
||||
}
|
||||
|
||||
qDebug() << "Spoilers Up to Date";
|
||||
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoilers Up to Date";
|
||||
return false;
|
||||
}
|
||||
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
qDebug() << "Spoiler Service Error: File open (w) failed for" << fileName;
|
||||
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoiler Service Error: File open (w) failed for" << fileName;
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file.write(data) == -1) {
|
||||
qDebug() << "Spoiler Service Error: File write (w) failed for" << fileName;
|
||||
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoiler Service Error: File write (w) failed for" << fileName;
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
@@ -159,7 +159,7 @@ bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
|
||||
file.close();
|
||||
|
||||
// Data written, so reload the card database
|
||||
qDebug() << "Spoiler Service Data Written";
|
||||
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoiler Service Data Written";
|
||||
const auto reloadOk = QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); });
|
||||
|
||||
// If the user has notifications enabled, let them know
|
||||
@@ -167,7 +167,7 @@ bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
|
||||
if (trayIcon) {
|
||||
QList<QByteArray> lines = data.split('\n');
|
||||
|
||||
foreach (QByteArray line, lines) {
|
||||
for (const QByteArray &line : lines) {
|
||||
if (line.contains("Created At:")) {
|
||||
QString timeStamp = QString(line).replace("Created At:", "").trimmed();
|
||||
timeStamp.chop(6); // Remove " (UTC)"
|
||||
@@ -202,12 +202,12 @@ QByteArray SpoilerBackgroundUpdater::getHash(const QString fileName)
|
||||
QCryptographicHash hash(QCryptographicHash::Algorithm::Md5);
|
||||
hash.addData(bytes);
|
||||
|
||||
qDebug() << "File Hash =" << hash.result();
|
||||
qCDebug(SpoilerBackgroundUpdaterLog) << "File Hash =" << hash.result();
|
||||
|
||||
file.close();
|
||||
return hash.result();
|
||||
} else {
|
||||
qDebug() << "getHash ReadOnly failed!";
|
||||
qCDebug(SpoilerBackgroundUpdaterLog) << "getHash ReadOnly failed!";
|
||||
file.close();
|
||||
return QByteArray();
|
||||
}
|
||||
@@ -221,7 +221,7 @@ QByteArray SpoilerBackgroundUpdater::getHash(QByteArray data)
|
||||
QCryptographicHash hash(QCryptographicHash::Algorithm::Md5);
|
||||
hash.addData(bytes);
|
||||
|
||||
qDebug() << "Data Hash =" << hash.result();
|
||||
qCDebug(SpoilerBackgroundUpdaterLog) << "Data Hash =" << hash.result();
|
||||
|
||||
return hash.result();
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
#define COCKATRICE_SPOILER_DOWNLOADER_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QLoggingCategory>
|
||||
#include <QObject>
|
||||
#include <QProcess>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(SpoilerBackgroundUpdaterLog, "spoiler_background_updater");
|
||||
|
||||
class SpoilerBackgroundUpdater : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
#define DEFAULT_THEME_NAME "Default"
|
||||
#define TEST_SOUND_FILENAME "player_join"
|
||||
|
||||
SoundEngine::SoundEngine(QObject *parent) : QObject(parent), player(nullptr)
|
||||
SoundEngine::SoundEngine(QObject *parent) : QObject(parent), audioOutput(nullptr), player(nullptr)
|
||||
{
|
||||
ensureThemeDirectoryExists();
|
||||
connect(&SettingsCache::instance(), SIGNAL(soundThemeChanged()), this, SLOT(themeChangedSlot()));
|
||||
connect(&SettingsCache::instance(), SIGNAL(soundEnabledChanged()), this, SLOT(soundEnabledChanged()));
|
||||
connect(&SettingsCache::instance(), &SettingsCache::soundThemeChanged, this, &SoundEngine::themeChangedSlot);
|
||||
connect(&SettingsCache::instance(), &SettingsCache::soundEnabledChanged, this, &SoundEngine::soundEnabledChanged);
|
||||
|
||||
soundEnabledChanged();
|
||||
themeChangedSlot();
|
||||
@@ -28,26 +28,34 @@ SoundEngine::~SoundEngine()
|
||||
player->deleteLater();
|
||||
player = nullptr;
|
||||
}
|
||||
if (audioOutput) {
|
||||
audioOutput->deleteLater();
|
||||
audioOutput = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void SoundEngine::soundEnabledChanged()
|
||||
{
|
||||
if (SettingsCache::instance().getSoundEnabled()) {
|
||||
qDebug() << "SoundEngine: enabling sound with" << audioData.size() << "sounds";
|
||||
qCDebug(SoundEngineLog) << "SoundEngine: enabling sound with" << audioData.size() << "sounds";
|
||||
if (!player) {
|
||||
player = new QMediaPlayer;
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
auto qAudioOutput = new QAudioOutput;
|
||||
player->setAudioOutput(qAudioOutput);
|
||||
audioOutput = new QAudioOutput(player);
|
||||
player->setAudioOutput(audioOutput);
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
qDebug() << "SoundEngine: disabling sound";
|
||||
qCDebug(SoundEngineLog) << "SoundEngine: disabling sound";
|
||||
if (player) {
|
||||
player->stop();
|
||||
player->deleteLater();
|
||||
player = nullptr;
|
||||
}
|
||||
if (audioOutput) {
|
||||
audioOutput->deleteLater();
|
||||
audioOutput = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +90,7 @@ void SoundEngine::ensureThemeDirectoryExists()
|
||||
{
|
||||
if (SettingsCache::instance().getSoundThemeName().isEmpty() ||
|
||||
!getAvailableThemes().contains(SettingsCache::instance().getSoundThemeName())) {
|
||||
qDebug() << "Sounds theme name not set, setting default value";
|
||||
qCDebug(SoundEngineLog) << "Sounds theme name not set, setting default value";
|
||||
SettingsCache::instance().setSoundThemeName(DEFAULT_THEME_NAME);
|
||||
}
|
||||
}
|
||||
@@ -123,7 +131,7 @@ QStringMap &SoundEngine::getAvailableThemes()
|
||||
void SoundEngine::themeChangedSlot()
|
||||
{
|
||||
QString themeName = SettingsCache::instance().getSoundThemeName();
|
||||
qDebug() << "Sound theme changed:" << themeName;
|
||||
qCDebug(SoundEngineLog) << "Sound theme changed:" << themeName;
|
||||
|
||||
QDir dir = getAvailableThemes().value(themeName);
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
#ifndef SOUNDENGINE_H
|
||||
#define SOUNDENGINE_H
|
||||
|
||||
#include <QAudioOutput>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMap>
|
||||
#include <QMediaPlayer>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
class QAudioOutput;
|
||||
inline Q_LOGGING_CATEGORY(SoundEngineLog, "sound_engine");
|
||||
|
||||
class QBuffer;
|
||||
|
||||
typedef QMap<QString, QString> QStringMap;
|
||||
@@ -23,6 +26,7 @@ public:
|
||||
private:
|
||||
QStringMap availableThemes;
|
||||
QMap<QString, QString> audioData;
|
||||
QAudioOutput *audioOutput;
|
||||
QMediaPlayer *player;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "edhrec_commander_api_response.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonArray>
|
||||
|
||||
void EdhrecCommanderApiResponse::fromJson(const QJsonObject &json)
|
||||
{
|
||||
// Parse the collapsed DeckStatistics
|
||||
deckStats.fromJson(json);
|
||||
|
||||
// Parse Archidekt section
|
||||
QJsonArray archidektJson = json.value("archidekt").toArray();
|
||||
archidekt.fromJson(archidektJson);
|
||||
|
||||
// Parse other fields
|
||||
similar = json.value("similar").toObject();
|
||||
header = json.value("header").toString();
|
||||
panels = json.value("panels").toObject();
|
||||
description = json.value("description").toString();
|
||||
QJsonObject containerJson = json.value("container").toObject();
|
||||
container.fromJson(containerJson);
|
||||
}
|
||||
|
||||
void EdhrecCommanderApiResponse::debugPrint() const
|
||||
{
|
||||
qDebug() << "Deck Statistics:";
|
||||
qDebug() << " Creature:" << deckStats.creature;
|
||||
qDebug() << " Instant:" << deckStats.instant;
|
||||
qDebug() << " Sorcery:" << deckStats.sorcery;
|
||||
qDebug() << " Artifact:" << deckStats.artifact;
|
||||
qDebug() << " Enchantment:" << deckStats.enchantment;
|
||||
qDebug() << " Battle:" << deckStats.battle;
|
||||
qDebug() << " Planeswalker:" << deckStats.planeswalker;
|
||||
qDebug() << " Land:" << deckStats.land;
|
||||
qDebug() << " Basic:" << deckStats.basic;
|
||||
qDebug() << " Nonbasic:" << deckStats.nonbasic;
|
||||
|
||||
archidekt.debugPrint();
|
||||
|
||||
qDebug() << "Similar:" << similar;
|
||||
qDebug() << "Header:" << header;
|
||||
qDebug() << "Panels:" << panels;
|
||||
qDebug() << "Description:" << description;
|
||||
container.debugPrint();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef DECKDATA_H
|
||||
#define DECKDATA_H
|
||||
|
||||
#include "edhrec_commander_api_response_archidekt_links.h"
|
||||
#include "edhrec_commander_api_response_average_deck_statistics.h"
|
||||
#include "edhrec_commander_api_response_card_container.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
// Represents the main structure of the JSON
|
||||
class EdhrecCommanderApiResponse
|
||||
{
|
||||
public:
|
||||
EdhrecCommanderApiResponseAverageDeckStatistics deckStats;
|
||||
EdhrecCommanderApiResponseArchidektLinks archidekt;
|
||||
QJsonObject similar;
|
||||
QString header;
|
||||
QJsonObject panels;
|
||||
QString description;
|
||||
EdhrecCommanderApiResponseCardContainer container;
|
||||
|
||||
void fromJson(const QJsonObject &json);
|
||||
void debugPrint() const;
|
||||
};
|
||||
|
||||
#endif // DECKDATA_H
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "edhrec_commander_api_response_archidekt_links.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
void EdhrecCommanderApiResponseArchidektLink::fromJson(const QJsonObject &json)
|
||||
{
|
||||
c = json.value("c").toString();
|
||||
f = json.value("f").toInt(0);
|
||||
q = json.value("q").toInt(0);
|
||||
u = json.value("u").toString();
|
||||
}
|
||||
|
||||
void EdhrecCommanderApiResponseArchidektLink::debugPrint() const
|
||||
{
|
||||
qDebug() << " C:" << c;
|
||||
qDebug() << " F:" << f;
|
||||
qDebug() << " Q:" << q;
|
||||
qDebug() << " U:" << u;
|
||||
}
|
||||
|
||||
void EdhrecCommanderApiResponseArchidektLinks::fromJson(const QJsonArray &json)
|
||||
{
|
||||
entries.clear();
|
||||
for (const QJsonValue &value : json) {
|
||||
if (value.isObject()) {
|
||||
QJsonObject entryJson = value.toObject();
|
||||
EdhrecCommanderApiResponseArchidektLink entry;
|
||||
entry.fromJson(entryJson);
|
||||
entries.append(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EdhrecCommanderApiResponseArchidektLinks::debugPrint() const
|
||||
{
|
||||
qDebug() << "Archidekt Entries:";
|
||||
for (const auto &entry : entries) {
|
||||
entry.debugPrint();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#ifndef ARCHIDEKTENTRY_H
|
||||
#define ARCHIDEKTENTRY_H
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
// Represents a single Archidekt entry
|
||||
class EdhrecCommanderApiResponseArchidektLink
|
||||
{
|
||||
public:
|
||||
QString c;
|
||||
int f = 0;
|
||||
int q = 0;
|
||||
QString u;
|
||||
|
||||
void fromJson(const QJsonObject &json);
|
||||
void debugPrint() const;
|
||||
};
|
||||
|
||||
// Represents the Archidekt section as a list of entries
|
||||
class EdhrecCommanderApiResponseArchidektLinks
|
||||
{
|
||||
public:
|
||||
QVector<EdhrecCommanderApiResponseArchidektLink> entries;
|
||||
|
||||
void fromJson(const QJsonArray &json);
|
||||
void debugPrint() const;
|
||||
};
|
||||
|
||||
#endif // ARCHIDEKTENTRY_H
|
||||
@@ -0,0 +1,15 @@
|
||||
#include "edhrec_commander_api_response_average_deck_statistics.h"
|
||||
|
||||
void EdhrecCommanderApiResponseAverageDeckStatistics::fromJson(const QJsonObject &json)
|
||||
{
|
||||
creature = json.value("creature").toInt(0);
|
||||
instant = json.value("instant").toInt(0);
|
||||
sorcery = json.value("sorcery").toInt(0);
|
||||
artifact = json.value("artifact").toInt(0);
|
||||
enchantment = json.value("enchantment").toInt(0);
|
||||
battle = json.value("battle").toInt(0);
|
||||
planeswalker = json.value("planeswalker").toInt(0);
|
||||
land = json.value("land").toInt(0);
|
||||
basic = json.value("basic").toInt(0);
|
||||
nonbasic = json.value("nonbasic").toInt(0);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef AVERAGE_DECK_STATISTICS_H
|
||||
#define AVERAGE_DECK_STATISTICS_H
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
// Represents the typical deck statistics (collapsed section)
|
||||
struct EdhrecCommanderApiResponseAverageDeckStatistics
|
||||
{
|
||||
int creature = 0;
|
||||
int instant = 0;
|
||||
int sorcery = 0;
|
||||
int artifact = 0;
|
||||
int enchantment = 0;
|
||||
int battle = 0;
|
||||
int planeswalker = 0;
|
||||
int land = 0;
|
||||
int basic = 0;
|
||||
int nonbasic = 0;
|
||||
|
||||
void fromJson(const QJsonObject &json);
|
||||
};
|
||||
#endif // AVERAGE_DECK_STATISTICS_H
|
||||
@@ -0,0 +1,49 @@
|
||||
#include "edhrec_commander_api_response_card_container.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
void EdhrecCommanderApiResponseCardContainer::fromJson(const QJsonObject &json)
|
||||
{
|
||||
// Parse breadcrumb
|
||||
QJsonArray breadcrumbArray = json.value("breadcrumb").toArray();
|
||||
for (const QJsonValue &breadcrumbValue : breadcrumbArray) {
|
||||
breadcrumb.push_back(breadcrumbValue.toObject());
|
||||
}
|
||||
|
||||
description = json.value("description").toString();
|
||||
QJsonObject jsonDict = json.value("json_dict").toObject();
|
||||
card.fromJson(jsonDict.value("card").toObject());
|
||||
QJsonArray cardlistsArray = jsonDict.value("cardlists").toArray();
|
||||
|
||||
for (const QJsonValue &cardlistValue : cardlistsArray) {
|
||||
QJsonObject cardlistObj = cardlistValue.toObject();
|
||||
QJsonArray cardviewsArray = cardlistObj.value("cardviews").toArray();
|
||||
EdhrecCommanderApiResponseCardList cardView;
|
||||
cardView.fromJson(cardlistValue.toObject());
|
||||
cardlists.push_back(cardView);
|
||||
}
|
||||
|
||||
keywords = json.value("keywords").toString();
|
||||
title = json.value("title").toString();
|
||||
}
|
||||
|
||||
void EdhrecCommanderApiResponseCardContainer::debugPrint() const
|
||||
{
|
||||
qDebug() << "Breadcrumb:";
|
||||
for (const auto &breadcrumbEntry : breadcrumb) {
|
||||
qDebug() << breadcrumbEntry;
|
||||
}
|
||||
|
||||
qDebug() << "Description:" << description;
|
||||
card.debugPrint();
|
||||
|
||||
qDebug() << "Cardlists:";
|
||||
for (const auto &cardlist : cardlists) {
|
||||
cardlist.debugPrint();
|
||||
}
|
||||
|
||||
qDebug() << "Keywords:" << keywords;
|
||||
qDebug() << "Title:" << title;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#ifndef CONTAINER_ENTRY_H
|
||||
#define CONTAINER_ENTRY_H
|
||||
|
||||
#include "edhrec_commander_api_response_card_list.h"
|
||||
#include "edhrec_commander_api_response_commander_details.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
class EdhrecCommanderApiResponseCardContainer
|
||||
{
|
||||
public:
|
||||
// Constructor
|
||||
EdhrecCommanderApiResponseCardContainer() = default;
|
||||
|
||||
// Parse deck-related data from JSON
|
||||
void fromJson(const QJsonObject &json);
|
||||
|
||||
// Debug method for logging
|
||||
void debugPrint() const;
|
||||
|
||||
// Getter methods for deck container
|
||||
const QString &getDescription() const
|
||||
{
|
||||
return description;
|
||||
}
|
||||
const QVector<QJsonObject> &getBreadcrumb() const
|
||||
{
|
||||
return breadcrumb;
|
||||
}
|
||||
const EdhrecCommanderApiResponseCommanderDetails &getCommanderDetails() const
|
||||
{
|
||||
return card;
|
||||
}
|
||||
const QVector<EdhrecCommanderApiResponseCardList> &getCardlists() const
|
||||
{
|
||||
return cardlists;
|
||||
}
|
||||
const QString &getKeywords() const
|
||||
{
|
||||
return keywords;
|
||||
}
|
||||
const QString &getTitle() const
|
||||
{
|
||||
return title;
|
||||
}
|
||||
|
||||
private:
|
||||
QString description;
|
||||
QVector<QJsonObject> breadcrumb;
|
||||
EdhrecCommanderApiResponseCommanderDetails card;
|
||||
QVector<EdhrecCommanderApiResponseCardList> cardlists;
|
||||
QString keywords;
|
||||
QString title;
|
||||
};
|
||||
|
||||
#endif // CONTAINER_ENTRY_H
|
||||
@@ -0,0 +1,36 @@
|
||||
#include "edhrec_commander_api_response_card_details.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
EdhrecCommanderApiResponseCardDetails::EdhrecCommanderApiResponseCardDetails()
|
||||
: synergy(0.0), inclusion(0), numDecks(0), potentialDecks(0)
|
||||
{
|
||||
}
|
||||
|
||||
void EdhrecCommanderApiResponseCardDetails::fromJson(const QJsonObject &json)
|
||||
{
|
||||
// Parse the fields from the JSON object
|
||||
name = json.value("name").toString();
|
||||
sanitized = json.value("sanitized").toString();
|
||||
sanitizedWo = json.value("sanitized_wo").toString();
|
||||
url = json.value("url").toString();
|
||||
synergy = json.value("synergy").toDouble(0.0);
|
||||
inclusion = json.value("inclusion").toInt(0);
|
||||
label = json.value("label").toString();
|
||||
numDecks = json.value("num_decks").toInt(0);
|
||||
potentialDecks = json.value("potential_decks").toInt(0);
|
||||
}
|
||||
|
||||
void EdhrecCommanderApiResponseCardDetails::debugPrint() const
|
||||
{
|
||||
// Print out all the fields for debugging
|
||||
qDebug() << "Name:" << name;
|
||||
qDebug() << "Sanitized:" << sanitized;
|
||||
qDebug() << "Sanitized Wo:" << sanitizedWo;
|
||||
qDebug() << "URL:" << url;
|
||||
qDebug() << "Synergy:" << synergy;
|
||||
qDebug() << "Inclusion:" << inclusion;
|
||||
qDebug() << "Label:" << label;
|
||||
qDebug() << "Num Decks:" << numDecks;
|
||||
qDebug() << "Potential Decks:" << potentialDecks;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#ifndef CARD_VIEW_H
|
||||
#define CARD_VIEW_H
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
class EdhrecCommanderApiResponseCardDetails
|
||||
{
|
||||
public:
|
||||
QString name;
|
||||
QString sanitized;
|
||||
QString sanitizedWo;
|
||||
QString url;
|
||||
double synergy;
|
||||
int inclusion;
|
||||
QString label;
|
||||
int numDecks;
|
||||
int potentialDecks;
|
||||
|
||||
EdhrecCommanderApiResponseCardDetails();
|
||||
|
||||
// Method to populate the object from a JSON object
|
||||
void fromJson(const QJsonObject &json);
|
||||
|
||||
// Debug method to print out the data
|
||||
void debugPrint() const;
|
||||
};
|
||||
|
||||
#endif // CARD_VIEW_H
|
||||
@@ -0,0 +1,33 @@
|
||||
#include "edhrec_commander_api_response_card_list.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
EdhrecCommanderApiResponseCardList::EdhrecCommanderApiResponseCardList()
|
||||
{
|
||||
}
|
||||
|
||||
void EdhrecCommanderApiResponseCardList::fromJson(const QJsonObject &json)
|
||||
{
|
||||
// Parse the header from the JSON object
|
||||
header = json.value("header").toString();
|
||||
|
||||
// Parse the cardviews array and populate cardViews
|
||||
QJsonArray cardviewsArray = json.value("cardviews").toArray();
|
||||
for (const QJsonValue &value : cardviewsArray) {
|
||||
QJsonObject cardviewObj = value.toObject();
|
||||
EdhrecCommanderApiResponseCardDetails cardView;
|
||||
cardView.fromJson(cardviewObj);
|
||||
cardViews.append(cardView);
|
||||
}
|
||||
}
|
||||
|
||||
void EdhrecCommanderApiResponseCardList::debugPrint() const
|
||||
{
|
||||
// Print out the header
|
||||
qDebug() << "Header:" << header;
|
||||
|
||||
// Print out all the CardView objects
|
||||
for (const EdhrecCommanderApiResponseCardDetails &cardView : cardViews) {
|
||||
cardView.debugPrint();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef CARD_LIST_H
|
||||
#define CARD_LIST_H
|
||||
|
||||
#include "edhrec_commander_api_response_card_details.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
class EdhrecCommanderApiResponseCardList
|
||||
{
|
||||
public:
|
||||
QString header;
|
||||
QList<EdhrecCommanderApiResponseCardDetails> cardViews;
|
||||
|
||||
// Default constructor
|
||||
EdhrecCommanderApiResponseCardList();
|
||||
|
||||
// Method to populate the object from a JSON object
|
||||
void fromJson(const QJsonObject &json);
|
||||
|
||||
// Debug method to print out the data
|
||||
void debugPrint() const;
|
||||
};
|
||||
|
||||
#endif // CARD_LIST_H
|
||||
@@ -0,0 +1,31 @@
|
||||
#include "edhrec_commander_api_response_card_prices.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
void CardPrices::fromJson(const QJsonObject &json)
|
||||
{
|
||||
// Parse prices from various sources
|
||||
cardhoarder = json.value("cardhoarder").toObject();
|
||||
cardkingdom = json.value("cardkingdom").toObject();
|
||||
cardmarket = json.value("cardmarket").toObject();
|
||||
face2face = json.value("face2face").toObject();
|
||||
manapool = json.value("manapool").toObject();
|
||||
mtgstocks = json.value("mtgstocks").toObject();
|
||||
scg = json.value("scg").toObject();
|
||||
tcgl = json.value("tcgl").toObject();
|
||||
tcgplayer = json.value("tcgplayer").toObject();
|
||||
}
|
||||
|
||||
void CardPrices::debugPrint() const
|
||||
{
|
||||
qDebug() << "Card Prices:";
|
||||
qDebug() << "Cardhoarder:" << cardhoarder;
|
||||
qDebug() << "Cardkingdom:" << cardkingdom;
|
||||
qDebug() << "Cardmarket:" << cardmarket;
|
||||
qDebug() << "Face2Face:" << face2face;
|
||||
qDebug() << "Manapool:" << manapool;
|
||||
qDebug() << "Mtgstocks:" << mtgstocks;
|
||||
qDebug() << "SCG:" << scg;
|
||||
qDebug() << "TCGL:" << tcgl;
|
||||
qDebug() << "Tcgplayer:" << tcgplayer;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
#ifndef EDHREC_COMMANDER_API_RESPONSE_CARD_PRICES_H
|
||||
#define EDHREC_COMMANDER_API_RESPONSE_CARD_PRICES_H
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
class CardPrices
|
||||
{
|
||||
public:
|
||||
// Constructor
|
||||
CardPrices() = default;
|
||||
|
||||
// Parse prices from JSON
|
||||
void fromJson(const QJsonObject &json);
|
||||
void debugPrint() const;
|
||||
|
||||
// Getter methods for card prices
|
||||
const QJsonObject &getCardhoarder() const
|
||||
{
|
||||
return cardhoarder;
|
||||
}
|
||||
const QJsonObject &getCardkingdom() const
|
||||
{
|
||||
return cardkingdom;
|
||||
}
|
||||
const QJsonObject &getCardmarket() const
|
||||
{
|
||||
return cardmarket;
|
||||
}
|
||||
const QJsonObject &getFace2face() const
|
||||
{
|
||||
return face2face;
|
||||
}
|
||||
const QJsonObject &getManapool() const
|
||||
{
|
||||
return manapool;
|
||||
}
|
||||
const QJsonObject &getMtgstocks() const
|
||||
{
|
||||
return mtgstocks;
|
||||
}
|
||||
const QJsonObject &getScg() const
|
||||
{
|
||||
return scg;
|
||||
}
|
||||
const QJsonObject &getTcgl() const
|
||||
{
|
||||
return tcgl;
|
||||
}
|
||||
const QJsonObject &getTcgplayer() const
|
||||
{
|
||||
return tcgplayer;
|
||||
}
|
||||
|
||||
private:
|
||||
QJsonObject cardhoarder;
|
||||
QJsonObject cardkingdom;
|
||||
QJsonObject cardmarket;
|
||||
QJsonObject face2face;
|
||||
QJsonObject manapool;
|
||||
QJsonObject mtgstocks;
|
||||
QJsonObject scg;
|
||||
QJsonObject tcgl;
|
||||
QJsonObject tcgplayer;
|
||||
};
|
||||
|
||||
#endif // EDHREC_COMMANDER_API_RESPONSE_CARD_PRICES_H
|
||||
@@ -0,0 +1,90 @@
|
||||
#include "edhrec_commander_api_response_commander_details.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
void EdhrecCommanderApiResponseCommanderDetails::fromJson(const QJsonObject &json)
|
||||
{
|
||||
// Parse card-related data
|
||||
aetherhubUri = json.value("aetherhub_uri").toString();
|
||||
archidektUri = json.value("archidekt_uri").toString();
|
||||
cmc = json.value("cmc").toInt(0);
|
||||
colorIdentity = json.value("color_identity").toArray();
|
||||
combos = json.value("combos").toBool(false);
|
||||
deckstatsUri = json.value("deckstats_uri").toString();
|
||||
|
||||
// Parse image URIs
|
||||
QJsonArray imageUrisArray = json.value("image_uris").toArray();
|
||||
for (const QJsonValue &imageValue : imageUrisArray) {
|
||||
QJsonObject imageObject = imageValue.toObject();
|
||||
imageUris.push_back(imageObject.value("normal").toString());
|
||||
imageUris.push_back(imageObject.value("art_crop").toString());
|
||||
}
|
||||
|
||||
inclusion = json.value("inclusion").toInt(0);
|
||||
isCommander = json.value("is_commander").toBool(false);
|
||||
label = json.value("label").toString();
|
||||
layout = json.value("layout").toString();
|
||||
legalCommander = json.value("legal_commander").toBool(false);
|
||||
moxfieldUri = json.value("moxfield_uri").toString();
|
||||
mtggoldfishUri = json.value("mtggoldfish_uri").toString();
|
||||
name = json.value("name").toString();
|
||||
names = json.value("names").toArray();
|
||||
numDecks = json.value("num_decks").toInt(0);
|
||||
potentialDecks = json.value("potential_decks").toInt(0);
|
||||
precon = json.value("precon").toString();
|
||||
|
||||
// Parse prices
|
||||
prices.fromJson(json.value("prices").toObject());
|
||||
|
||||
primaryType = json.value("primary_type").toString();
|
||||
rarity = json.value("rarity").toString();
|
||||
salt = json.value("salt").toDouble(0.0);
|
||||
sanitized = json.value("sanitized").toString();
|
||||
sanitizedWo = json.value("sanitized_wo").toString();
|
||||
scryfallUri = json.value("scryfall_uri").toString();
|
||||
spellbookUri = json.value("spellbook_uri").toString();
|
||||
type = json.value("type").toString();
|
||||
url = json.value("url").toString();
|
||||
}
|
||||
|
||||
void EdhrecCommanderApiResponseCommanderDetails::debugPrint() const
|
||||
{
|
||||
qDebug() << "Card Data:";
|
||||
qDebug() << "Aetherhub URI:" << aetherhubUri;
|
||||
qDebug() << "Archidekt URI:" << archidektUri;
|
||||
qDebug() << "CMC:" << cmc;
|
||||
qDebug() << "Color Identity:" << colorIdentity;
|
||||
qDebug() << "Combos:" << combos;
|
||||
qDebug() << "Deckstats URI:" << deckstatsUri;
|
||||
|
||||
qDebug() << "Image URIs:";
|
||||
for (const auto &uri : imageUris) {
|
||||
qDebug() << uri;
|
||||
}
|
||||
|
||||
qDebug() << "Inclusion:" << inclusion;
|
||||
qDebug() << "Is Commander:" << isCommander;
|
||||
qDebug() << "Label:" << label;
|
||||
qDebug() << "Layout:" << layout;
|
||||
qDebug() << "Legal Commander:" << legalCommander;
|
||||
qDebug() << "Moxfield URI:" << moxfieldUri;
|
||||
qDebug() << "MTGGoldfish URI:" << mtggoldfishUri;
|
||||
qDebug() << "Name:" << name;
|
||||
qDebug() << "Names:" << names;
|
||||
qDebug() << "Number of Decks:" << numDecks;
|
||||
qDebug() << "Potential Decks:" << potentialDecks;
|
||||
qDebug() << "Precon:" << precon;
|
||||
|
||||
// Print the prices using the debugPrint method from CardPrices
|
||||
prices.debugPrint();
|
||||
|
||||
qDebug() << "Primary Type:" << primaryType;
|
||||
qDebug() << "Rarity:" << rarity;
|
||||
qDebug() << "Salt:" << salt;
|
||||
qDebug() << "Sanitized:" << sanitized;
|
||||
qDebug() << "Sanitized WO:" << sanitizedWo;
|
||||
qDebug() << "Scryfall URI:" << scryfallUri;
|
||||
qDebug() << "Spellbook URI:" << spellbookUri;
|
||||
qDebug() << "Type:" << type;
|
||||
qDebug() << "URL:" << url;
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
#ifndef EDHREC_COMMANDER_API_RESPONSE_COMMANDER_DETAILS_H
|
||||
#define EDHREC_COMMANDER_API_RESPONSE_COMMANDER_DETAILS_H
|
||||
|
||||
#include "edhrec_commander_api_response_card_prices.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
class EdhrecCommanderApiResponseCommanderDetails
|
||||
{
|
||||
public:
|
||||
// Constructor
|
||||
EdhrecCommanderApiResponseCommanderDetails() = default;
|
||||
|
||||
// Parse card-related data from JSON
|
||||
void fromJson(const QJsonObject &json);
|
||||
|
||||
// Debug method for logging
|
||||
void debugPrint() const;
|
||||
|
||||
// Getters for the card data
|
||||
const QString &getAetherhubUri() const
|
||||
{
|
||||
return aetherhubUri;
|
||||
}
|
||||
const QString &getArchidektUri() const
|
||||
{
|
||||
return archidektUri;
|
||||
}
|
||||
int getCmc() const
|
||||
{
|
||||
return cmc;
|
||||
}
|
||||
const QJsonArray &getColorIdentity() const
|
||||
{
|
||||
return colorIdentity;
|
||||
}
|
||||
bool isCombos() const
|
||||
{
|
||||
return combos;
|
||||
}
|
||||
const QString &getDeckstatsUri() const
|
||||
{
|
||||
return deckstatsUri;
|
||||
}
|
||||
const QVector<QString> &getImageUris() const
|
||||
{
|
||||
return imageUris;
|
||||
}
|
||||
int getInclusion() const
|
||||
{
|
||||
return inclusion;
|
||||
}
|
||||
bool getIsCommander() const
|
||||
{
|
||||
return isCommander;
|
||||
}
|
||||
const QString &getLabel() const
|
||||
{
|
||||
return label;
|
||||
}
|
||||
const QString &getLayout() const
|
||||
{
|
||||
return layout;
|
||||
}
|
||||
bool getLegalCommander() const
|
||||
{
|
||||
return legalCommander;
|
||||
}
|
||||
const QString &getMoxfieldUri() const
|
||||
{
|
||||
return moxfieldUri;
|
||||
}
|
||||
const QString &getMtggoldfishUri() const
|
||||
{
|
||||
return mtggoldfishUri;
|
||||
}
|
||||
const QString &getName() const
|
||||
{
|
||||
return name;
|
||||
}
|
||||
const QJsonArray &getNames() const
|
||||
{
|
||||
return names;
|
||||
}
|
||||
int getNumDecks() const
|
||||
{
|
||||
return numDecks;
|
||||
}
|
||||
int getPotentialDecks() const
|
||||
{
|
||||
return potentialDecks;
|
||||
}
|
||||
const QString &getPrecon() const
|
||||
{
|
||||
return precon;
|
||||
}
|
||||
const CardPrices &getPrices() const
|
||||
{
|
||||
return prices;
|
||||
}
|
||||
const QString &getPrimaryType() const
|
||||
{
|
||||
return primaryType;
|
||||
}
|
||||
const QString &getRarity() const
|
||||
{
|
||||
return rarity;
|
||||
}
|
||||
double getSalt() const
|
||||
{
|
||||
return salt;
|
||||
}
|
||||
const QString &getSanitized() const
|
||||
{
|
||||
return sanitized;
|
||||
}
|
||||
const QString &getSanitizedWo() const
|
||||
{
|
||||
return sanitizedWo;
|
||||
}
|
||||
const QString &getScryfallUri() const
|
||||
{
|
||||
return scryfallUri;
|
||||
}
|
||||
const QString &getSpellbookUri() const
|
||||
{
|
||||
return spellbookUri;
|
||||
}
|
||||
const QString &getType() const
|
||||
{
|
||||
return type;
|
||||
}
|
||||
const QString &getUrl() const
|
||||
{
|
||||
return url;
|
||||
}
|
||||
|
||||
private:
|
||||
QString aetherhubUri;
|
||||
QString archidektUri;
|
||||
int cmc = 0;
|
||||
QJsonArray colorIdentity;
|
||||
bool combos = false;
|
||||
QString deckstatsUri;
|
||||
QVector<QString> imageUris;
|
||||
int inclusion = 0;
|
||||
bool isCommander = false;
|
||||
QString label;
|
||||
QString layout;
|
||||
bool legalCommander = false;
|
||||
QString moxfieldUri;
|
||||
QString mtggoldfishUri;
|
||||
QString name;
|
||||
QJsonArray names;
|
||||
int numDecks = 0;
|
||||
int potentialDecks = 0;
|
||||
QString precon;
|
||||
CardPrices prices;
|
||||
QString primaryType;
|
||||
QString rarity;
|
||||
double salt = 0.0;
|
||||
QString sanitized;
|
||||
QString sanitizedWo;
|
||||
QString scryfallUri;
|
||||
QString spellbookUri;
|
||||
QString type;
|
||||
QString url;
|
||||
};
|
||||
|
||||
#endif // EDHREC_COMMANDER_API_RESPONSE_COMMANDER_DETAILS_H
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "edhrec_commander_api_response_card_details_display_widget.h"
|
||||
|
||||
#include "../../../../game/cards/card_database_manager.h"
|
||||
|
||||
EdhrecCommanderApiResponseCardDetailsDisplayWidget::EdhrecCommanderApiResponseCardDetailsDisplayWidget(
|
||||
QWidget *parent,
|
||||
const EdhrecCommanderApiResponseCardDetails &_toDisplay)
|
||||
: QWidget(parent), toDisplay(_toDisplay)
|
||||
{
|
||||
layout = new QVBoxLayout(this);
|
||||
setLayout(layout);
|
||||
|
||||
cardPictureWidget = new CardInfoPictureWidget(this);
|
||||
cardPictureWidget->setCard(CardDatabaseManager::getInstance()->getCard(toDisplay.name));
|
||||
|
||||
label = new QLabel(this);
|
||||
label->setText(toDisplay.name + "\n" + toDisplay.label);
|
||||
label->setAlignment(Qt::AlignHCenter);
|
||||
|
||||
// Set label color based on inclusion rate
|
||||
int inclusionRate = (toDisplay.numDecks * 100) / toDisplay.potentialDecks;
|
||||
|
||||
QColor labelColor;
|
||||
if (inclusionRate <= 30) {
|
||||
labelColor = QColor(255, 0, 0); // Red
|
||||
} else if (inclusionRate <= 60) {
|
||||
int red = 255 - ((inclusionRate - 30) * 2);
|
||||
int green = (inclusionRate - 30) * 4; // Adjust green to make the transition smoother
|
||||
labelColor = QColor(red, green, 0); // purple-ish
|
||||
} else if (inclusionRate <= 90) {
|
||||
int green = (inclusionRate - 60) * 5; // Increase green
|
||||
labelColor = QColor(100, green, 100); // Green shades
|
||||
} else {
|
||||
labelColor = QColor(100, 200, 100); // Dark Green
|
||||
}
|
||||
|
||||
label->setStyleSheet(QString("color: %1").arg(labelColor.name()));
|
||||
|
||||
layout->addWidget(cardPictureWidget);
|
||||
layout->addWidget(label);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef EDHREC_COMMANDER_API_RESPONSE_CARD_DETAILS_DISPLAY_WIDGET_H
|
||||
#define EDHREC_COMMANDER_API_RESPONSE_CARD_DETAILS_DISPLAY_WIDGET_H
|
||||
|
||||
#include "../../../ui/widgets/cards/card_info_picture_widget.h"
|
||||
#include "api_response/edhrec_commander_api_response_card_details.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QWidget>
|
||||
|
||||
class EdhrecCommanderApiResponseCardDetailsDisplayWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit EdhrecCommanderApiResponseCardDetailsDisplayWidget(
|
||||
QWidget *parent,
|
||||
const EdhrecCommanderApiResponseCardDetails &_toDisplay);
|
||||
|
||||
private:
|
||||
EdhrecCommanderApiResponseCardDetails toDisplay;
|
||||
QVBoxLayout *layout;
|
||||
CardInfoPictureWidget *cardPictureWidget;
|
||||
QLabel *label;
|
||||
};
|
||||
|
||||
#endif // EDHREC_COMMANDER_API_RESPONSE_CARD_DETAILS_DISPLAY_WIDGET_H
|
||||
@@ -0,0 +1,34 @@
|
||||
#include "edhrec_commander_api_response_card_list_display_widget.h"
|
||||
|
||||
#include "../../../ui/widgets/general/display/banner_widget.h"
|
||||
#include "edhrec_commander_api_response_card_details_display_widget.h"
|
||||
|
||||
#include <QLabel>
|
||||
|
||||
EdhrecCommanderApiResponseCardListDisplayWidget::EdhrecCommanderApiResponseCardListDisplayWidget(
|
||||
QWidget *parent,
|
||||
EdhrecCommanderApiResponseCardList toDisplay)
|
||||
: QWidget(parent)
|
||||
{
|
||||
layout = new QVBoxLayout(this);
|
||||
setLayout(layout);
|
||||
|
||||
header = new BannerWidget(this, toDisplay.header);
|
||||
|
||||
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff);
|
||||
header->setBuddy(flowWidget);
|
||||
|
||||
foreach (EdhrecCommanderApiResponseCardDetails card_detail, toDisplay.cardViews) {
|
||||
auto widget = new EdhrecCommanderApiResponseCardDetailsDisplayWidget(flowWidget, card_detail);
|
||||
flowWidget->addWidget(widget);
|
||||
}
|
||||
|
||||
layout->addWidget(header);
|
||||
layout->addWidget(flowWidget);
|
||||
}
|
||||
|
||||
void EdhrecCommanderApiResponseCardListDisplayWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
qDebug() << event->size();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef EDHREC_COMMANDER_API_RESPONSE_CARD_LIST_DISPLAY_WIDGET_H
|
||||
#define EDHREC_COMMANDER_API_RESPONSE_CARD_LIST_DISPLAY_WIDGET_H
|
||||
|
||||
#include "../../../ui/widgets/general/display/banner_widget.h"
|
||||
#include "../../../ui/widgets/general/layout_containers/flow_widget.h"
|
||||
#include "api_response/edhrec_commander_api_response_card_list.h"
|
||||
|
||||
#include <QResizeEvent>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
class EdhrecCommanderApiResponseCardListDisplayWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit EdhrecCommanderApiResponseCardListDisplayWidget(QWidget *parent,
|
||||
EdhrecCommanderApiResponseCardList toDisplay);
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
[[nodiscard]] QString getBannerText() const
|
||||
{
|
||||
return header->getText();
|
||||
};
|
||||
|
||||
private:
|
||||
QVBoxLayout *layout;
|
||||
BannerWidget *header;
|
||||
FlowWidget *flowWidget;
|
||||
};
|
||||
|
||||
#endif // EDHREC_COMMANDER_API_RESPONSE_CARD_LIST_DISPLAY_WIDGET_H
|
||||
@@ -0,0 +1,36 @@
|
||||
#include "edhrec_commander_api_response_commander_details_display_widget.h"
|
||||
|
||||
#include "../../../../game/cards/card_database_manager.h"
|
||||
#include "../../../ui/widgets/cards/card_info_picture_widget.h"
|
||||
|
||||
#include <QLabel>
|
||||
|
||||
EdhrecCommanderResponseCommanderDetailsDisplayWidget::EdhrecCommanderResponseCommanderDetailsDisplayWidget(
|
||||
QWidget *parent,
|
||||
const EdhrecCommanderApiResponseCommanderDetails &_commanderDetails)
|
||||
: QWidget(parent), commanderDetails(_commanderDetails)
|
||||
{
|
||||
layout = new QVBoxLayout(this);
|
||||
setLayout(layout);
|
||||
|
||||
commanderPicture = new CardInfoPictureWidget(this);
|
||||
commanderPicture->setCard(CardDatabaseManager::getInstance()->getCard(commanderDetails.getName()));
|
||||
|
||||
commanderDetails.debugPrint();
|
||||
|
||||
label = new QLabel(this);
|
||||
label->setAlignment(Qt::AlignCenter);
|
||||
salt = new QLabel(this);
|
||||
salt->setAlignment(Qt::AlignCenter);
|
||||
|
||||
layout->addWidget(commanderPicture);
|
||||
layout->addWidget(label);
|
||||
layout->addWidget(salt);
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void EdhrecCommanderResponseCommanderDetailsDisplayWidget::retranslateUi()
|
||||
{
|
||||
label->setText(commanderDetails.getLabel());
|
||||
salt->setText(tr("Salt: ") + QString::number(commanderDetails.getSalt()));
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef EDHREC_COMMANDER_API_RESPONSE_COMMANDER_DETAILS_DISPLAY_WIDGET_H
|
||||
#define EDHREC_COMMANDER_API_RESPONSE_COMMANDER_DETAILS_DISPLAY_WIDGET_H
|
||||
|
||||
#include "../../../ui/widgets/cards/card_info_picture_widget.h"
|
||||
#include "api_response/edhrec_commander_api_response_commander_details.h"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
class EdhrecCommanderResponseCommanderDetailsDisplayWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit EdhrecCommanderResponseCommanderDetailsDisplayWidget(
|
||||
QWidget *parent,
|
||||
const EdhrecCommanderApiResponseCommanderDetails &_commanderDetails);
|
||||
void retranslateUi();
|
||||
|
||||
private:
|
||||
QLabel *label;
|
||||
QLabel *salt;
|
||||
QVBoxLayout *layout;
|
||||
CardInfoPictureWidget *commanderPicture;
|
||||
EdhrecCommanderApiResponseCommanderDetails commanderDetails;
|
||||
};
|
||||
|
||||
#endif // EDHREC_COMMANDER_API_RESPONSE_COMMANDER_DETAILS_DISPLAY_WIDGET_H
|
||||
@@ -0,0 +1,102 @@
|
||||
#include "edhrec_commander_api_response_display_widget.h"
|
||||
|
||||
#include "../../../ui/widgets/cards/card_info_picture_widget.h"
|
||||
#include "api_response/edhrec_commander_api_response.h"
|
||||
#include "edhrec_commander_api_response_card_list_display_widget.h"
|
||||
#include "edhrec_commander_api_response_commander_details_display_widget.h"
|
||||
|
||||
#include <QListView>
|
||||
#include <QResizeEvent>
|
||||
#include <QScrollArea>
|
||||
#include <QSplitter>
|
||||
#include <QStringListModel>
|
||||
|
||||
EdhrecCommanderApiResponseDisplayWidget::EdhrecCommanderApiResponseDisplayWidget(QWidget *parent,
|
||||
EdhrecCommanderApiResponse response)
|
||||
: QWidget(parent)
|
||||
{
|
||||
layout = new QHBoxLayout(this);
|
||||
setLayout(layout);
|
||||
|
||||
cardDisplayLayout = new QVBoxLayout(this);
|
||||
|
||||
// Create a QSplitter to hold the ListView and ScrollArea holding CardListdisplayWidgets side by side
|
||||
auto splitter = new QSplitter(this);
|
||||
splitter->setOrientation(Qt::Horizontal);
|
||||
|
||||
auto listView = new QListView(splitter);
|
||||
listView->setMinimumWidth(50);
|
||||
listView->setMaximumWidth(150);
|
||||
auto listModel = new QStringListModel(this);
|
||||
QStringList widgetNames;
|
||||
|
||||
// Add commander details
|
||||
auto commanderPicture =
|
||||
new EdhrecCommanderResponseCommanderDetailsDisplayWidget(this, response.container.getCommanderDetails());
|
||||
cardDisplayLayout->addWidget(commanderPicture);
|
||||
widgetNames.append("Commander Details");
|
||||
|
||||
// Add card list widgets
|
||||
auto edhrec_commander_api_response_card_lists = response.container.getCardlists();
|
||||
for (const EdhrecCommanderApiResponseCardList &card_list : edhrec_commander_api_response_card_lists) {
|
||||
auto cardListDisplayWidget = new EdhrecCommanderApiResponseCardListDisplayWidget(this, card_list);
|
||||
cardDisplayLayout->addWidget(cardListDisplayWidget);
|
||||
widgetNames.append(cardListDisplayWidget->getBannerText());
|
||||
}
|
||||
|
||||
// Create a QScrollArea to hold the card display widgets
|
||||
scrollArea = new QScrollArea(splitter);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
|
||||
// Set the cardDisplayLayout inside the scroll area
|
||||
auto scrollWidget = new QWidget(scrollArea);
|
||||
scrollWidget->setLayout(cardDisplayLayout);
|
||||
connect(splitter, &QSplitter::splitterMoved, this, &EdhrecCommanderApiResponseDisplayWidget::onSplitterChange);
|
||||
scrollArea->setWidget(scrollWidget);
|
||||
|
||||
// Configure the list view
|
||||
listModel->setStringList(widgetNames);
|
||||
listView->setModel(listModel);
|
||||
listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
|
||||
// Connect the list view to ensure the corresponding widget is visible
|
||||
connect(listView, &QListView::clicked, this, [this](const QModelIndex &index) {
|
||||
int widgetIndex = index.row();
|
||||
qDebug() << "clicked: " << widgetIndex;
|
||||
auto targetWidget = cardDisplayLayout->itemAt(widgetIndex)->widget();
|
||||
if (targetWidget) {
|
||||
qDebug() << "Found targetWidget" << targetWidget;
|
||||
// Attempt to cast the parent to QScrollArea
|
||||
auto scrollArea = qobject_cast<QScrollArea *>(this->scrollArea); // Use the scroll area instance
|
||||
if (scrollArea) {
|
||||
qDebug() << "ScrollArea" << scrollArea;
|
||||
scrollArea->ensureWidgetVisible(targetWidget);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add splitter to the main layout
|
||||
splitter->addWidget(listView);
|
||||
splitter->addWidget(scrollArea);
|
||||
|
||||
layout->addWidget(splitter);
|
||||
}
|
||||
|
||||
void EdhrecCommanderApiResponseDisplayWidget::onSplitterChange()
|
||||
{
|
||||
scrollArea->widget()->resize(scrollArea->size());
|
||||
}
|
||||
|
||||
void EdhrecCommanderApiResponseDisplayWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
qDebug() << event->size();
|
||||
layout->invalidate();
|
||||
layout->activate();
|
||||
layout->update();
|
||||
if (scrollArea && scrollArea->widget()) {
|
||||
scrollArea->widget()->resize(event->size());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef EDHREC_COMMANDER_API_RESPONSE_DISPLAY_WIDGET_H
|
||||
#define EDHREC_COMMANDER_API_RESPONSE_DISPLAY_WIDGET_H
|
||||
|
||||
#include "api_response/edhrec_commander_api_response.h"
|
||||
|
||||
#include <QScrollArea>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
class EdhrecCommanderApiResponseDisplayWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit EdhrecCommanderApiResponseDisplayWidget(QWidget *parent, EdhrecCommanderApiResponse response);
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
||||
public slots:
|
||||
void onSplitterChange();
|
||||
|
||||
private:
|
||||
QHBoxLayout *layout;
|
||||
QVBoxLayout *cardDisplayLayout;
|
||||
QScrollArea *scrollArea;
|
||||
};
|
||||
|
||||
#endif // EDHREC_COMMANDER_API_RESPONSE_DISPLAY_WIDGET_H
|
||||
112
cockatrice/src/client/tabs/api/edhrec/tab_edhrec.cpp
Normal file
@@ -0,0 +1,112 @@
|
||||
#include "tab_edhrec.h"
|
||||
|
||||
#include "api_response/edhrec_commander_api_response.h"
|
||||
#include "edhrec_commander_api_response_display_widget.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QHBoxLayout>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QRegularExpression>
|
||||
#include <QResizeEvent>
|
||||
|
||||
TabEdhRec::TabEdhRec(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor)
|
||||
{
|
||||
networkManager = new QNetworkAccessManager(this);
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
|
||||
networkManager->setTransferTimeout(); // Use Qt's default timeout
|
||||
#endif
|
||||
|
||||
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
|
||||
connect(networkManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(processApiJson(QNetworkReply *)));
|
||||
}
|
||||
|
||||
void TabEdhRec::retranslateUi()
|
||||
{
|
||||
}
|
||||
|
||||
void TabEdhRec::setCard(CardInfoPtr _cardToQuery, bool isCommander)
|
||||
{
|
||||
cardToQuery = _cardToQuery;
|
||||
|
||||
if (!cardToQuery) {
|
||||
qDebug() << "Invalid card information provided.";
|
||||
return;
|
||||
}
|
||||
|
||||
QString cardName = cardToQuery->getName();
|
||||
QString formattedName = cardName.toLower().replace(" ", "-").remove(QRegularExpression("[^a-z0-9\\-]"));
|
||||
|
||||
QString url;
|
||||
if (isCommander) {
|
||||
url = QString("https://json.edhrec.com/pages/commanders/%1.json").arg(formattedName);
|
||||
} else {
|
||||
url = QString("https://json.edhrec.com/pages/cards/%1.json").arg(formattedName);
|
||||
}
|
||||
|
||||
QNetworkRequest request{QUrl(url)};
|
||||
|
||||
networkManager->get(request);
|
||||
}
|
||||
|
||||
void TabEdhRec::processApiJson(QNetworkReply *reply)
|
||||
{
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qDebug() << "Network error occurred:" << reply->errorString();
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray responseData = reply->readAll();
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData);
|
||||
|
||||
if (!jsonDoc.isObject()) {
|
||||
qDebug() << "Invalid JSON response received.";
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject jsonObj = jsonDoc.object();
|
||||
|
||||
// qDebug() << jsonObj;
|
||||
|
||||
EdhrecCommanderApiResponse deckData;
|
||||
deckData.fromJson(jsonObj);
|
||||
|
||||
displayWidget = new EdhrecCommanderApiResponseDisplayWidget(this, deckData);
|
||||
// flowWidget->addWidget(displayWidget);
|
||||
setCentralWidget(displayWidget);
|
||||
|
||||
reply->deleteLater();
|
||||
update();
|
||||
}
|
||||
|
||||
void TabEdhRec::prettyPrintJson(const QJsonValue &value, int indentLevel)
|
||||
{
|
||||
const QString indent(indentLevel * 2, ' '); // Adjust spacing as needed for pretty printing
|
||||
|
||||
if (value.isObject()) {
|
||||
QJsonObject obj = value.toObject();
|
||||
for (auto it = obj.begin(); it != obj.end(); ++it) {
|
||||
qDebug().noquote() << indent + it.key() + ":";
|
||||
prettyPrintJson(it.value(), indentLevel + 1);
|
||||
}
|
||||
} else if (value.isArray()) {
|
||||
QJsonArray array = value.toArray();
|
||||
for (int i = 0; i < array.size(); ++i) {
|
||||
qDebug().noquote() << indent + QString("[%1]:").arg(i);
|
||||
prettyPrintJson(array[i], indentLevel + 1);
|
||||
}
|
||||
} else if (value.isString()) {
|
||||
qDebug().noquote() << indent + "\"" + value.toString() + "\"";
|
||||
} else if (value.isDouble()) {
|
||||
qDebug().noquote() << indent + QString::number(value.toDouble());
|
||||
} else if (value.isBool()) {
|
||||
qDebug().noquote() << indent + (value.toBool() ? "true" : "false");
|
||||
} else if (value.isNull()) {
|
||||
qDebug().noquote() << indent + "null";
|
||||
}
|
||||
}
|
||||
37
cockatrice/src/client/tabs/api/edhrec/tab_edhrec.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#ifndef TAB_EDHREC_H
|
||||
#define TAB_EDHREC_H
|
||||
|
||||
#include "../../../../game/cards/card_database.h"
|
||||
#include "../../../ui/widgets/general/layout_containers/flow_widget.h"
|
||||
#include "../../tab.h"
|
||||
#include "edhrec_commander_api_response_display_widget.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QNetworkAccessManager>
|
||||
|
||||
class TabEdhRec : public Tab
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TabEdhRec(TabSupervisor *_tabSupervisor);
|
||||
|
||||
void retranslateUi() override;
|
||||
QString getTabText() const override
|
||||
{
|
||||
auto cardName = cardToQuery.isNull() ? QString() : cardToQuery->getName();
|
||||
return tr("EDHREC: ") + cardName;
|
||||
}
|
||||
|
||||
QNetworkAccessManager *networkManager;
|
||||
|
||||
public slots:
|
||||
void processApiJson(QNetworkReply *reply);
|
||||
void prettyPrintJson(const QJsonValue &value, int indentLevel);
|
||||
void setCard(CardInfoPtr _cardToQuery, bool isCommander = false);
|
||||
|
||||
private:
|
||||
CardInfoPtr cardToQuery;
|
||||
EdhrecCommanderApiResponseDisplayWidget *displayWidget;
|
||||
};
|
||||
|
||||
#endif // TAB_EDHREC_H
|
||||
@@ -1,25 +1,29 @@
|
||||
#include "tab.h"
|
||||
|
||||
#include "../ui/widgets/cards/card_info_display_widget.h"
|
||||
#include "./tab_supervisor.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCloseEvent>
|
||||
#include <QDebug>
|
||||
#include <QScreen>
|
||||
|
||||
Tab::Tab(TabSupervisor *_tabSupervisor, QWidget *parent)
|
||||
: QMainWindow(parent), tabSupervisor(_tabSupervisor), contentsChanged(false), infoPopup(0)
|
||||
Tab::Tab(TabSupervisor *_tabSupervisor)
|
||||
: QMainWindow(_tabSupervisor), tabSupervisor(_tabSupervisor), contentsChanged(false), infoPopup(0)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
}
|
||||
|
||||
void Tab::showCardInfoPopup(const QPoint &pos, const QString &cardName)
|
||||
void Tab::showCardInfoPopup(const QPoint &pos, const QString &cardName, const QString &providerId)
|
||||
{
|
||||
if (infoPopup) {
|
||||
infoPopup->deleteLater();
|
||||
}
|
||||
currentCardName = cardName;
|
||||
infoPopup = new CardInfoDisplayWidget(
|
||||
cardName, 0, Qt::Widget | Qt::FramelessWindowHint | Qt::X11BypassWindowManagerHint | Qt::WindowStaysOnTopHint);
|
||||
currentProviderId = providerId;
|
||||
infoPopup = new CardInfoDisplayWidget(cardName, providerId, nullptr,
|
||||
Qt::Widget | Qt::FramelessWindowHint | Qt::X11BypassWindowManagerHint |
|
||||
Qt::WindowStaysOnTopHint);
|
||||
infoPopup->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
auto screenRect = qApp->primaryScreen()->geometry();
|
||||
@@ -39,3 +43,17 @@ void Tab::deleteCardInfoPopup(const QString &cardName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the closeEvent in order to emit a close signal
|
||||
*/
|
||||
void Tab::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
emit closed();
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void Tab::closeRequest(bool /*forced*/)
|
||||
{
|
||||
close();
|
||||
}
|
||||
@@ -13,6 +13,12 @@ class Tab : public QMainWindow
|
||||
signals:
|
||||
void userEvent(bool globalEvent = true);
|
||||
void tabTextChanged(Tab *tab, const QString &newTabText);
|
||||
/**
|
||||
* Emitted when the tab is closed (because Qt doesn't provide a built-in close signal)
|
||||
* This signal is emitted from this class's overridden Tab::closeEvent method.
|
||||
* Make sure any subclasses that override closeEvent still emit this signal from there.
|
||||
*/
|
||||
void closed();
|
||||
|
||||
protected:
|
||||
TabSupervisor *tabSupervisor;
|
||||
@@ -21,17 +27,18 @@ protected:
|
||||
tabMenus.append(menu);
|
||||
}
|
||||
protected slots:
|
||||
void showCardInfoPopup(const QPoint &pos, const QString &cardName);
|
||||
void showCardInfoPopup(const QPoint &pos, const QString &cardName, const QString &providerId);
|
||||
void deleteCardInfoPopup(const QString &cardName);
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
private:
|
||||
QString currentCardName;
|
||||
QString currentCardName, currentProviderId;
|
||||
bool contentsChanged;
|
||||
CardInfoDisplayWidget *infoPopup;
|
||||
QList<QMenu *> tabMenus;
|
||||
|
||||
public:
|
||||
Tab(TabSupervisor *_tabSupervisor, QWidget *parent = nullptr);
|
||||
explicit Tab(TabSupervisor *_tabSupervisor);
|
||||
const QList<QMenu *> &getTabMenus() const
|
||||
{
|
||||
return tabMenus;
|
||||
@@ -50,9 +57,13 @@ public:
|
||||
}
|
||||
virtual QString getTabText() const = 0;
|
||||
virtual void retranslateUi() = 0;
|
||||
virtual void closeRequest()
|
||||
{
|
||||
}
|
||||
/**
|
||||
* Sends a request to close the tab.
|
||||
* Signals for cleanup should be emitted from this method instead of the destructor.
|
||||
*
|
||||
* @param forced whether this close request was initiated by the user or forced by the server.
|
||||
*/
|
||||
virtual void closeRequest(bool forced = false);
|
||||
virtual void tabActivated()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
#include "../../deck/custom_line_edit.h"
|
||||
#include "../../server/pending_command.h"
|
||||
#include "../../server/user/user_info_box.h"
|
||||
#include "../../server/user/user_list.h"
|
||||
#include "../../server/user/user_list_manager.h"
|
||||
#include "../../server/user/user_list_widget.h"
|
||||
#include "../game_logic/abstract_client.h"
|
||||
#include "../sound_engine.h"
|
||||
#include "pb/event_add_to_list.pb.h"
|
||||
@@ -12,125 +13,118 @@
|
||||
#include "pb/event_user_left.pb.h"
|
||||
#include "pb/response_list_users.pb.h"
|
||||
#include "pb/session_commands.pb.h"
|
||||
#include "tab_supervisor.h"
|
||||
#include "trice_limits.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
TabUserLists::TabUserLists(TabSupervisor *_tabSupervisor,
|
||||
AbstractClient *_client,
|
||||
const ServerInfo_User &userInfo,
|
||||
QWidget *parent)
|
||||
: Tab(_tabSupervisor, parent), client(_client)
|
||||
TabAccount::TabAccount(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User &userInfo)
|
||||
: Tab(_tabSupervisor), client(_client)
|
||||
{
|
||||
allUsersList = new UserList(_tabSupervisor, client, UserList::AllUsersList);
|
||||
buddyList = new UserList(_tabSupervisor, client, UserList::BuddyList);
|
||||
ignoreList = new UserList(_tabSupervisor, client, UserList::IgnoreList);
|
||||
allUsersList = new UserListWidget(_tabSupervisor, client, UserListWidget::AllUsersList);
|
||||
buddyList = new UserListWidget(_tabSupervisor, client, UserListWidget::BuddyList);
|
||||
ignoreList = new UserListWidget(_tabSupervisor, client, UserListWidget::IgnoreList);
|
||||
userInfoBox = new UserInfoBox(client, true);
|
||||
userInfoBox->updateInfo(userInfo);
|
||||
|
||||
connect(allUsersList, SIGNAL(openMessageDialog(const QString &, bool)), this,
|
||||
SIGNAL(openMessageDialog(const QString &, bool)));
|
||||
connect(buddyList, SIGNAL(openMessageDialog(const QString &, bool)), this,
|
||||
SIGNAL(openMessageDialog(const QString &, bool)));
|
||||
connect(ignoreList, SIGNAL(openMessageDialog(const QString &, bool)), this,
|
||||
SIGNAL(openMessageDialog(const QString &, bool)));
|
||||
connect(allUsersList, &UserListWidget::openMessageDialog, this, &TabAccount::openMessageDialog);
|
||||
connect(buddyList, &UserListWidget::openMessageDialog, this, &TabAccount::openMessageDialog);
|
||||
connect(ignoreList, &UserListWidget::openMessageDialog, this, &TabAccount::openMessageDialog);
|
||||
|
||||
connect(client, SIGNAL(userJoinedEventReceived(const Event_UserJoined &)), this,
|
||||
SLOT(processUserJoinedEvent(const Event_UserJoined &)));
|
||||
connect(client, SIGNAL(userLeftEventReceived(const Event_UserLeft &)), this,
|
||||
SLOT(processUserLeftEvent(const Event_UserLeft &)));
|
||||
connect(client, SIGNAL(buddyListReceived(const QList<ServerInfo_User> &)), this,
|
||||
SLOT(buddyListReceived(const QList<ServerInfo_User> &)));
|
||||
connect(client, SIGNAL(ignoreListReceived(const QList<ServerInfo_User> &)), this,
|
||||
SLOT(ignoreListReceived(const QList<ServerInfo_User> &)));
|
||||
connect(client, SIGNAL(addToListEventReceived(const Event_AddToList &)), this,
|
||||
SLOT(processAddToListEvent(const Event_AddToList &)));
|
||||
connect(client, SIGNAL(removeFromListEventReceived(const Event_RemoveFromList &)), this,
|
||||
SLOT(processRemoveFromListEvent(const Event_RemoveFromList &)));
|
||||
connect(client, &AbstractClient::userJoinedEventReceived, this, &TabAccount::processUserJoinedEvent);
|
||||
connect(client, &AbstractClient::userLeftEventReceived, this, &TabAccount::processUserLeftEvent);
|
||||
connect(client, &AbstractClient::buddyListReceived, this, &TabAccount::buddyListReceived);
|
||||
connect(client, &AbstractClient::ignoreListReceived, this, &TabAccount::ignoreListReceived);
|
||||
connect(client, &AbstractClient::addToListEventReceived, this, &TabAccount::processAddToListEvent);
|
||||
connect(client, &AbstractClient::removeFromListEventReceived, this, &TabAccount::processRemoveFromListEvent);
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(Command_ListUsers());
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(processListUsersResponse(const Response &)));
|
||||
// Attempt to populate the tab with the cache
|
||||
buddyListReceived(tabSupervisor->getUserListManager()->getBuddyList().values());
|
||||
ignoreListReceived(tabSupervisor->getUserListManager()->getIgnoreList().values());
|
||||
|
||||
PendingCommand *pend = AbstractClient::prepareSessionCommand(Command_ListUsers());
|
||||
connect(pend, &PendingCommand::finished, this, &TabAccount::processListUsersResponse);
|
||||
client->sendCommand(pend);
|
||||
|
||||
QVBoxLayout *vbox = new QVBoxLayout;
|
||||
auto *vbox = new QVBoxLayout;
|
||||
vbox->addWidget(userInfoBox);
|
||||
vbox->addWidget(allUsersList);
|
||||
|
||||
QHBoxLayout *addToBuddyList = new QHBoxLayout;
|
||||
auto *addToBuddyList = new QHBoxLayout;
|
||||
addBuddyEdit = new LineEditUnfocusable;
|
||||
addBuddyEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||
addBuddyEdit->setPlaceholderText(tr("Add to Buddy List"));
|
||||
connect(addBuddyEdit, SIGNAL(returnPressed()), this, SLOT(addToBuddyList()));
|
||||
QPushButton *addBuddyButton = new QPushButton("Add");
|
||||
connect(addBuddyButton, SIGNAL(clicked()), this, SLOT(addToBuddyList()));
|
||||
connect(addBuddyEdit, &LineEditUnfocusable::returnPressed, this, &TabAccount::addToBuddyList);
|
||||
auto *addBuddyButton = new QPushButton("Add");
|
||||
connect(addBuddyButton, &QPushButton::clicked, this, &TabAccount::addToBuddyList);
|
||||
addToBuddyList->addWidget(addBuddyEdit);
|
||||
addToBuddyList->addWidget(addBuddyButton);
|
||||
|
||||
QHBoxLayout *addToIgnoreList = new QHBoxLayout;
|
||||
auto *addToIgnoreList = new QHBoxLayout;
|
||||
addIgnoreEdit = new LineEditUnfocusable;
|
||||
addIgnoreEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||
addIgnoreEdit->setPlaceholderText(tr("Add to Ignore List"));
|
||||
connect(addIgnoreEdit, SIGNAL(returnPressed()), this, SLOT(addToIgnoreList()));
|
||||
QPushButton *addIgnoreButton = new QPushButton("Add");
|
||||
connect(addIgnoreButton, SIGNAL(clicked()), this, SLOT(addToIgnoreList()));
|
||||
connect(addIgnoreEdit, &LineEditUnfocusable::returnPressed, this, &TabAccount::addToIgnoreList);
|
||||
auto *addIgnoreButton = new QPushButton("Add");
|
||||
connect(addIgnoreButton, &QPushButton::clicked, this, &TabAccount::addToIgnoreList);
|
||||
addToIgnoreList->addWidget(addIgnoreEdit);
|
||||
addToIgnoreList->addWidget(addIgnoreButton);
|
||||
|
||||
QVBoxLayout *buddyPanel = new QVBoxLayout;
|
||||
auto *buddyPanel = new QVBoxLayout;
|
||||
buddyPanel->addWidget(buddyList);
|
||||
buddyPanel->addLayout(addToBuddyList);
|
||||
|
||||
QVBoxLayout *ignorePanel = new QVBoxLayout;
|
||||
auto *ignorePanel = new QVBoxLayout;
|
||||
ignorePanel->addWidget(ignoreList);
|
||||
ignorePanel->addLayout(addToIgnoreList);
|
||||
|
||||
QHBoxLayout *mainLayout = new QHBoxLayout;
|
||||
auto *mainLayout = new QHBoxLayout;
|
||||
mainLayout->addLayout(buddyPanel);
|
||||
mainLayout->addLayout(ignorePanel);
|
||||
mainLayout->addLayout(vbox);
|
||||
|
||||
retranslateUi();
|
||||
|
||||
QWidget *mainWidget = new QWidget(this);
|
||||
auto *mainWidget = new QWidget(this);
|
||||
mainWidget->setLayout(mainLayout);
|
||||
setCentralWidget(mainWidget);
|
||||
}
|
||||
|
||||
void TabUserLists::addToBuddyList()
|
||||
void TabAccount::addToBuddyList()
|
||||
{
|
||||
QString userName = addBuddyEdit->text();
|
||||
if (userName.length() < 1)
|
||||
const QString &userName = addBuddyEdit->text();
|
||||
if (userName.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string listName = "buddy";
|
||||
const std::string listName = "buddy";
|
||||
addToList(listName, userName);
|
||||
addBuddyEdit->clear();
|
||||
}
|
||||
|
||||
void TabUserLists::addToIgnoreList()
|
||||
void TabAccount::addToIgnoreList()
|
||||
{
|
||||
QString userName = addIgnoreEdit->text();
|
||||
if (userName.length() < 1)
|
||||
const QString &userName = addIgnoreEdit->text();
|
||||
if (userName.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string listName = "ignore";
|
||||
const std::string listName = "ignore";
|
||||
addToList(listName, userName);
|
||||
addIgnoreEdit->clear();
|
||||
}
|
||||
|
||||
void TabUserLists::addToList(const std::string &listName, const QString &userName)
|
||||
void TabAccount::addToList(const std::string &listName, const QString &userName)
|
||||
{
|
||||
Command_AddToList cmd;
|
||||
cmd.set_list(listName);
|
||||
cmd.set_user_name(userName.toStdString());
|
||||
|
||||
client->sendCommand(client->prepareSessionCommand(cmd));
|
||||
client->sendCommand(AbstractClient::prepareSessionCommand(cmd));
|
||||
}
|
||||
|
||||
void TabUserLists::retranslateUi()
|
||||
void TabAccount::retranslateUi()
|
||||
{
|
||||
allUsersList->retranslateUi();
|
||||
buddyList->retranslateUi();
|
||||
@@ -138,14 +132,12 @@ void TabUserLists::retranslateUi()
|
||||
userInfoBox->retranslateUi();
|
||||
}
|
||||
|
||||
void TabUserLists::processListUsersResponse(const Response &response)
|
||||
void TabAccount::processListUsersResponse(const Response &response)
|
||||
{
|
||||
const Response_ListUsers &resp = response.GetExtension(Response_ListUsers::ext);
|
||||
|
||||
const int userListSize = resp.user_list_size();
|
||||
for (int i = 0; i < userListSize; ++i) {
|
||||
for (int i = 0; i < resp.user_list_size(); ++i) {
|
||||
const ServerInfo_User &info = resp.user_list(i);
|
||||
const QString userName = QString::fromStdString(info.name());
|
||||
const QString &userName = QString::fromStdString(info.name());
|
||||
allUsersList->processUserInfo(info, true);
|
||||
ignoreList->setUserOnline(userName, true);
|
||||
buddyList->setUserOnline(userName, true);
|
||||
@@ -156,10 +148,10 @@ void TabUserLists::processListUsersResponse(const Response &response)
|
||||
buddyList->sortItems();
|
||||
}
|
||||
|
||||
void TabUserLists::processUserJoinedEvent(const Event_UserJoined &event)
|
||||
void TabAccount::processUserJoinedEvent(const Event_UserJoined &event)
|
||||
{
|
||||
const ServerInfo_User &info = event.user_info();
|
||||
const QString userName = QString::fromStdString(info.name());
|
||||
const QString &userName = QString::fromStdString(info.name());
|
||||
|
||||
allUsersList->processUserInfo(info, true);
|
||||
ignoreList->setUserOnline(userName, true);
|
||||
@@ -169,18 +161,20 @@ void TabUserLists::processUserJoinedEvent(const Event_UserJoined &event)
|
||||
ignoreList->sortItems();
|
||||
buddyList->sortItems();
|
||||
|
||||
if (buddyList->getUsers().keys().contains(userName))
|
||||
if (buddyList->getUsers().keys().contains(userName)) {
|
||||
soundEngine->playSound("buddy_join");
|
||||
}
|
||||
|
||||
emit userJoined(info);
|
||||
}
|
||||
|
||||
void TabUserLists::processUserLeftEvent(const Event_UserLeft &event)
|
||||
void TabAccount::processUserLeftEvent(const Event_UserLeft &event)
|
||||
{
|
||||
QString userName = QString::fromStdString(event.name());
|
||||
const QString &userName = QString::fromStdString(event.name());
|
||||
|
||||
if (buddyList->getUsers().keys().contains(userName))
|
||||
if (buddyList->getUsers().keys().contains(userName)) {
|
||||
soundEngine->playSound("buddy_leave");
|
||||
}
|
||||
|
||||
if (allUsersList->deleteUser(userName)) {
|
||||
ignoreList->setUserOnline(userName, false);
|
||||
@@ -192,47 +186,54 @@ void TabUserLists::processUserLeftEvent(const Event_UserLeft &event)
|
||||
}
|
||||
}
|
||||
|
||||
void TabUserLists::buddyListReceived(const QList<ServerInfo_User> &_buddyList)
|
||||
void TabAccount::buddyListReceived(const QList<ServerInfo_User> &_buddyList)
|
||||
{
|
||||
for (int i = 0; i < _buddyList.size(); ++i)
|
||||
buddyList->processUserInfo(_buddyList[i], false);
|
||||
for (const auto &user : _buddyList) {
|
||||
buddyList->processUserInfo(user, false);
|
||||
}
|
||||
buddyList->sortItems();
|
||||
}
|
||||
|
||||
void TabUserLists::ignoreListReceived(const QList<ServerInfo_User> &_ignoreList)
|
||||
void TabAccount::ignoreListReceived(const QList<ServerInfo_User> &_ignoreList)
|
||||
{
|
||||
for (int i = 0; i < _ignoreList.size(); ++i)
|
||||
ignoreList->processUserInfo(_ignoreList[i], false);
|
||||
for (const auto &user : _ignoreList) {
|
||||
ignoreList->processUserInfo(user, false);
|
||||
}
|
||||
ignoreList->sortItems();
|
||||
}
|
||||
|
||||
void TabUserLists::processAddToListEvent(const Event_AddToList &event)
|
||||
void TabAccount::processAddToListEvent(const Event_AddToList &event)
|
||||
{
|
||||
const ServerInfo_User &info = event.user_info();
|
||||
bool online = allUsersList->getUsers().contains(QString::fromStdString(info.name()));
|
||||
QString list = QString::fromStdString(event.list_name());
|
||||
UserList *userList = 0;
|
||||
if (list == "buddy")
|
||||
const bool online = allUsersList->getUsers().contains(QString::fromStdString(info.name()));
|
||||
const QString &list = QString::fromStdString(event.list_name());
|
||||
|
||||
UserListWidget *userList;
|
||||
if (list == "buddy") {
|
||||
userList = buddyList;
|
||||
else if (list == "ignore")
|
||||
} else if (list == "ignore") {
|
||||
userList = ignoreList;
|
||||
if (!userList)
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
userList->processUserInfo(info, online);
|
||||
userList->sortItems();
|
||||
}
|
||||
|
||||
void TabUserLists::processRemoveFromListEvent(const Event_RemoveFromList &event)
|
||||
void TabAccount::processRemoveFromListEvent(const Event_RemoveFromList &event)
|
||||
{
|
||||
QString list = QString::fromStdString(event.list_name());
|
||||
QString user = QString::fromStdString(event.user_name());
|
||||
UserList *userList = 0;
|
||||
if (list == "buddy")
|
||||
const auto &list = QString::fromStdString(event.list_name());
|
||||
const auto &user = QString::fromStdString(event.user_name());
|
||||
|
||||
UserListWidget *userList;
|
||||
if (list == "buddy") {
|
||||
userList = buddyList;
|
||||
else if (list == "ignore")
|
||||
} else if (list == "ignore") {
|
||||
userList = ignoreList;
|
||||
if (!userList)
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
userList->deleteUser(user);
|
||||
}
|
||||
|
||||
@@ -5,25 +5,25 @@
|
||||
#include "tab.h"
|
||||
|
||||
class AbstractClient;
|
||||
class UserList;
|
||||
class UserInfoBox;
|
||||
class LineEditUnfocusable;
|
||||
|
||||
class Event_AddToList;
|
||||
class Event_ListRooms;
|
||||
class Event_RemoveFromList;
|
||||
class Event_UserJoined;
|
||||
class Event_UserLeft;
|
||||
class LineEditUnfocusable;
|
||||
class Response;
|
||||
class ServerInfo_User;
|
||||
class Event_AddToList;
|
||||
class Event_RemoveFromList;
|
||||
class UserInfoBox;
|
||||
class UserListWidget;
|
||||
|
||||
class TabUserLists : public Tab
|
||||
class TabAccount : public Tab
|
||||
{
|
||||
Q_OBJECT
|
||||
signals:
|
||||
void openMessageDialog(const QString &userName, bool focus);
|
||||
void userLeft(const QString &userName);
|
||||
void userJoined(const ServerInfo_User &userInfo);
|
||||
|
||||
private slots:
|
||||
void processListUsersResponse(const Response &response);
|
||||
void processUserJoinedEvent(const Event_UserJoined &event);
|
||||
@@ -37,36 +37,21 @@ private slots:
|
||||
|
||||
private:
|
||||
AbstractClient *client;
|
||||
UserList *allUsersList;
|
||||
UserList *buddyList;
|
||||
UserList *ignoreList;
|
||||
UserListWidget *allUsersList;
|
||||
UserListWidget *buddyList;
|
||||
UserListWidget *ignoreList;
|
||||
UserInfoBox *userInfoBox;
|
||||
LineEditUnfocusable *addBuddyEdit;
|
||||
LineEditUnfocusable *addIgnoreEdit;
|
||||
void addToList(const std::string &listName, const QString &userName);
|
||||
|
||||
public:
|
||||
TabUserLists(TabSupervisor *_tabSupervisor,
|
||||
AbstractClient *_client,
|
||||
const ServerInfo_User &userInfo,
|
||||
QWidget *parent = nullptr);
|
||||
void retranslateUi();
|
||||
QString getTabText() const
|
||||
explicit TabAccount(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User &userInfo);
|
||||
void retranslateUi() override;
|
||||
[[nodiscard]] QString getTabText() const override
|
||||
{
|
||||
return tr("Account");
|
||||
}
|
||||
const UserList *getAllUsersList() const
|
||||
{
|
||||
return allUsersList;
|
||||
}
|
||||
const UserList *getBuddyList() const
|
||||
{
|
||||
return buddyList;
|
||||
}
|
||||
const UserList *getIgnoreList() const
|
||||
{
|
||||
return ignoreList;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
#include "tab_admin.h"
|
||||
|
||||
#include "../../server/pending_command.h"
|
||||
#include "../game_logic/abstract_client.h"
|
||||
#include "pb/admin_commands.pb.h"
|
||||
#include "pb/event_replay_added.pb.h"
|
||||
#include "pb/moderator_commands.pb.h"
|
||||
#include "trice_limits.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QSpinBox>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
ShutdownDialog::ShutdownDialog(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
@@ -29,8 +30,8 @@ ShutdownDialog::ShutdownDialog(QWidget *parent) : QDialog(parent)
|
||||
minutesEdit->setMaximum(999);
|
||||
|
||||
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
|
||||
connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &ShutdownDialog::accept);
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &ShutdownDialog::reject);
|
||||
|
||||
QGridLayout *mainLayout = new QGridLayout;
|
||||
mainLayout->addWidget(reasonLabel, 0, 0);
|
||||
@@ -53,34 +54,66 @@ int ShutdownDialog::getMinutes() const
|
||||
return minutesEdit->value();
|
||||
}
|
||||
|
||||
TabAdmin::TabAdmin(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _fullAdmin, QWidget *parent)
|
||||
: Tab(_tabSupervisor, parent), locked(true), client(_client), fullAdmin(_fullAdmin)
|
||||
TabAdmin::TabAdmin(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _fullAdmin)
|
||||
: Tab(_tabSupervisor), locked(true), client(_client), fullAdmin(_fullAdmin)
|
||||
{
|
||||
updateServerMessageButton = new QPushButton;
|
||||
connect(updateServerMessageButton, SIGNAL(clicked()), this, SLOT(actUpdateServerMessage()));
|
||||
connect(updateServerMessageButton, &QPushButton::clicked, this, &TabAdmin::actUpdateServerMessage);
|
||||
shutdownServerButton = new QPushButton;
|
||||
connect(shutdownServerButton, SIGNAL(clicked()), this, SLOT(actShutdownServer()));
|
||||
connect(shutdownServerButton, &QPushButton::clicked, this, &TabAdmin::actShutdownServer);
|
||||
reloadConfigButton = new QPushButton;
|
||||
connect(reloadConfigButton, SIGNAL(clicked()), this, SLOT(actReloadConfig()));
|
||||
connect(reloadConfigButton, &QPushButton::clicked, this, &TabAdmin::actReloadConfig);
|
||||
|
||||
QVBoxLayout *vbox = new QVBoxLayout;
|
||||
vbox->addWidget(updateServerMessageButton);
|
||||
vbox->addWidget(shutdownServerButton);
|
||||
vbox->addWidget(reloadConfigButton);
|
||||
vbox->addStretch();
|
||||
grantReplayAccessButton = new QPushButton;
|
||||
grantReplayAccessButton->setEnabled(false);
|
||||
connect(grantReplayAccessButton, &QPushButton::clicked, this, &TabAdmin::actGrantReplayAccess);
|
||||
replayIdToGrant = new QLineEdit;
|
||||
replayIdToGrant->setMaximumWidth(500);
|
||||
replayIdToGrant->setValidator(new QIntValidator(0, INT_MAX, this));
|
||||
connect(replayIdToGrant, &QLineEdit::textChanged, this,
|
||||
[=, this]() { grantReplayAccessButton->setEnabled(!replayIdToGrant->text().isEmpty()); });
|
||||
auto *grandReplayAccessLayout = new QGridLayout(this);
|
||||
grandReplayAccessLayout->addWidget(replayIdToGrant, 0, 0);
|
||||
grandReplayAccessLayout->addWidget(grantReplayAccessButton, 0, 1);
|
||||
|
||||
activateUserButton = new QPushButton;
|
||||
activateUserButton->setEnabled(false);
|
||||
connect(activateUserButton, &QPushButton::clicked, this, &TabAdmin::actForceActivateUser);
|
||||
userToActivate = new QLineEdit;
|
||||
userToActivate->setMaximumWidth(500);
|
||||
connect(userToActivate, &QLineEdit::textChanged, this,
|
||||
[=, this]() { activateUserButton->setEnabled(!userToActivate->text().isEmpty()); });
|
||||
auto *activateUserLayout = new QGridLayout(this);
|
||||
activateUserLayout->addWidget(userToActivate, 0, 0);
|
||||
activateUserLayout->addWidget(activateUserButton, 0, 1);
|
||||
|
||||
auto *adminVBox = new QVBoxLayout;
|
||||
adminVBox->addWidget(updateServerMessageButton);
|
||||
adminVBox->addWidget(shutdownServerButton);
|
||||
adminVBox->addWidget(reloadConfigButton);
|
||||
|
||||
adminGroupBox = new QGroupBox;
|
||||
adminGroupBox->setLayout(vbox);
|
||||
adminGroupBox->setLayout(adminVBox);
|
||||
adminGroupBox->setEnabled(false);
|
||||
|
||||
auto *moderatorVBox = new QVBoxLayout;
|
||||
moderatorVBox->addLayout(grandReplayAccessLayout);
|
||||
moderatorVBox->addLayout(activateUserLayout);
|
||||
|
||||
moderatorGroupBox = new QGroupBox;
|
||||
moderatorGroupBox->setLayout(moderatorVBox);
|
||||
moderatorGroupBox->setEnabled(false);
|
||||
|
||||
unlockButton = new QPushButton;
|
||||
connect(unlockButton, SIGNAL(clicked()), this, SLOT(actUnlock()));
|
||||
connect(unlockButton, &QPushButton::clicked, this, &TabAdmin::actUnlock);
|
||||
lockButton = new QPushButton;
|
||||
lockButton->setEnabled(false);
|
||||
connect(lockButton, SIGNAL(clicked()), this, SLOT(actLock()));
|
||||
connect(lockButton, &QPushButton::clicked, this, &TabAdmin::actLock);
|
||||
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout;
|
||||
mainLayout->addWidget(adminGroupBox);
|
||||
mainLayout->addWidget(moderatorGroupBox);
|
||||
mainLayout->addStretch();
|
||||
mainLayout->addWidget(unlockButton);
|
||||
mainLayout->addWidget(lockButton);
|
||||
|
||||
@@ -99,6 +132,13 @@ void TabAdmin::retranslateUi()
|
||||
shutdownServerButton->setText(tr("&Shut down server"));
|
||||
reloadConfigButton->setText(tr("&Reload configuration"));
|
||||
adminGroupBox->setTitle(tr("Server administration functions"));
|
||||
moderatorGroupBox->setTitle(tr("Server moderator functions"));
|
||||
|
||||
replayIdToGrant->setPlaceholderText(tr("Replay ID"));
|
||||
grantReplayAccessButton->setText(tr("Grant Replay Access"));
|
||||
|
||||
userToActivate->setPlaceholderText(tr("Username to Activate"));
|
||||
activateUserButton->setText(tr("Force Activate User"));
|
||||
|
||||
unlockButton->setText(tr("&Unlock functions"));
|
||||
lockButton->setText(tr("&Lock functions"));
|
||||
@@ -117,7 +157,7 @@ void TabAdmin::actShutdownServer()
|
||||
cmd.set_reason(dlg.getReason().toStdString());
|
||||
cmd.set_minutes(dlg.getMinutes());
|
||||
|
||||
client->sendCommand(client->prepareAdminCommand(cmd));
|
||||
client->sendCommand(AbstractClient::prepareAdminCommand(cmd));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,22 +167,98 @@ void TabAdmin::actReloadConfig()
|
||||
client->sendCommand(client->prepareAdminCommand(cmd));
|
||||
}
|
||||
|
||||
void TabAdmin::actGrantReplayAccess()
|
||||
{
|
||||
if (!replayIdToGrant) {
|
||||
return;
|
||||
}
|
||||
|
||||
Command_GrantReplayAccess cmd;
|
||||
cmd.set_replay_id(replayIdToGrant->text().toUInt());
|
||||
cmd.set_moderator_name(client->getUserName().toStdString());
|
||||
|
||||
auto *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabAdmin::grantReplayAccessProcessResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabAdmin::actForceActivateUser()
|
||||
{
|
||||
if (!userToActivate) {
|
||||
return;
|
||||
}
|
||||
|
||||
Command_ForceActivateUser cmd;
|
||||
cmd.set_username_to_activate(userToActivate->text().trimmed().toStdString());
|
||||
cmd.set_moderator_name(client->getUserName().toStdString());
|
||||
|
||||
auto *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend,
|
||||
QOverload<const Response &, const CommandContainer &, const QVariant &>::of(&PendingCommand::finished),
|
||||
this, &TabAdmin::activateUserProcessResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabAdmin::grantReplayAccessProcessResponse(const Response &response)
|
||||
{
|
||||
auto *event = new Event_ReplayAdded();
|
||||
|
||||
switch (response.response_code()) {
|
||||
case Response::RespOk:
|
||||
client->replayAddedEventReceived(*event);
|
||||
QMessageBox::information(this, tr("Success"), tr("Replay access granted"));
|
||||
break;
|
||||
case Response::RespContextError:
|
||||
QMessageBox::critical(this, tr("Error"), tr("Unable to grant replay access. Replay ID invalid"));
|
||||
break;
|
||||
default:
|
||||
QMessageBox::critical(this, tr("Error"), tr("Unable to grant replay access. Internal error"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void TabAdmin::activateUserProcessResponse(const Response &response)
|
||||
{
|
||||
switch (response.response_code()) {
|
||||
case Response::RespActivationAccepted:
|
||||
QMessageBox::information(this, tr("Success"), tr("User successfully activated"));
|
||||
break;
|
||||
case Response::RespNameNotFound:
|
||||
QMessageBox::critical(this, tr("Error"), tr("Unable to activate user. Username invalid"));
|
||||
break;
|
||||
case Response::RespActivationFailed:
|
||||
QMessageBox::critical(this, tr("Error"), tr("Unable to activate user. User already active"));
|
||||
break;
|
||||
default:
|
||||
QMessageBox::critical(this, tr("Error"), tr("Unable to activate user. Internal error"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void TabAdmin::actUnlock()
|
||||
{
|
||||
if (fullAdmin)
|
||||
if (fullAdmin) {
|
||||
adminGroupBox->setEnabled(true);
|
||||
}
|
||||
|
||||
moderatorGroupBox->setEnabled(true);
|
||||
lockButton->setEnabled(true);
|
||||
unlockButton->setEnabled(false);
|
||||
locked = false;
|
||||
|
||||
emit adminLockChanged(false);
|
||||
}
|
||||
|
||||
void TabAdmin::actLock()
|
||||
{
|
||||
if (fullAdmin)
|
||||
if (fullAdmin) {
|
||||
adminGroupBox->setEnabled(false);
|
||||
}
|
||||
|
||||
moderatorGroupBox->setEnabled(false);
|
||||
lockButton->setEnabled(false);
|
||||
unlockButton->setEnabled(true);
|
||||
locked = true;
|
||||
|
||||
emit adminLockChanged(true);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#ifndef TAB_ADMIN_H
|
||||
#define TAB_ADMIN_H
|
||||
|
||||
#include "pb/commands.pb.h"
|
||||
#include "pb/response.pb.h"
|
||||
#include "tab.h"
|
||||
|
||||
#include <QDialog>
|
||||
@@ -20,7 +22,7 @@ private:
|
||||
QSpinBox *minutesEdit;
|
||||
|
||||
public:
|
||||
ShutdownDialog(QWidget *parent = nullptr);
|
||||
explicit ShutdownDialog(QWidget *parent = nullptr);
|
||||
QString getReason() const;
|
||||
int getMinutes() const;
|
||||
};
|
||||
@@ -32,23 +34,29 @@ private:
|
||||
bool locked;
|
||||
AbstractClient *client;
|
||||
bool fullAdmin;
|
||||
QPushButton *updateServerMessageButton, *shutdownServerButton, *reloadConfigButton;
|
||||
QGroupBox *adminGroupBox;
|
||||
QPushButton *updateServerMessageButton, *shutdownServerButton, *reloadConfigButton, *grantReplayAccessButton,
|
||||
*activateUserButton;
|
||||
QGroupBox *adminGroupBox, *moderatorGroupBox;
|
||||
QPushButton *unlockButton, *lockButton;
|
||||
QLineEdit *replayIdToGrant, *userToActivate;
|
||||
signals:
|
||||
void adminLockChanged(bool lock);
|
||||
private slots:
|
||||
void actUpdateServerMessage();
|
||||
void actShutdownServer();
|
||||
void actReloadConfig();
|
||||
void actGrantReplayAccess();
|
||||
void actForceActivateUser();
|
||||
void grantReplayAccessProcessResponse(const Response &response);
|
||||
void activateUserProcessResponse(const Response &response);
|
||||
|
||||
void actUnlock();
|
||||
void actLock();
|
||||
|
||||
public:
|
||||
TabAdmin(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _fullAdmin, QWidget *parent = nullptr);
|
||||
void retranslateUi();
|
||||
QString getTabText() const
|
||||
TabAdmin(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _fullAdmin);
|
||||
void retranslateUi() override;
|
||||
QString getTabText() const override
|
||||
{
|
||||
return tr("Administration");
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
#include "../../client/game_logic/abstract_client.h"
|
||||
#include "../../client/tapped_out_interface.h"
|
||||
#include "../../client/ui/widgets/cards/card_info_frame_widget.h"
|
||||
#include "../../deck/deck_list_model.h"
|
||||
#include "../../deck/deck_stats_interface.h"
|
||||
#include "../../dialogs/dlg_load_deck.h"
|
||||
#include "../../dialogs/dlg_load_deck_from_clipboard.h"
|
||||
#include "../../game/cards/card_database_manager.h"
|
||||
#include "../../game/cards/card_database_model.h"
|
||||
@@ -13,9 +13,8 @@
|
||||
#include "../../main.h"
|
||||
#include "../../server/pending_command.h"
|
||||
#include "../../settings/cache_settings.h"
|
||||
#include "../ui/picture_loader.h"
|
||||
#include "../ui/picture_loader/picture_loader.h"
|
||||
#include "../ui/pixel_map_generator.h"
|
||||
#include "../ui/widgets/printing_selector/printing_selector.h"
|
||||
#include "pb/command_deck_upload.pb.h"
|
||||
#include "pb/response.pb.h"
|
||||
#include "tab_supervisor.h"
|
||||
@@ -25,6 +24,8 @@
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QCloseEvent>
|
||||
#include <QComboBox>
|
||||
#include <QDebug>
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QDockWidget>
|
||||
@@ -50,20 +51,11 @@
|
||||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
void SearchLineEdit::keyPressEvent(QKeyEvent *event)
|
||||
static bool canBeCommander(const CardInfoPtr &cardInfo)
|
||||
{
|
||||
// List of key events that must be handled by the card list instead of the search box
|
||||
static const QVector<Qt::Key> forwardToTreeView = {Qt::Key_Up, Qt::Key_Down, Qt::Key_PageDown, Qt::Key_PageUp};
|
||||
// forward only if the search text is empty
|
||||
static const QVector<Qt::Key> forwardWhenEmpty = {Qt::Key_Home, Qt::Key_End};
|
||||
Qt::Key key = static_cast<Qt::Key>(event->key());
|
||||
if (treeView) {
|
||||
if (forwardToTreeView.contains(key))
|
||||
QCoreApplication::sendEvent(treeView, event);
|
||||
if (text().isEmpty() && forwardWhenEmpty.contains(key))
|
||||
QCoreApplication::sendEvent(treeView, event);
|
||||
}
|
||||
LineEditUnfocusable::keyPressEvent(event);
|
||||
return ((cardInfo->getCardType().contains("Legendary", Qt::CaseInsensitive) &&
|
||||
cardInfo->getCardType().contains("Creature", Qt::CaseInsensitive))) ||
|
||||
cardInfo->getText().contains("can be your commander", Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
void TabDeckEditor::createDeckDock()
|
||||
@@ -80,6 +72,7 @@ void TabDeckEditor::createDeckDock()
|
||||
deckView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
deckView->installEventFilter(&deckViewKeySignals);
|
||||
deckView->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
connect(deckView->selectionModel(), SIGNAL(currentRowChanged(const QModelIndex &, const QModelIndex &)), this,
|
||||
SLOT(updateCardInfoRight(const QModelIndex &, const QModelIndex &)));
|
||||
connect(deckView->selectionModel(), SIGNAL(currentRowChanged(const QModelIndex &, const QModelIndex &)), this,
|
||||
@@ -109,6 +102,16 @@ void TabDeckEditor::createDeckDock()
|
||||
commentsEdit->setObjectName("commentsEdit");
|
||||
commentsLabel->setBuddy(commentsEdit);
|
||||
connect(commentsEdit, SIGNAL(textChanged()), this, SLOT(updateComments()));
|
||||
bannerCardLabel = new QLabel();
|
||||
bannerCardLabel->setObjectName("bannerCardLabel");
|
||||
bannerCardLabel->setText(tr("Banner Card"));
|
||||
bannerCardComboBox = new QComboBox(this);
|
||||
connect(deckModel, &DeckListModel::dataChanged, this, [this]() {
|
||||
// Delay the update to avoid race conditions
|
||||
QTimer::singleShot(100, this, &TabDeckEditor::updateBannerCardComboBox);
|
||||
});
|
||||
connect(bannerCardComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||
&TabDeckEditor::setBannerCard);
|
||||
|
||||
aIncrement = new QAction(QString(), this);
|
||||
aIncrement->setIcon(QPixmap("theme:icons/increment"));
|
||||
@@ -128,6 +131,12 @@ void TabDeckEditor::createDeckDock()
|
||||
auto *tbRemoveCard = new QToolButton(this);
|
||||
tbRemoveCard->setDefaultAction(aRemoveCard);
|
||||
|
||||
aSwapCard = new QAction(QString(), this);
|
||||
aSwapCard->setIcon(QPixmap("theme:icons/swap"));
|
||||
connect(aSwapCard, SIGNAL(triggered()), this, SLOT(actSwapCard()));
|
||||
auto *tbSwapCard = new QToolButton(this);
|
||||
tbSwapCard->setDefaultAction(aSwapCard);
|
||||
|
||||
auto *upperLayout = new QGridLayout;
|
||||
upperLayout->setObjectName("upperLayout");
|
||||
upperLayout->addWidget(nameLabel, 0, 0);
|
||||
@@ -136,6 +145,9 @@ void TabDeckEditor::createDeckDock()
|
||||
upperLayout->addWidget(commentsLabel, 1, 0);
|
||||
upperLayout->addWidget(commentsEdit, 1, 1);
|
||||
|
||||
upperLayout->addWidget(bannerCardLabel, 2, 0);
|
||||
upperLayout->addWidget(bannerCardComboBox, 2, 1);
|
||||
|
||||
hashLabel1 = new QLabel();
|
||||
hashLabel1->setObjectName("hashLabel1");
|
||||
auto *hashSizePolicy = new QSizePolicy();
|
||||
@@ -153,7 +165,8 @@ void TabDeckEditor::createDeckDock()
|
||||
lowerLayout->addWidget(tbIncrement, 0, 2);
|
||||
lowerLayout->addWidget(tbDecrement, 0, 3);
|
||||
lowerLayout->addWidget(tbRemoveCard, 0, 4);
|
||||
lowerLayout->addWidget(deckView, 1, 0, 1, 5);
|
||||
lowerLayout->addWidget(tbSwapCard, 0, 5);
|
||||
lowerLayout->addWidget(deckView, 1, 0, 1, 6);
|
||||
|
||||
// Create widgets for both layouts to make splitter work correctly
|
||||
auto *topWidget = new QWidget;
|
||||
@@ -308,6 +321,15 @@ void TabDeckEditor::createMenus()
|
||||
aLoadDeck = new QAction(QString(), this);
|
||||
connect(aLoadDeck, SIGNAL(triggered()), this, SLOT(actLoadDeck()));
|
||||
|
||||
loadRecentDeckMenu = new QMenu(this);
|
||||
connect(&SettingsCache::instance().recents(), &RecentsSettings::recentlyOpenedDeckPathsChanged, this,
|
||||
&TabDeckEditor::updateRecentlyOpened);
|
||||
|
||||
aClearRecents = new QAction(QString(), this);
|
||||
connect(aClearRecents, &QAction::triggered, this, &TabDeckEditor::actClearRecents);
|
||||
|
||||
updateRecentlyOpened();
|
||||
|
||||
aSaveDeck = new QAction(QString(), this);
|
||||
connect(aSaveDeck, SIGNAL(triggered()), this, SLOT(actSaveDeck()));
|
||||
|
||||
@@ -320,9 +342,17 @@ void TabDeckEditor::createMenus()
|
||||
aSaveDeckToClipboard = new QAction(QString(), this);
|
||||
connect(aSaveDeckToClipboard, SIGNAL(triggered()), this, SLOT(actSaveDeckToClipboard()));
|
||||
|
||||
aSaveDeckToClipboardNoSetNameAndNumber = new QAction(QString(), this);
|
||||
connect(aSaveDeckToClipboardNoSetNameAndNumber, SIGNAL(triggered()), this,
|
||||
SLOT(actSaveDeckToClipboardNoSetNameAndNumber()));
|
||||
|
||||
aSaveDeckToClipboardRaw = new QAction(QString(), this);
|
||||
connect(aSaveDeckToClipboardRaw, SIGNAL(triggered()), this, SLOT(actSaveDeckToClipboardRaw()));
|
||||
|
||||
aSaveDeckToClipboardRawNoSetNameAndNumber = new QAction(QString(), this);
|
||||
connect(aSaveDeckToClipboardRawNoSetNameAndNumber, SIGNAL(triggered()), this,
|
||||
SLOT(actSaveDeckToClipboardRawNoSetNameAndNumber()));
|
||||
|
||||
aPrintDeck = new QAction(QString(), this);
|
||||
connect(aPrintDeck, SIGNAL(triggered()), this, SLOT(actPrintDeck()));
|
||||
|
||||
@@ -341,7 +371,7 @@ void TabDeckEditor::createMenus()
|
||||
analyzeDeckMenu->addAction(aAnalyzeDeckTappedout);
|
||||
|
||||
aClose = new QAction(QString(), this);
|
||||
connect(aClose, SIGNAL(triggered()), this, SLOT(closeRequest()));
|
||||
connect(aClose, &QAction::triggered, this, [this] { closeRequest(); });
|
||||
|
||||
aClearFilterAll = new QAction(QString(), this);
|
||||
aClearFilterAll->setIcon(QPixmap("theme:icons/clearsearch"));
|
||||
@@ -353,11 +383,14 @@ void TabDeckEditor::createMenus()
|
||||
|
||||
saveDeckToClipboardMenu = new QMenu(this);
|
||||
saveDeckToClipboardMenu->addAction(aSaveDeckToClipboard);
|
||||
saveDeckToClipboardMenu->addAction(aSaveDeckToClipboardNoSetNameAndNumber);
|
||||
saveDeckToClipboardMenu->addAction(aSaveDeckToClipboardRaw);
|
||||
saveDeckToClipboardMenu->addAction(aSaveDeckToClipboardRawNoSetNameAndNumber);
|
||||
|
||||
deckMenu = new QMenu(this);
|
||||
deckMenu->addAction(aNewDeck);
|
||||
deckMenu->addAction(aLoadDeck);
|
||||
deckMenu->addMenu(loadRecentDeckMenu);
|
||||
deckMenu->addAction(aSaveDeck);
|
||||
deckMenu->addAction(aSaveDeckAs);
|
||||
deckMenu->addSeparator();
|
||||
@@ -514,31 +547,40 @@ void TabDeckEditor::databaseCustomMenu(QPoint point)
|
||||
QMenu menu;
|
||||
const CardInfoPtr info = currentCardInfo();
|
||||
|
||||
// add to deck and sideboard options
|
||||
QAction *addToDeck, *addToSideboard, *selectPrinting;
|
||||
addToDeck = menu.addAction(tr("Add to Deck"));
|
||||
addToSideboard = menu.addAction(tr("Add to Sideboard"));
|
||||
selectPrinting = menu.addAction(tr("Select Printing"));
|
||||
|
||||
connect(addToDeck, SIGNAL(triggered()), this, SLOT(actAddCard()));
|
||||
connect(addToSideboard, SIGNAL(triggered()), this, SLOT(actAddCardToSideboard()));
|
||||
connect(selectPrinting, &QAction::triggered, this, [this, info] { this->showPrintingSelector(); });
|
||||
|
||||
// filling out the related cards submenu
|
||||
auto *relatedMenu = new QMenu(tr("Show Related cards"));
|
||||
menu.addMenu(relatedMenu);
|
||||
auto relatedCards = info->getAllRelatedCards();
|
||||
if (relatedCards.isEmpty()) {
|
||||
relatedMenu->setDisabled(true);
|
||||
} else {
|
||||
for (const CardRelation *rel : relatedCards) {
|
||||
const QString &relatedCardName = rel->getName();
|
||||
QAction *relatedCard = relatedMenu->addAction(relatedCardName);
|
||||
connect(relatedCard, &QAction::triggered, cardInfo,
|
||||
[this, relatedCardName] { cardInfo->setCard(relatedCardName); });
|
||||
if (info) {
|
||||
// add to deck and sideboard options
|
||||
QAction *addToDeck, *addToSideboard, *selectPrinting, *edhRecCommander, *edhRecCard;
|
||||
addToDeck = menu.addAction(tr("Add to Deck"));
|
||||
addToSideboard = menu.addAction(tr("Add to Sideboard"));
|
||||
selectPrinting = menu.addAction(tr("Select Printing"));
|
||||
if (canBeCommander(info)) {
|
||||
edhRecCommander = menu.addAction(tr("Show on EDHREC (Commander)"));
|
||||
connect(edhRecCommander, &QAction::triggered, this,
|
||||
[this, info] { this->tabSupervisor->addEdhrecTab(info, true); });
|
||||
}
|
||||
edhRecCard = menu.addAction(tr("Show on EDHREC (Card)"));
|
||||
|
||||
connect(addToDeck, SIGNAL(triggered()), this, SLOT(actAddCard()));
|
||||
connect(addToSideboard, SIGNAL(triggered()), this, SLOT(actAddCardToSideboard()));
|
||||
connect(selectPrinting, &QAction::triggered, this, [this, info] { this->showPrintingSelector(); });
|
||||
connect(edhRecCard, &QAction::triggered, this, [this, info] { this->tabSupervisor->addEdhrecTab(info); });
|
||||
|
||||
// filling out the related cards submenu
|
||||
auto *relatedMenu = new QMenu(tr("Show Related cards"));
|
||||
menu.addMenu(relatedMenu);
|
||||
auto relatedCards = info->getAllRelatedCards();
|
||||
if (relatedCards.isEmpty()) {
|
||||
relatedMenu->setDisabled(true);
|
||||
} else {
|
||||
for (const CardRelation *rel : relatedCards) {
|
||||
const QString &relatedCardName = rel->getName();
|
||||
QAction *relatedCard = relatedMenu->addAction(relatedCardName);
|
||||
connect(relatedCard, &QAction::triggered, cardInfo,
|
||||
[this, relatedCardName] { cardInfo->setCard(relatedCardName); });
|
||||
}
|
||||
}
|
||||
menu.exec(databaseView->mapToGlobal(point));
|
||||
}
|
||||
menu.exec(databaseView->mapToGlobal(point));
|
||||
}
|
||||
|
||||
void TabDeckEditor::decklistCustomMenu(QPoint point)
|
||||
@@ -650,10 +692,10 @@ void TabDeckEditor::loadLayout()
|
||||
restoreGeometry(layouts.getDeckEditorGeometry());
|
||||
}
|
||||
|
||||
aCardInfoDockVisible->setChecked(cardInfoDock->isVisible());
|
||||
aFilterDockVisible->setChecked(filterDock->isVisible());
|
||||
aDeckDockVisible->setChecked(deckDock->isVisible());
|
||||
aPrintingSelectorDockVisible->setChecked(printingSelectorDock->isVisible());
|
||||
aCardInfoDockVisible->setChecked(!cardInfoDock->isHidden());
|
||||
aFilterDockVisible->setChecked(!filterDock->isHidden());
|
||||
aDeckDockVisible->setChecked(!deckDock->isHidden());
|
||||
aPrintingSelectorDockVisible->setChecked(!printingSelectorDock->isHidden());
|
||||
|
||||
aCardInfoDockFloating->setEnabled(aCardInfoDockVisible->isChecked());
|
||||
aDeckDockFloating->setEnabled(aDeckDockVisible->isChecked());
|
||||
@@ -680,8 +722,7 @@ void TabDeckEditor::loadLayout()
|
||||
QTimer::singleShot(100, this, SLOT(freeDocksSize()));
|
||||
}
|
||||
|
||||
TabDeckEditor::TabDeckEditor(TabSupervisor *_tabSupervisor, QWidget *parent)
|
||||
: Tab(_tabSupervisor, parent), modified(false)
|
||||
TabDeckEditor::TabDeckEditor(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor), modified(false)
|
||||
{
|
||||
setObjectName("TabDeckEditor");
|
||||
|
||||
@@ -703,11 +744,6 @@ TabDeckEditor::TabDeckEditor(TabSupervisor *_tabSupervisor, QWidget *parent)
|
||||
loadLayout();
|
||||
}
|
||||
|
||||
TabDeckEditor::~TabDeckEditor()
|
||||
{
|
||||
emit deckEditorClosing(this);
|
||||
}
|
||||
|
||||
void TabDeckEditor::retranslateUi()
|
||||
{
|
||||
cardInfo->retranslateUi();
|
||||
@@ -721,13 +757,17 @@ void TabDeckEditor::retranslateUi()
|
||||
|
||||
aNewDeck->setText(tr("&New deck"));
|
||||
aLoadDeck->setText(tr("&Load deck..."));
|
||||
loadRecentDeckMenu->setTitle(tr("Load recent deck..."));
|
||||
aClearRecents->setText(tr("Clear"));
|
||||
aSaveDeck->setText(tr("&Save deck"));
|
||||
aSaveDeckAs->setText(tr("Save deck &as..."));
|
||||
aLoadDeckFromClipboard->setText(tr("Load deck from cl&ipboard..."));
|
||||
|
||||
saveDeckToClipboardMenu->setTitle(tr("Save deck to clipboard"));
|
||||
aSaveDeckToClipboard->setText(tr("Annotated"));
|
||||
aSaveDeckToClipboardNoSetNameAndNumber->setText(tr("Annotated (No set name or number)"));
|
||||
aSaveDeckToClipboardRaw->setText(tr("Not Annotated"));
|
||||
aSaveDeckToClipboardRawNoSetNameAndNumber->setText(tr("Not Annotated (No set name or number)"));
|
||||
|
||||
aPrintDeck->setText(tr("&Print deck..."));
|
||||
|
||||
@@ -741,11 +781,10 @@ void TabDeckEditor::retranslateUi()
|
||||
aAddCard->setText(tr("Add card to &maindeck"));
|
||||
aAddCardToSideboard->setText(tr("Add card to &sideboard"));
|
||||
|
||||
aRemoveCard->setText(tr("&Remove row"));
|
||||
|
||||
aIncrement->setText(tr("&Increment number"));
|
||||
|
||||
aDecrement->setText(tr("&Decrement number"));
|
||||
aRemoveCard->setText(tr("&Remove row"));
|
||||
aSwapCard->setText(tr("Swap card to/from sideboard"));
|
||||
|
||||
deckMenu->setTitle(tr("&Deck Editor"));
|
||||
|
||||
@@ -797,6 +836,79 @@ void TabDeckEditor::updateComments()
|
||||
setSaveStatus(true);
|
||||
}
|
||||
|
||||
void TabDeckEditor::updateBannerCardComboBox()
|
||||
{
|
||||
// Store the current text of the combo box
|
||||
QString currentText = bannerCardComboBox->currentText();
|
||||
|
||||
// Block signals temporarily
|
||||
bool wasBlocked = bannerCardComboBox->blockSignals(true);
|
||||
|
||||
// Clear the existing items in the combo box
|
||||
bannerCardComboBox->clear();
|
||||
|
||||
// Prepare the new items with deduplication
|
||||
QSet<QPair<QString, QString>> bannerCardSet;
|
||||
InnerDecklistNode *listRoot = deckModel->getDeckList()->getRoot();
|
||||
for (int i = 0; i < listRoot->size(); i++) {
|
||||
InnerDecklistNode *currentZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
|
||||
for (int j = 0; j < currentZone->size(); j++) {
|
||||
DecklistCardNode *currentCard = dynamic_cast<DecklistCardNode *>(currentZone->at(j));
|
||||
if (!currentCard)
|
||||
continue;
|
||||
|
||||
for (int k = 0; k < currentCard->getNumber(); ++k) {
|
||||
CardInfoPtr info = CardDatabaseManager::getInstance()->getCardByNameAndProviderId(
|
||||
currentCard->getName(), currentCard->getCardProviderId());
|
||||
if (info) {
|
||||
bannerCardSet.insert(
|
||||
QPair<QString, QString>(currentCard->getName(), currentCard->getCardProviderId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QList<QPair<QString, QString>> pairList = bannerCardSet.values();
|
||||
|
||||
// Sort QList by the first() element of the QPair
|
||||
std::sort(pairList.begin(), pairList.end(), [](const QPair<QString, QString> &a, const QPair<QString, QString> &b) {
|
||||
return a.first.toLower() < b.first.toLower();
|
||||
});
|
||||
|
||||
for (const auto &pair : pairList) {
|
||||
QVariantMap dataMap;
|
||||
dataMap["name"] = pair.first;
|
||||
dataMap["uuid"] = pair.second;
|
||||
|
||||
bannerCardComboBox->addItem(pair.first, dataMap);
|
||||
}
|
||||
|
||||
// Try to restore the previous selection by finding the currentText
|
||||
int restoredIndex = bannerCardComboBox->findText(currentText);
|
||||
if (restoredIndex != -1) {
|
||||
bannerCardComboBox->setCurrentIndex(restoredIndex);
|
||||
} else {
|
||||
// Add a placeholder "-" and set it as the current selection
|
||||
int bannerIndex = bannerCardComboBox->findText(deckModel->getDeckList()->getBannerCard().first);
|
||||
if (bannerIndex != -1) {
|
||||
bannerCardComboBox->setCurrentIndex(bannerIndex);
|
||||
} else {
|
||||
bannerCardComboBox->insertItem(0, "-");
|
||||
bannerCardComboBox->setCurrentIndex(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the previous signal blocking state
|
||||
bannerCardComboBox->blockSignals(wasBlocked);
|
||||
}
|
||||
|
||||
void TabDeckEditor::setBannerCard(int /* changedIndex */)
|
||||
{
|
||||
QVariantMap itemData = bannerCardComboBox->itemData(bannerCardComboBox->currentIndex()).toMap();
|
||||
deckModel->getDeckList()->setBannerCard(
|
||||
QPair<QString, QString>(itemData["name"].toString(), itemData["uuid"].toString()));
|
||||
}
|
||||
|
||||
void TabDeckEditor::updateCardInfo(CardInfoPtr _card)
|
||||
{
|
||||
cardInfo->setCard(_card);
|
||||
@@ -868,6 +980,20 @@ void TabDeckEditor::updateHash()
|
||||
hashLabel->setText(deckModel->getDeckList()->getDeckHash());
|
||||
}
|
||||
|
||||
void TabDeckEditor::updateRecentlyOpened()
|
||||
{
|
||||
loadRecentDeckMenu->clear();
|
||||
for (const auto &deckPath : SettingsCache::instance().recents().getRecentlyOpenedDeckPaths()) {
|
||||
QAction *aRecentlyOpenedDeck = new QAction(deckPath, this);
|
||||
loadRecentDeckMenu->addAction(aRecentlyOpenedDeck);
|
||||
connect(aRecentlyOpenedDeck, &QAction::triggered, this,
|
||||
[=, this] { actOpenRecent(aRecentlyOpenedDeck->text()); });
|
||||
}
|
||||
loadRecentDeckMenu->addSeparator();
|
||||
loadRecentDeckMenu->addAction(aClearRecents);
|
||||
aClearRecents->setEnabled(SettingsCache::instance().recents().getRecentlyOpenedDeckPaths().length() > 0);
|
||||
}
|
||||
|
||||
bool TabDeckEditor::confirmClose()
|
||||
{
|
||||
if (modified) {
|
||||
@@ -881,10 +1007,14 @@ bool TabDeckEditor::confirmClose()
|
||||
return true;
|
||||
}
|
||||
|
||||
void TabDeckEditor::closeRequest()
|
||||
void TabDeckEditor::closeRequest(bool forced)
|
||||
{
|
||||
if (confirmClose())
|
||||
deleteLater();
|
||||
if (!forced && !confirmClose()) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit deckEditorClosing(this);
|
||||
close();
|
||||
}
|
||||
|
||||
void TabDeckEditor::actNewDeck()
|
||||
@@ -916,28 +1046,60 @@ void TabDeckEditor::actLoadDeck()
|
||||
return;
|
||||
}
|
||||
|
||||
QFileDialog dialog(this, tr("Load deck"));
|
||||
dialog.setDirectory(SettingsCache::instance().getDeckPath());
|
||||
dialog.setNameFilters(DeckLoader::fileNameFilters);
|
||||
DlgLoadDeck dialog(this);
|
||||
if (!dialog.exec())
|
||||
return;
|
||||
|
||||
QString fileName = dialog.selectedFiles().at(0);
|
||||
openDeckFromFile(fileName, deckOpenLocation);
|
||||
updateBannerCardComboBox();
|
||||
}
|
||||
|
||||
void TabDeckEditor::actOpenRecent(const QString &fileName)
|
||||
{
|
||||
auto deckOpenLocation = confirmOpen();
|
||||
|
||||
if (deckOpenLocation == CANCELLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
openDeckFromFile(fileName, deckOpenLocation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually opens the deck from file
|
||||
* @param fileName The path of the deck to open
|
||||
* @param deckOpenLocation Which tab to open the deck
|
||||
*/
|
||||
void TabDeckEditor::openDeckFromFile(const QString &fileName, DeckOpenLocation deckOpenLocation)
|
||||
{
|
||||
DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(fileName);
|
||||
|
||||
auto *l = new DeckLoader;
|
||||
if (l->loadFromFile(fileName, fmt)) {
|
||||
if (l->loadFromFile(fileName, fmt, true)) {
|
||||
SettingsCache::instance().recents().updateRecentlyOpenedDeckPaths(fileName);
|
||||
updateBannerCardComboBox();
|
||||
if (!l->getBannerCard().first.isEmpty()) {
|
||||
bannerCardComboBox->setCurrentIndex(bannerCardComboBox->findText(l->getBannerCard().first));
|
||||
}
|
||||
if (deckOpenLocation == NEW_TAB) {
|
||||
emit openDeckEditor(l);
|
||||
} else {
|
||||
setSaveStatus(false);
|
||||
setDeck(l);
|
||||
}
|
||||
} else
|
||||
} else {
|
||||
delete l;
|
||||
QMessageBox::critical(this, tr("Error"), tr("Could not open deck at %1").arg(fileName));
|
||||
}
|
||||
setSaveStatus(true);
|
||||
}
|
||||
|
||||
void TabDeckEditor::actClearRecents()
|
||||
{
|
||||
SettingsCache::instance().recents().clearRecentlyOpenedDeckPaths();
|
||||
}
|
||||
|
||||
void TabDeckEditor::saveDeckRemoteFinished(const Response &response)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk)
|
||||
@@ -999,6 +1161,9 @@ bool TabDeckEditor::actSaveDeckAs()
|
||||
return false;
|
||||
}
|
||||
setModified(false);
|
||||
|
||||
SettingsCache::instance().recents().updateRecentlyOpenedDeckPaths(fileName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1033,6 +1198,15 @@ void TabDeckEditor::actSaveDeckToClipboard()
|
||||
QApplication::clipboard()->setText(buffer, QClipboard::Selection);
|
||||
}
|
||||
|
||||
void TabDeckEditor::actSaveDeckToClipboardNoSetNameAndNumber()
|
||||
{
|
||||
QString buffer;
|
||||
QTextStream stream(&buffer);
|
||||
deckModel->getDeckList()->saveToStream_Plain(stream, true, false);
|
||||
QApplication::clipboard()->setText(buffer, QClipboard::Clipboard);
|
||||
QApplication::clipboard()->setText(buffer, QClipboard::Selection);
|
||||
}
|
||||
|
||||
void TabDeckEditor::actSaveDeckToClipboardRaw()
|
||||
{
|
||||
QString buffer;
|
||||
@@ -1042,6 +1216,15 @@ void TabDeckEditor::actSaveDeckToClipboardRaw()
|
||||
QApplication::clipboard()->setText(buffer, QClipboard::Selection);
|
||||
}
|
||||
|
||||
void TabDeckEditor::actSaveDeckToClipboardRawNoSetNameAndNumber()
|
||||
{
|
||||
QString buffer;
|
||||
QTextStream stream(&buffer);
|
||||
deckModel->getDeckList()->saveToStream_Plain(stream, false, false);
|
||||
QApplication::clipboard()->setText(buffer, QClipboard::Clipboard);
|
||||
QApplication::clipboard()->setText(buffer, QClipboard::Selection);
|
||||
}
|
||||
|
||||
void TabDeckEditor::actPrintDeck()
|
||||
{
|
||||
auto *dlg = new QPrintPreviewDialog(this);
|
||||
@@ -1199,9 +1382,25 @@ CardInfoPtr TabDeckEditor::currentCardInfo() const
|
||||
return CardDatabaseManager::getInstance()->getCard(cardName);
|
||||
}
|
||||
|
||||
void TabDeckEditor::addCardHelper(QString zoneName)
|
||||
/**
|
||||
* Gets the index of all the currently selected card nodes in the decklist table.
|
||||
* The list is in reverse order of the visual selection, so that rows can be deleted while iterating over them.
|
||||
*
|
||||
* @return A model index list containing all selected card nodes
|
||||
*/
|
||||
QModelIndexList TabDeckEditor::getSelectedCardNodes() const
|
||||
{
|
||||
auto selectedRows = deckView->selectionModel()->selectedRows();
|
||||
|
||||
const auto notLeafNode = [this](const auto &index) { return deckModel->hasChildren(index); };
|
||||
selectedRows.erase(std::remove_if(selectedRows.begin(), selectedRows.end(), notLeafNode), selectedRows.end());
|
||||
|
||||
std::reverse(selectedRows.begin(), selectedRows.end());
|
||||
return selectedRows;
|
||||
}
|
||||
|
||||
void TabDeckEditor::addCardHelper(const CardInfoPtr info, QString zoneName)
|
||||
{
|
||||
const CardInfoPtr info = currentCardInfo();
|
||||
if (!info)
|
||||
return;
|
||||
if (info->getIsToken())
|
||||
@@ -1209,6 +1408,7 @@ void TabDeckEditor::addCardHelper(QString zoneName)
|
||||
|
||||
QModelIndex newCardIndex = deckModel->addPreferredPrintingCard(info->getName(), zoneName, false);
|
||||
recursiveExpand(newCardIndex);
|
||||
deckView->clearSelection();
|
||||
deckView->setCurrentIndex(newCardIndex);
|
||||
setModified(true);
|
||||
searchEdit->setSelection(0, searchEdit->text().length());
|
||||
@@ -1216,18 +1416,50 @@ void TabDeckEditor::addCardHelper(QString zoneName)
|
||||
|
||||
void TabDeckEditor::actSwapCard()
|
||||
{
|
||||
const QModelIndex currentIndex = deckView->selectionModel()->currentIndex();
|
||||
auto selectedRows = getSelectedCardNodes();
|
||||
|
||||
// hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted
|
||||
// TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted
|
||||
if (selectedRows.length() == 1) {
|
||||
deckView->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
}
|
||||
|
||||
bool isModified = false;
|
||||
for (const auto ¤tIndex : selectedRows) {
|
||||
if (swapCard(currentIndex)) {
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
|
||||
if (isModified) {
|
||||
setModified(true);
|
||||
setSaveStatus(true);
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Swaps the card at the index between the maindeck and sideboard
|
||||
*
|
||||
* @param currentIndex The index to swap.
|
||||
* @return True if the swap was successful
|
||||
*/
|
||||
bool TabDeckEditor::swapCard(const QModelIndex ¤tIndex)
|
||||
{
|
||||
if (!currentIndex.isValid())
|
||||
return;
|
||||
return false;
|
||||
const QString cardName = currentIndex.sibling(currentIndex.row(), 1).data().toString();
|
||||
const QString cardProviderID = currentIndex.sibling(currentIndex.row(), 4).data().toString();
|
||||
const QModelIndex gparent = currentIndex.parent().parent();
|
||||
|
||||
if (!gparent.isValid())
|
||||
return;
|
||||
return false;
|
||||
|
||||
const QString zoneName = gparent.sibling(gparent.row(), 1).data(Qt::EditRole).toString();
|
||||
actDecrement();
|
||||
offsetCountAtIndex(currentIndex, -1);
|
||||
const QString otherZoneName = zoneName == DECK_ZONE_MAIN ? DECK_ZONE_SIDE : DECK_ZONE_MAIN;
|
||||
|
||||
// Third argument (true) says create the card no matter what, even if not in DB
|
||||
@@ -1236,8 +1468,7 @@ void TabDeckEditor::actSwapCard()
|
||||
true);
|
||||
recursiveExpand(newCardIndex);
|
||||
|
||||
setModified(true);
|
||||
setSaveStatus(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
void TabDeckEditor::actAddCard()
|
||||
@@ -1245,37 +1476,53 @@ void TabDeckEditor::actAddCard()
|
||||
if (QApplication::keyboardModifiers() & Qt::ControlModifier)
|
||||
actAddCardToSideboard();
|
||||
else
|
||||
addCardHelper(DECK_ZONE_MAIN);
|
||||
addCardHelper(currentCardInfo(), DECK_ZONE_MAIN);
|
||||
setSaveStatus(true);
|
||||
}
|
||||
|
||||
void TabDeckEditor::actAddCardToSideboard()
|
||||
{
|
||||
addCardHelper(DECK_ZONE_SIDE);
|
||||
addCardHelper(currentCardInfo(), DECK_ZONE_SIDE);
|
||||
setSaveStatus(true);
|
||||
}
|
||||
|
||||
void TabDeckEditor::actRemoveCard()
|
||||
{
|
||||
const QModelIndex ¤tIndex = deckView->selectionModel()->currentIndex();
|
||||
if (!currentIndex.isValid() || deckModel->hasChildren(currentIndex))
|
||||
return;
|
||||
deckModel->removeRow(currentIndex.row(), currentIndex.parent());
|
||||
auto selectedRows = getSelectedCardNodes();
|
||||
|
||||
DeckLoader *const deck = deckModel->getDeckList();
|
||||
setSaveStatus(!deck->isEmpty());
|
||||
setModified(true);
|
||||
// hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted
|
||||
// TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted
|
||||
if (selectedRows.length() == 1) {
|
||||
deckView->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
}
|
||||
|
||||
bool isModified = false;
|
||||
for (const auto &index : selectedRows) {
|
||||
if (!index.isValid() || deckModel->hasChildren(index)) {
|
||||
continue;
|
||||
}
|
||||
deckModel->removeRow(index.row(), index.parent());
|
||||
isModified = true;
|
||||
}
|
||||
|
||||
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
|
||||
if (isModified) {
|
||||
DeckLoader *const deck = deckModel->getDeckList();
|
||||
setSaveStatus(!deck->isEmpty());
|
||||
setModified(true);
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckEditor::offsetCountAtIndex(const QModelIndex &idx, int offset)
|
||||
{
|
||||
if (!idx.isValid() || offset == 0)
|
||||
if (!idx.isValid() || deckModel->hasChildren(idx)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QModelIndex numberIndex = idx.sibling(idx.row(), 0);
|
||||
const int count = deckModel->data(numberIndex, Qt::EditRole).toInt();
|
||||
const int new_count = count + offset;
|
||||
deckView->setCurrentIndex(numberIndex);
|
||||
if (new_count <= 0)
|
||||
deckModel->removeRow(idx.row(), idx.parent());
|
||||
else
|
||||
@@ -1286,14 +1533,18 @@ void TabDeckEditor::offsetCountAtIndex(const QModelIndex &idx, int offset)
|
||||
void TabDeckEditor::decrementCardHelper(QString zoneName)
|
||||
{
|
||||
const CardInfoPtr info = currentCardInfo();
|
||||
QModelIndex idx;
|
||||
|
||||
if (!info)
|
||||
return;
|
||||
if (info->getIsToken())
|
||||
zoneName = DECK_ZONE_TOKENS;
|
||||
|
||||
idx = deckModel->findCard(info->getName(), zoneName);
|
||||
QModelIndex idx = deckModel->findCard(info->getName(), zoneName);
|
||||
if (!idx.isValid()) {
|
||||
return;
|
||||
}
|
||||
deckView->clearSelection();
|
||||
deckView->setCurrentIndex(idx);
|
||||
offsetCountAtIndex(idx, -1);
|
||||
}
|
||||
|
||||
@@ -1315,14 +1566,28 @@ void TabDeckEditor::copyDatabaseCellContents()
|
||||
|
||||
void TabDeckEditor::actIncrement()
|
||||
{
|
||||
const QModelIndex ¤tIndex = deckView->selectionModel()->currentIndex();
|
||||
offsetCountAtIndex(currentIndex, 1);
|
||||
auto selectedRows = getSelectedCardNodes();
|
||||
|
||||
for (const auto &index : selectedRows) {
|
||||
offsetCountAtIndex(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckEditor::actDecrement()
|
||||
{
|
||||
const QModelIndex ¤tIndex = deckView->selectionModel()->currentIndex();
|
||||
offsetCountAtIndex(currentIndex, -1);
|
||||
auto selectedRows = getSelectedCardNodes();
|
||||
|
||||
// hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted
|
||||
// TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted
|
||||
if (selectedRows.length() == 1) {
|
||||
deckView->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
}
|
||||
|
||||
for (const auto &index : selectedRows) {
|
||||
offsetCountAtIndex(index, -1);
|
||||
}
|
||||
|
||||
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
}
|
||||
|
||||
void TabDeckEditor::setDeck(DeckLoader *_deck)
|
||||
@@ -1331,6 +1596,8 @@ void TabDeckEditor::setDeck(DeckLoader *_deck)
|
||||
|
||||
nameEdit->setText(deckModel->getDeckList()->getName());
|
||||
commentsEdit->setText(deckModel->getDeckList()->getComments());
|
||||
bannerCardComboBox->setCurrentText(deckModel->getDeckList()->getBannerCard().first);
|
||||
updateBannerCardComboBox();
|
||||
updateHash();
|
||||
deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder());
|
||||
deckView->expandAll();
|
||||
@@ -1415,25 +1682,25 @@ void TabDeckEditor::dockVisibleTriggered()
|
||||
{
|
||||
QObject *o = sender();
|
||||
if (o == aCardInfoDockVisible) {
|
||||
cardInfoDock->setVisible(aCardInfoDockVisible->isChecked());
|
||||
cardInfoDock->setHidden(!aCardInfoDockVisible->isChecked());
|
||||
aCardInfoDockFloating->setEnabled(aCardInfoDockVisible->isChecked());
|
||||
return;
|
||||
}
|
||||
|
||||
if (o == aDeckDockVisible) {
|
||||
deckDock->setVisible(aDeckDockVisible->isChecked());
|
||||
deckDock->setHidden(!aDeckDockVisible->isChecked());
|
||||
aDeckDockFloating->setEnabled(aDeckDockVisible->isChecked());
|
||||
return;
|
||||
}
|
||||
|
||||
if (o == aFilterDockVisible) {
|
||||
filterDock->setVisible(aFilterDockVisible->isChecked());
|
||||
filterDock->setHidden(!aFilterDockVisible->isChecked());
|
||||
aFilterDockFloating->setEnabled(aFilterDockVisible->isChecked());
|
||||
return;
|
||||
}
|
||||
|
||||
if (o == aPrintingSelectorDockVisible) {
|
||||
printingSelectorDock->setVisible(aPrintingSelectorDockVisible->isChecked());
|
||||
printingSelectorDock->setHidden(!aPrintingSelectorDockVisible->isChecked());
|
||||
aPrintingSelectorDockFloating->setEnabled(aPrintingSelectorDockVisible->isChecked());
|
||||
return;
|
||||
}
|
||||
@@ -1497,7 +1764,9 @@ void TabDeckEditor::setSaveStatus(bool newStatus)
|
||||
aSaveDeck->setEnabled(newStatus);
|
||||
aSaveDeckAs->setEnabled(newStatus);
|
||||
aSaveDeckToClipboard->setEnabled(newStatus);
|
||||
aSaveDeckToClipboardNoSetNameAndNumber->setEnabled(newStatus);
|
||||
aSaveDeckToClipboardRaw->setEnabled(newStatus);
|
||||
aSaveDeckToClipboardRawNoSetNameAndNumber->setEnabled(newStatus);
|
||||
saveDeckToClipboardMenu->setEnabled(newStatus);
|
||||
aPrintDeck->setEnabled(newStatus);
|
||||
analyzeDeckMenu->setEnabled(newStatus);
|
||||
|
||||
@@ -22,6 +22,7 @@ class DeckLoader;
|
||||
class Response;
|
||||
class FilterTreeModel;
|
||||
class FilterBuilder;
|
||||
class QComboBox;
|
||||
class QGroupBox;
|
||||
class QMessageBox;
|
||||
class QHBoxLayout;
|
||||
@@ -29,30 +30,14 @@ class QVBoxLayout;
|
||||
class QPushButton;
|
||||
class QDockWidget;
|
||||
|
||||
class SearchLineEdit : public LineEditUnfocusable
|
||||
{
|
||||
private:
|
||||
QTreeView *treeView;
|
||||
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
|
||||
public:
|
||||
SearchLineEdit() : LineEditUnfocusable(), treeView(nullptr)
|
||||
{
|
||||
}
|
||||
void setTreeView(QTreeView *_treeView)
|
||||
{
|
||||
treeView = _treeView;
|
||||
}
|
||||
};
|
||||
|
||||
class TabDeckEditor : public Tab
|
||||
{
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void updateName(const QString &name);
|
||||
void updateComments();
|
||||
void updateBannerCardComboBox();
|
||||
void setBannerCard(int);
|
||||
void updateHash();
|
||||
void updateCardInfoLeft(const QModelIndex ¤t, const QModelIndex &previous);
|
||||
void updateCardInfoRight(const QModelIndex ¤t, const QModelIndex &previous);
|
||||
@@ -61,14 +46,19 @@ private slots:
|
||||
void updateSearch(const QString &search);
|
||||
void databaseCustomMenu(QPoint point);
|
||||
void decklistCustomMenu(QPoint point);
|
||||
void updateRecentlyOpened();
|
||||
|
||||
void actNewDeck();
|
||||
void actLoadDeck();
|
||||
void actOpenRecent(const QString &fileName);
|
||||
void actClearRecents();
|
||||
bool actSaveDeck();
|
||||
bool actSaveDeckAs();
|
||||
void actLoadDeckFromClipboard();
|
||||
void actSaveDeckToClipboard();
|
||||
void actSaveDeckToClipboardNoSetNameAndNumber();
|
||||
void actSaveDeckToClipboardRaw();
|
||||
void actSaveDeckToClipboardRawNoSetNameAndNumber();
|
||||
void actPrintDeck();
|
||||
void actExportDeckDecklist();
|
||||
void actAnalyzeDeckDeckstats();
|
||||
@@ -120,10 +110,13 @@ private:
|
||||
|
||||
bool isBlankNewDeck() const;
|
||||
CardInfoPtr currentCardInfo() const;
|
||||
void addCardHelper(QString zoneName);
|
||||
void offsetCountAtIndex(const QModelIndex &idx, int offset);
|
||||
void decrementCardHelper(QString zoneName);
|
||||
bool swapCard(const QModelIndex &idx);
|
||||
void recursiveExpand(const QModelIndex &index);
|
||||
void openDeckFromFile(const QString &fileName, DeckOpenLocation deckOpenLocation);
|
||||
|
||||
QModelIndexList getSelectedCardNodes() const;
|
||||
|
||||
CardDatabaseModel *databaseModel;
|
||||
CardDatabaseDisplayModel *databaseDisplayModel;
|
||||
@@ -141,6 +134,8 @@ private:
|
||||
LineEditUnfocusable *nameEdit;
|
||||
QLabel *commentsLabel;
|
||||
QTextEdit *commentsEdit;
|
||||
QLabel *bannerCardLabel;
|
||||
QComboBox *bannerCardComboBox;
|
||||
QLabel *hashLabel1;
|
||||
LineEditUnfocusable *hashLabel;
|
||||
FilterTreeModel *filterModel;
|
||||
@@ -149,12 +144,13 @@ private:
|
||||
QWidget *filterBox;
|
||||
|
||||
QMenu *deckMenu, *viewMenu, *cardInfoDockMenu, *deckDockMenu, *filterDockMenu, *printingSelectorDockMenu,
|
||||
*analyzeDeckMenu, *saveDeckToClipboardMenu;
|
||||
QAction *aNewDeck, *aLoadDeck, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard, *aSaveDeckToClipboard,
|
||||
*aSaveDeckToClipboardRaw, *aPrintDeck, *aExportDeckDecklist, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout,
|
||||
*aClose;
|
||||
*analyzeDeckMenu, *saveDeckToClipboardMenu, *loadRecentDeckMenu;
|
||||
QAction *aNewDeck, *aLoadDeck, *aClearRecents, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard,
|
||||
*aSaveDeckToClipboard, *aSaveDeckToClipboardNoSetNameAndNumber, *aSaveDeckToClipboardRaw,
|
||||
*aSaveDeckToClipboardRawNoSetNameAndNumber, *aPrintDeck, *aExportDeckDecklist, *aAnalyzeDeckDeckstats,
|
||||
*aAnalyzeDeckTappedout, *aClose;
|
||||
QAction *aClearFilterAll, *aClearFilterOne;
|
||||
QAction *aAddCard, *aAddCardToSideboard, *aRemoveCard, *aIncrement, *aDecrement;
|
||||
QAction *aAddCard, *aAddCardToSideboard, *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;
|
||||
QAction *aResetLayout;
|
||||
QAction *aCardInfoDockVisible, *aCardInfoDockFloating, *aDeckDockVisible, *aDeckDockFloating, *aFilterDockVisible,
|
||||
*aFilterDockFloating, *aPrintingSelectorDockVisible, *aPrintingSelectorDockFloating;
|
||||
@@ -169,8 +165,7 @@ private:
|
||||
QWidget *centralWidget;
|
||||
|
||||
public:
|
||||
explicit TabDeckEditor(TabSupervisor *_tabSupervisor, QWidget *parent = nullptr);
|
||||
~TabDeckEditor() override;
|
||||
explicit TabDeckEditor(TabSupervisor *_tabSupervisor);
|
||||
void retranslateUi() override;
|
||||
QString getTabText() const override;
|
||||
void setDeck(DeckLoader *_deckLoader);
|
||||
@@ -183,9 +178,10 @@ public:
|
||||
void createMenus();
|
||||
void createCentralFrame();
|
||||
void updateCardInfo(CardInfoPtr _card);
|
||||
void addCardHelper(CardInfoPtr info, QString zoneName);
|
||||
|
||||
public slots:
|
||||
void closeRequest() override;
|
||||
void closeRequest(bool forced = false) override;
|
||||
void showPrintingSelector();
|
||||
signals:
|
||||
void openDeckEditor(const DeckLoader *deckLoader);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "../../server/pending_command.h"
|
||||
#include "../../server/remote/remote_decklist_tree_widget.h"
|
||||
#include "../../settings/cache_settings.h"
|
||||
#include "../game_logic/abstract_client.h"
|
||||
#include "../get_text_with_max.h"
|
||||
#include "decklist.h"
|
||||
#include "pb/command_deck_del.pb.h"
|
||||
@@ -19,6 +18,7 @@
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QDesktopServices>
|
||||
#include <QFileSystemModel>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
@@ -27,9 +27,12 @@
|
||||
#include <QMessageBox>
|
||||
#include <QToolBar>
|
||||
#include <QTreeView>
|
||||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_client)
|
||||
TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
|
||||
AbstractClient *_client,
|
||||
const ServerInfo_User *currentUserInfo)
|
||||
: Tab(_tabSupervisor), client(_client)
|
||||
{
|
||||
localDirModel = new QFileSystemModel(this);
|
||||
@@ -41,16 +44,33 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_c
|
||||
localDirView->setColumnHidden(1, true);
|
||||
localDirView->setRootIndex(localDirModel->index(localDirModel->rootPath(), 0));
|
||||
localDirView->setSortingEnabled(true);
|
||||
localDirView->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
localDirView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
localDirView->header()->setSortIndicator(0, Qt::AscendingOrder);
|
||||
|
||||
leftToolBar = new QToolBar;
|
||||
connect(localDirView, &QTreeView::doubleClicked, this, &TabDeckStorage::actLocalDoubleClick);
|
||||
|
||||
// Left side layout
|
||||
/* put an invisible dummy QToolBar in the leftmost column so that the main toolbar is centered.
|
||||
* Really ugly workaround, but I couldn't figure out the proper way to make it centered */
|
||||
QToolBar *dummyToolBar = new QToolBar(this);
|
||||
QSizePolicy sizePolicy = dummyToolBar->sizePolicy();
|
||||
sizePolicy.setRetainSizeWhenHidden(true);
|
||||
dummyToolBar->setSizePolicy(sizePolicy);
|
||||
dummyToolBar->setVisible(false);
|
||||
|
||||
leftToolBar = new QToolBar(this);
|
||||
leftToolBar->setOrientation(Qt::Horizontal);
|
||||
leftToolBar->setIconSize(QSize(32, 32));
|
||||
QHBoxLayout *leftToolBarLayout = new QHBoxLayout;
|
||||
leftToolBarLayout->addStretch();
|
||||
leftToolBarLayout->addWidget(leftToolBar);
|
||||
leftToolBarLayout->addStretch();
|
||||
|
||||
QToolBar *leftRightmostToolBar = new QToolBar(this);
|
||||
leftRightmostToolBar->setOrientation(Qt::Horizontal);
|
||||
leftRightmostToolBar->setIconSize(QSize(32, 32));
|
||||
|
||||
QGridLayout *leftToolBarLayout = new QGridLayout;
|
||||
leftToolBarLayout->addWidget(dummyToolBar, 0, 0, Qt::AlignLeft);
|
||||
leftToolBarLayout->addWidget(leftToolBar, 0, 1, Qt::AlignHCenter);
|
||||
leftToolBarLayout->addWidget(leftRightmostToolBar, 0, 2, Qt::AlignRight);
|
||||
|
||||
QVBoxLayout *leftVbox = new QVBoxLayout;
|
||||
leftVbox->addWidget(localDirView);
|
||||
@@ -58,6 +78,7 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_c
|
||||
leftGroupBox = new QGroupBox;
|
||||
leftGroupBox->setLayout(leftVbox);
|
||||
|
||||
// Right side layout
|
||||
rightToolBar = new QToolBar;
|
||||
rightToolBar->setOrientation(Qt::Horizontal);
|
||||
rightToolBar->setIconSize(QSize(32, 32));
|
||||
@@ -68,25 +89,38 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_c
|
||||
|
||||
serverDirView = new RemoteDeckList_TreeWidget(client);
|
||||
|
||||
connect(serverDirView, &QTreeView::doubleClicked, this, &TabDeckStorage::actRemoteDoubleClick);
|
||||
|
||||
QVBoxLayout *rightVbox = new QVBoxLayout;
|
||||
rightVbox->addWidget(serverDirView);
|
||||
rightVbox->addLayout(rightToolBarLayout);
|
||||
rightGroupBox = new QGroupBox;
|
||||
rightGroupBox->setLayout(rightVbox);
|
||||
|
||||
// combine layouts
|
||||
QHBoxLayout *hbox = new QHBoxLayout;
|
||||
hbox->addWidget(leftGroupBox);
|
||||
hbox->addWidget(rightGroupBox);
|
||||
|
||||
// Left side actions
|
||||
aOpenLocalDeck = new QAction(this);
|
||||
aOpenLocalDeck->setIcon(QPixmap("theme:icons/pencil"));
|
||||
connect(aOpenLocalDeck, SIGNAL(triggered()), this, SLOT(actOpenLocalDeck()));
|
||||
aUpload = new QAction(this);
|
||||
aUpload->setIcon(QPixmap("theme:icons/arrow_right_green"));
|
||||
connect(aUpload, SIGNAL(triggered()), this, SLOT(actUpload()));
|
||||
aNewLocalFolder = new QAction(this);
|
||||
aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
|
||||
connect(aNewLocalFolder, &QAction::triggered, this, &TabDeckStorage::actNewLocalFolder);
|
||||
aDeleteLocalDeck = new QAction(this);
|
||||
aDeleteLocalDeck->setIcon(QPixmap("theme:icons/remove_row"));
|
||||
connect(aDeleteLocalDeck, SIGNAL(triggered()), this, SLOT(actDeleteLocalDeck()));
|
||||
|
||||
aOpenDecksFolder = new QAction(this);
|
||||
aOpenDecksFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_DirOpenIcon));
|
||||
connect(aOpenDecksFolder, &QAction::triggered, this, &TabDeckStorage::actOpenDecksFolder);
|
||||
|
||||
// Right side actions
|
||||
aOpenRemoteDeck = new QAction(this);
|
||||
aOpenRemoteDeck->setIcon(QPixmap("theme:icons/pencil"));
|
||||
connect(aOpenRemoteDeck, SIGNAL(triggered()), this, SLOT(actOpenRemoteDeck()));
|
||||
@@ -100,9 +134,14 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_c
|
||||
aDeleteRemoteDeck->setIcon(QPixmap("theme:icons/remove_row"));
|
||||
connect(aDeleteRemoteDeck, SIGNAL(triggered()), this, SLOT(actDeleteRemoteDeck()));
|
||||
|
||||
// Add actions to toolbars
|
||||
leftToolBar->addAction(aOpenLocalDeck);
|
||||
leftToolBar->addAction(aUpload);
|
||||
leftToolBar->addAction(aNewLocalFolder);
|
||||
leftToolBar->addAction(aDeleteLocalDeck);
|
||||
|
||||
leftRightmostToolBar->addAction(aOpenDecksFolder);
|
||||
|
||||
rightToolBar->addAction(aOpenRemoteDeck);
|
||||
rightToolBar->addAction(aDownload);
|
||||
rightToolBar->addAction(aNewFolder);
|
||||
@@ -113,6 +152,10 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_c
|
||||
QWidget *mainWidget = new QWidget(this);
|
||||
mainWidget->setLayout(hbox);
|
||||
setCentralWidget(mainWidget);
|
||||
|
||||
connect(client, &AbstractClient::userInfoChanged, this, &TabDeckStorage::handleConnected);
|
||||
connect(client, &AbstractClient::statusChanged, this, &TabDeckStorage::handleConnectionChanged);
|
||||
setRemoteEnabled(currentUserInfo && currentUserInfo->user_level() & ServerInfo_User::IsRegistered);
|
||||
}
|
||||
|
||||
void TabDeckStorage::retranslateUi()
|
||||
@@ -124,9 +167,11 @@ void TabDeckStorage::retranslateUi()
|
||||
aUpload->setText(tr("Upload deck"));
|
||||
aOpenRemoteDeck->setText(tr("Open in deck editor"));
|
||||
aDownload->setText(tr("Download deck"));
|
||||
aNewLocalFolder->setText(tr("New folder"));
|
||||
aNewFolder->setText(tr("New folder"));
|
||||
aDeleteLocalDeck->setText(tr("Delete"));
|
||||
aDeleteRemoteDeck->setText(tr("Delete"));
|
||||
aOpenDecksFolder->setText(tr("Open decks folder"));
|
||||
}
|
||||
|
||||
QString TabDeckStorage::getTargetPath() const
|
||||
@@ -147,43 +192,91 @@ QString TabDeckStorage::getTargetPath() const
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::handleConnected(const ServerInfo_User &userInfo)
|
||||
{
|
||||
setRemoteEnabled(userInfo.user_level() & ServerInfo_User::IsRegistered);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is only responsible for handling the disconnect. The connect is already handled elsewhere
|
||||
*/
|
||||
void TabDeckStorage::handleConnectionChanged(ClientStatus status)
|
||||
{
|
||||
if (status == StatusDisconnected) {
|
||||
setRemoteEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::setRemoteEnabled(bool enabled)
|
||||
{
|
||||
aUpload->setEnabled(enabled);
|
||||
aOpenRemoteDeck->setEnabled(enabled);
|
||||
aDownload->setEnabled(enabled);
|
||||
aNewFolder->setEnabled(enabled);
|
||||
aDeleteRemoteDeck->setEnabled(enabled);
|
||||
|
||||
if (enabled) {
|
||||
serverDirView->refreshTree();
|
||||
} else {
|
||||
serverDirView->clearTree();
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::actLocalDoubleClick(const QModelIndex &curLeft)
|
||||
{
|
||||
if (!localDirModel->isDir(curLeft)) {
|
||||
actOpenLocalDeck();
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::actOpenLocalDeck()
|
||||
{
|
||||
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
|
||||
if (localDirModel->isDir(curLeft))
|
||||
return;
|
||||
QString filePath = localDirModel->filePath(curLeft);
|
||||
QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
|
||||
for (const auto &curLeft : curLefts) {
|
||||
if (localDirModel->isDir(curLeft))
|
||||
continue;
|
||||
QString filePath = localDirModel->filePath(curLeft);
|
||||
|
||||
DeckLoader deckLoader;
|
||||
if (!deckLoader.loadFromFile(filePath, DeckLoader::CockatriceFormat))
|
||||
return;
|
||||
DeckLoader deckLoader;
|
||||
if (!deckLoader.loadFromFile(filePath, DeckLoader::CockatriceFormat, true))
|
||||
continue;
|
||||
|
||||
emit openDeckEditor(&deckLoader);
|
||||
SettingsCache::instance().recents().updateRecentlyOpenedDeckPaths(filePath);
|
||||
|
||||
emit openDeckEditor(&deckLoader);
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::actUpload()
|
||||
{
|
||||
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
|
||||
if (localDirModel->isDir(curLeft))
|
||||
QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
|
||||
if (curLefts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString targetPath = getTargetPath();
|
||||
if (targetPath.length() > MAX_NAME_LENGTH) {
|
||||
qCritical() << "target path to upload to is too long" << targetPath;
|
||||
return;
|
||||
}
|
||||
|
||||
QString filePath = localDirModel->filePath(curLeft);
|
||||
for (const auto &curLeft : curLefts) {
|
||||
if (localDirModel->isDir(curLeft)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString filePath = localDirModel->filePath(curLeft);
|
||||
uploadDeck(filePath, targetPath);
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::uploadDeck(const QString &filePath, const QString &targetPath)
|
||||
{
|
||||
QFile deckFile(filePath);
|
||||
QFileInfo deckFileInfo(deckFile);
|
||||
|
||||
QString deckString;
|
||||
DeckLoader deck;
|
||||
bool error = !deck.loadFromFile(filePath, DeckLoader::CockatriceFormat);
|
||||
if (!error) {
|
||||
deckString = deck.writeToString_Native();
|
||||
error = deckString.length() > MAX_FILE_LENGTH;
|
||||
}
|
||||
if (error) {
|
||||
if (!deck.loadFromFile(filePath, DeckLoader::CockatriceFormat)) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Invalid deck file"));
|
||||
return;
|
||||
}
|
||||
@@ -202,6 +295,12 @@ void TabDeckStorage::actUpload()
|
||||
deck.setName(deck.getName().left(MAX_NAME_LENGTH));
|
||||
}
|
||||
|
||||
QString deckString = deck.writeToString_Native();
|
||||
if (deckString.length() > MAX_FILE_LENGTH) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Invalid deck file"));
|
||||
return;
|
||||
}
|
||||
|
||||
Command_DeckUpload cmd;
|
||||
cmd.set_path(targetPath.toStdString());
|
||||
cmd.set_deck_list(deckString.toStdString());
|
||||
@@ -226,34 +325,73 @@ void TabDeckStorage::uploadFinished(const Response &r, const CommandContainer &c
|
||||
serverDirView->addFileToTree(resp.new_file(), serverDirView->getNodeByPath(QString::fromStdString(cmd.path())));
|
||||
}
|
||||
|
||||
void TabDeckStorage::actDeleteLocalDeck()
|
||||
void TabDeckStorage::actNewLocalFolder()
|
||||
{
|
||||
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
|
||||
if (localDirModel->isDir(curLeft))
|
||||
|
||||
QModelIndex dirIndex;
|
||||
if (curLeft.isValid() && !localDirModel->isDir(curLeft)) {
|
||||
dirIndex = curLeft.parent();
|
||||
} else {
|
||||
dirIndex = curLeft;
|
||||
}
|
||||
|
||||
bool ok;
|
||||
QString folderName =
|
||||
QInputDialog::getText(this, tr("New folder"), tr("Name of new folder:"), QLineEdit::Normal, "", &ok);
|
||||
if (!ok || folderName.isEmpty())
|
||||
return;
|
||||
|
||||
if (QMessageBox::warning(this, tr("Delete local file"),
|
||||
tr("Are you sure you want to delete \"%1\"?").arg(localDirModel->fileName(curLeft)),
|
||||
localDirModel->mkdir(dirIndex, folderName);
|
||||
}
|
||||
|
||||
void TabDeckStorage::actDeleteLocalDeck()
|
||||
{
|
||||
const QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
|
||||
|
||||
if (curLefts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (QMessageBox::warning(this, tr("Delete local file"), tr("Are you sure you want to delete the selected files?"),
|
||||
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
|
||||
return;
|
||||
|
||||
localDirModel->remove(curLeft);
|
||||
for (const auto &curLeft : curLefts) {
|
||||
if (curLeft.isValid()) {
|
||||
localDirModel->remove(curLeft);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::actOpenDecksFolder()
|
||||
{
|
||||
QString dir = localDirModel->rootPath();
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(dir));
|
||||
}
|
||||
|
||||
void TabDeckStorage::actRemoteDoubleClick(const QModelIndex &curRight)
|
||||
{
|
||||
if (dynamic_cast<RemoteDeckList_TreeModel::FileNode *>(serverDirView->getNode(curRight))) {
|
||||
actOpenRemoteDeck();
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::actOpenRemoteDeck()
|
||||
{
|
||||
RemoteDeckList_TreeModel::FileNode *curRight =
|
||||
dynamic_cast<RemoteDeckList_TreeModel::FileNode *>(serverDirView->getCurrentItem());
|
||||
if (!curRight)
|
||||
return;
|
||||
for (const auto &curRight : serverDirView->getCurrentSelection()) {
|
||||
RemoteDeckList_TreeModel::FileNode *node = dynamic_cast<RemoteDeckList_TreeModel::FileNode *>(curRight);
|
||||
if (!node)
|
||||
continue;
|
||||
|
||||
Command_DeckDownload cmd;
|
||||
cmd.set_deck_id(curRight->getId());
|
||||
Command_DeckDownload cmd;
|
||||
cmd.set_deck_id(node->getId());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(openRemoteDeckFinished(Response, CommandContainer)));
|
||||
client->sendCommand(pend);
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(openRemoteDeckFinished(Response, CommandContainer)));
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::openRemoteDeckFinished(const Response &r, const CommandContainer &commandContainer)
|
||||
@@ -273,30 +411,46 @@ void TabDeckStorage::openRemoteDeckFinished(const Response &r, const CommandCont
|
||||
|
||||
void TabDeckStorage::actDownload()
|
||||
{
|
||||
QString filePath;
|
||||
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
|
||||
if (!curLeft.isValid())
|
||||
filePath = localDirModel->rootPath();
|
||||
else {
|
||||
while (!localDirModel->isDir(curLeft))
|
||||
curLeft = curLeft.parent();
|
||||
filePath = localDirModel->filePath(curLeft);
|
||||
while (!localDirModel->isDir(curLeft)) {
|
||||
curLeft = curLeft.parent();
|
||||
}
|
||||
|
||||
RemoteDeckList_TreeModel::FileNode *curRight =
|
||||
dynamic_cast<RemoteDeckList_TreeModel::FileNode *>(serverDirView->getCurrentItem());
|
||||
if (!curRight)
|
||||
return;
|
||||
filePath += QString("/deck_%1.cod").arg(curRight->getId());
|
||||
for (const auto curRight : serverDirView->selectionModel()->selectedRows()) {
|
||||
downloadNodeAtIndex(curLeft, curRight);
|
||||
}
|
||||
}
|
||||
|
||||
Command_DeckDownload cmd;
|
||||
cmd.set_deck_id(curRight->getId());
|
||||
void TabDeckStorage::downloadNodeAtIndex(const QModelIndex &curLeft, const QModelIndex &curRight)
|
||||
{
|
||||
auto node = serverDirView->getNode(curRight);
|
||||
if (const auto dirNode = dynamic_cast<RemoteDeckList_TreeModel::DirectoryNode *>(node)) {
|
||||
// node at index is a folder
|
||||
const QString name = dirNode->getName();
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
pend->setExtraData(filePath);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(downloadFinished(Response, CommandContainer, QVariant)));
|
||||
client->sendCommand(pend);
|
||||
const auto dirIndex = curLeft.isValid() ? curLeft : localDirModel->index(localDirModel->rootPath());
|
||||
const auto newDirIndex = localDirModel->mkdir(dirIndex, name);
|
||||
|
||||
int rows = serverDirView->model()->rowCount(curRight);
|
||||
for (int i = 0; i < rows; i++) {
|
||||
const auto childIndex = serverDirView->model()->index(i, 0, curRight);
|
||||
downloadNodeAtIndex(newDirIndex, childIndex);
|
||||
}
|
||||
} else if (const auto fileNode = dynamic_cast<RemoteDeckList_TreeModel::FileNode *>(node)) {
|
||||
// node at index is a deck
|
||||
const QString dirPath = curLeft.isValid() ? localDirModel->filePath(curLeft) : localDirModel->rootPath();
|
||||
const QString filePath = dirPath + QString("/deck_%1.cod").arg(fileNode->getId());
|
||||
|
||||
Command_DeckDownload cmd;
|
||||
cmd.set_deck_id(fileNode->getId());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
pend->setExtraData(filePath);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(downloadFinished(Response, CommandContainer, QVariant)));
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
// node at index is invalid
|
||||
}
|
||||
|
||||
void TabDeckStorage::downloadFinished(const Response &r,
|
||||
@@ -352,12 +506,30 @@ void TabDeckStorage::newFolderFinished(const Response &response, const CommandCo
|
||||
|
||||
void TabDeckStorage::actDeleteRemoteDeck()
|
||||
{
|
||||
PendingCommand *pend;
|
||||
RemoteDeckList_TreeModel::Node *curRight = serverDirView->getCurrentItem();
|
||||
if (!curRight)
|
||||
auto curRights = serverDirView->getCurrentSelection();
|
||||
|
||||
if (curRights.isEmpty()) {
|
||||
return;
|
||||
RemoteDeckList_TreeModel::DirectoryNode *dir = dynamic_cast<RemoteDeckList_TreeModel::DirectoryNode *>(curRight);
|
||||
if (dir) {
|
||||
}
|
||||
|
||||
if (QMessageBox::warning(this, tr("Delete remote decks"), tr("Are you sure you want to delete the selected decks?"),
|
||||
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto &curRight : curRights) {
|
||||
deleteRemoteDeck(curRight);
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::deleteRemoteDeck(const RemoteDeckList_TreeModel::Node *curRight)
|
||||
{
|
||||
if (!curRight) {
|
||||
return;
|
||||
}
|
||||
|
||||
PendingCommand *pend;
|
||||
if (const auto *dir = dynamic_cast<const RemoteDeckList_TreeModel::DirectoryNode *>(curRight)) {
|
||||
QString targetPath = dir->getPath();
|
||||
if (targetPath.isEmpty())
|
||||
return;
|
||||
@@ -365,22 +537,13 @@ void TabDeckStorage::actDeleteRemoteDeck()
|
||||
qCritical() << "target path to delete is too long" << targetPath;
|
||||
return;
|
||||
}
|
||||
if (QMessageBox::warning(this, tr("Delete remote folder"),
|
||||
tr("Are you sure you want to delete \"%1\"?").arg(targetPath),
|
||||
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
|
||||
return;
|
||||
Command_DeckDelDir cmd;
|
||||
cmd.set_path(targetPath.toStdString());
|
||||
pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(deleteFolderFinished(Response, CommandContainer)));
|
||||
} else {
|
||||
RemoteDeckList_TreeModel::FileNode *deckNode = dynamic_cast<RemoteDeckList_TreeModel::FileNode *>(curRight);
|
||||
if (QMessageBox::warning(this, tr("Delete remote deck"),
|
||||
tr("Are you sure you want to delete \"%1\"?").arg(deckNode->getName()),
|
||||
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
|
||||
return;
|
||||
|
||||
const auto *deckNode = dynamic_cast<const RemoteDeckList_TreeModel::FileNode *>(curRight);
|
||||
Command_DeckDel cmd;
|
||||
cmd.set_deck_id(deckNode->getId());
|
||||
pend = client->prepareSessionCommand(cmd);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#ifndef TAB_DECK_STORAGE_H
|
||||
#define TAB_DECK_STORAGE_H
|
||||
|
||||
#include "../../server/remote/remote_decklist_tree_widget.h"
|
||||
#include "../game_logic/abstract_client.h"
|
||||
#include "tab.h"
|
||||
|
||||
class ServerInfo_User;
|
||||
class AbstractClient;
|
||||
class QTreeView;
|
||||
class QFileSystemModel;
|
||||
@@ -10,7 +13,6 @@ class QToolBar;
|
||||
class QTreeWidget;
|
||||
class QTreeWidgetItem;
|
||||
class QGroupBox;
|
||||
class RemoteDeckList_TreeWidget;
|
||||
class CommandContainer;
|
||||
class Response;
|
||||
class DeckLoader;
|
||||
@@ -26,16 +28,34 @@ private:
|
||||
RemoteDeckList_TreeWidget *serverDirView;
|
||||
QGroupBox *leftGroupBox, *rightGroupBox;
|
||||
|
||||
QAction *aOpenLocalDeck, *aUpload, *aDeleteLocalDeck, *aOpenRemoteDeck, *aDownload, *aNewFolder, *aDeleteRemoteDeck;
|
||||
QAction *aOpenLocalDeck, *aUpload, *aNewLocalFolder, *aDeleteLocalDeck;
|
||||
QAction *aOpenDecksFolder;
|
||||
QAction *aOpenRemoteDeck, *aDownload, *aNewFolder, *aDeleteRemoteDeck;
|
||||
QString getTargetPath() const;
|
||||
|
||||
void setRemoteEnabled(bool enabled);
|
||||
|
||||
void uploadDeck(const QString &filePath, const QString &targetPath);
|
||||
void deleteRemoteDeck(const RemoteDeckList_TreeModel::Node *node);
|
||||
|
||||
void downloadNodeAtIndex(const QModelIndex &curLeft, const QModelIndex &curRight);
|
||||
|
||||
private slots:
|
||||
void handleConnected(const ServerInfo_User &userInfo);
|
||||
void handleConnectionChanged(ClientStatus status);
|
||||
|
||||
void actLocalDoubleClick(const QModelIndex &curLeft);
|
||||
void actOpenLocalDeck();
|
||||
|
||||
void actUpload();
|
||||
void uploadFinished(const Response &r, const CommandContainer &commandContainer);
|
||||
|
||||
void actNewLocalFolder();
|
||||
void actDeleteLocalDeck();
|
||||
|
||||
void actOpenDecksFolder();
|
||||
|
||||
void actRemoteDoubleClick(const QModelIndex &curRight);
|
||||
void actOpenRemoteDeck();
|
||||
void openRemoteDeckFinished(const Response &r, const CommandContainer &commandContainer);
|
||||
|
||||
@@ -50,11 +70,11 @@ private slots:
|
||||
void deleteDeckFinished(const Response &response, const CommandContainer &commandContainer);
|
||||
|
||||
public:
|
||||
TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_client);
|
||||
void retranslateUi();
|
||||
QString getTabText() const
|
||||
TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User *currentUserInfo);
|
||||
void retranslateUi() override;
|
||||
QString getTabText() const override
|
||||
{
|
||||
return tr("Deck storage");
|
||||
return tr("Deck Storage");
|
||||
}
|
||||
signals:
|
||||
void openDeckEditor(const DeckLoader *deckLoader);
|
||||
|
||||
@@ -1,43 +1,36 @@
|
||||
#include "tab_game.h"
|
||||
|
||||
#include "../../client/ui/widgets/cards/card_info_frame_widget.h"
|
||||
#include "../../deck/deck_loader.h"
|
||||
#include "../../deck/deck_view.h"
|
||||
#include "../../dialogs/dlg_create_game.h"
|
||||
#include "../../dialogs/dlg_load_remote_deck.h"
|
||||
#include "../../dialogs/dlg_manage_sets.h"
|
||||
#include "../../game/board/arrow_item.h"
|
||||
#include "../../game/cards/card_database.h"
|
||||
#include "../../game/cards/card_database_manager.h"
|
||||
#include "../../game/cards/card_item.h"
|
||||
#include "../../game/deckview/deck_view_container.h"
|
||||
#include "../../game/game_scene.h"
|
||||
#include "../../game/game_view.h"
|
||||
#include "../../game/player/player.h"
|
||||
#include "../../game/player/player_list_widget.h"
|
||||
#include "../../game/zones/view_zone.h"
|
||||
#include "../../game/zones/view_zone_widget.h"
|
||||
#include "../../game/zones/card_zone.h"
|
||||
#include "../../main.h"
|
||||
#include "../../server/message_log_widget.h"
|
||||
#include "../../server/pending_command.h"
|
||||
#include "../../server/user/user_list_manager.h"
|
||||
#include "../../settings/cache_settings.h"
|
||||
#include "../game_logic/abstract_client.h"
|
||||
#include "../network/replay_timeline_widget.h"
|
||||
#include "../ui/line_edit_completer.h"
|
||||
#include "../ui/phases_toolbar.h"
|
||||
#include "../ui/picture_loader.h"
|
||||
#include "../ui/picture_loader/picture_loader.h"
|
||||
#include "../ui/window_main.h"
|
||||
#include "get_pb_extension.h"
|
||||
#include "pb/command_concede.pb.h"
|
||||
#include "pb/command_deck_select.pb.h"
|
||||
#include "pb/command_delete_arrow.pb.h"
|
||||
#include "pb/command_game_say.pb.h"
|
||||
#include "pb/command_leave_game.pb.h"
|
||||
#include "pb/command_next_turn.pb.h"
|
||||
#include "pb/command_ready_start.pb.h"
|
||||
#include "pb/command_reverse_turn.pb.h"
|
||||
#include "pb/command_set_active_phase.pb.h"
|
||||
#include "pb/command_set_sideboard_lock.pb.h"
|
||||
#include "pb/command_set_sideboard_plan.pb.h"
|
||||
#include "pb/context_connection_state_changed.pb.h"
|
||||
#include "pb/context_deck_select.pb.h"
|
||||
#include "pb/context_ping_changed.pb.h"
|
||||
@@ -55,7 +48,6 @@
|
||||
#include "pb/event_set_active_player.pb.h"
|
||||
#include "pb/game_event_container.pb.h"
|
||||
#include "pb/game_replay.pb.h"
|
||||
#include "pb/response_deck_download.pb.h"
|
||||
#include "tab_supervisor.h"
|
||||
#include "trice_limits.h"
|
||||
|
||||
@@ -73,110 +65,239 @@
|
||||
#include <QTimer>
|
||||
#include <QToolButton>
|
||||
#include <QWidget>
|
||||
#include <google/protobuf/descriptor.h>
|
||||
|
||||
ToggleButton::ToggleButton(QWidget *parent) : QPushButton(parent), state(false)
|
||||
TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay)
|
||||
: Tab(_tabSupervisor), secondsElapsed(0), hostId(-1), localPlayerId(-1),
|
||||
isLocalGame(_tabSupervisor->getIsLocalGame()), spectator(true), judge(false), gameStateKnown(false),
|
||||
resuming(false), currentPhase(-1), activeCard(nullptr), gameClosed(false), replay(_replay), currentReplayStep(0),
|
||||
sayLabel(nullptr), sayEdit(nullptr)
|
||||
{
|
||||
}
|
||||
// THIS CTOR IS USED ON REPLAY
|
||||
gameInfo.CopyFrom(replay->game_info());
|
||||
gameInfo.set_spectators_omniscient(true);
|
||||
|
||||
void ToggleButton::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
QPushButton::paintEvent(event);
|
||||
// Create list: event number -> time [ms]
|
||||
// Distribute simultaneous events evenly across 1 second.
|
||||
unsigned int lastEventTimestamp = 0;
|
||||
const int eventCount = replay->event_list_size();
|
||||
for (int i = 0; i < eventCount; ++i) {
|
||||
int j = i + 1;
|
||||
while ((j < eventCount) && (replay->event_list(j).seconds_elapsed() == lastEventTimestamp))
|
||||
++j;
|
||||
|
||||
QPainter painter(this);
|
||||
QPen pen;
|
||||
pen.setWidth(3);
|
||||
pen.setJoinStyle(Qt::MiterJoin);
|
||||
pen.setColor(state ? Qt::green : Qt::red);
|
||||
painter.setPen(pen);
|
||||
painter.drawRect(QRect(1, 1, width() - 3, height() - 3));
|
||||
}
|
||||
const int numberEventsThisSecond = j - i;
|
||||
for (int k = 0; k < numberEventsThisSecond; ++k)
|
||||
replayTimeline.append(replay->event_list(i + k).seconds_elapsed() * 1000 +
|
||||
(int)((qreal)k / (qreal)numberEventsThisSecond * 1000));
|
||||
|
||||
void ToggleButton::setState(bool _state)
|
||||
{
|
||||
state = _state;
|
||||
emit stateChanged();
|
||||
update();
|
||||
}
|
||||
|
||||
DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
||||
: QWidget(nullptr), parentGame(parent), playerId(_playerId)
|
||||
{
|
||||
loadLocalButton = new QPushButton;
|
||||
loadRemoteButton = new QPushButton;
|
||||
readyStartButton = new ToggleButton;
|
||||
readyStartButton->setEnabled(false);
|
||||
sideboardLockButton = new ToggleButton;
|
||||
sideboardLockButton->setEnabled(false);
|
||||
|
||||
connect(loadLocalButton, SIGNAL(clicked()), this, SLOT(loadLocalDeck()));
|
||||
connect(readyStartButton, SIGNAL(clicked()), this, SLOT(readyStart()));
|
||||
connect(sideboardLockButton, SIGNAL(clicked()), this, SLOT(sideboardLockButtonClicked()));
|
||||
connect(sideboardLockButton, SIGNAL(stateChanged()), this, SLOT(updateSideboardLockButtonText()));
|
||||
|
||||
if (parentGame->getIsLocalGame()) {
|
||||
loadRemoteButton->setEnabled(false);
|
||||
} else {
|
||||
connect(loadRemoteButton, SIGNAL(clicked()), this, SLOT(loadRemoteDeck()));
|
||||
if (j < eventCount)
|
||||
lastEventTimestamp = replay->event_list(j).seconds_elapsed();
|
||||
i += numberEventsThisSecond - 1;
|
||||
}
|
||||
|
||||
auto *buttonHBox = new QHBoxLayout;
|
||||
buttonHBox->addWidget(loadLocalButton);
|
||||
buttonHBox->addWidget(loadRemoteButton);
|
||||
buttonHBox->addWidget(readyStartButton);
|
||||
buttonHBox->addWidget(sideboardLockButton);
|
||||
buttonHBox->setContentsMargins(0, 0, 0, 0);
|
||||
buttonHBox->addStretch();
|
||||
deckView = new DeckView;
|
||||
connect(deckView, SIGNAL(newCardAdded(AbstractCardItem *)), this, SIGNAL(newCardAdded(AbstractCardItem *)));
|
||||
connect(deckView, SIGNAL(sideboardPlanChanged()), this, SLOT(sideboardPlanChanged()));
|
||||
createCardInfoDock(true);
|
||||
createPlayerListDock(true);
|
||||
createMessageDock(true);
|
||||
createPlayAreaWidget(true);
|
||||
createDeckViewContainerWidget(true);
|
||||
createReplayDock();
|
||||
|
||||
auto *deckViewLayout = new QVBoxLayout;
|
||||
deckViewLayout->addLayout(buttonHBox);
|
||||
deckViewLayout->addWidget(deckView);
|
||||
deckViewLayout->setContentsMargins(0, 0, 0, 0);
|
||||
setLayout(deckViewLayout);
|
||||
addDockWidget(Qt::RightDockWidgetArea, cardInfoDock);
|
||||
addDockWidget(Qt::RightDockWidgetArea, playerListDock);
|
||||
addDockWidget(Qt::RightDockWidgetArea, messageLayoutDock);
|
||||
addDockWidget(Qt::BottomDockWidgetArea, replayDock);
|
||||
|
||||
mainWidget = new QStackedWidget(this);
|
||||
mainWidget->addWidget(deckViewContainerWidget);
|
||||
mainWidget->addWidget(gamePlayAreaWidget);
|
||||
setCentralWidget(mainWidget);
|
||||
|
||||
createReplayMenuItems();
|
||||
createViewMenuItems();
|
||||
retranslateUi();
|
||||
connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts()));
|
||||
refreshShortcuts();
|
||||
messageLog->logReplayStarted(gameInfo.game_id());
|
||||
|
||||
this->installEventFilter(this);
|
||||
QTimer::singleShot(0, this, SLOT(loadLayout()));
|
||||
}
|
||||
|
||||
void DeckViewContainer::retranslateUi()
|
||||
TabGame::TabGame(TabSupervisor *_tabSupervisor,
|
||||
QList<AbstractClient *> &_clients,
|
||||
const Event_GameJoined &event,
|
||||
const QMap<int, QString> &_roomGameTypes)
|
||||
: Tab(_tabSupervisor), userListProxy(_tabSupervisor->getUserListManager()), clients(_clients),
|
||||
gameInfo(event.game_info()), roomGameTypes(_roomGameTypes), hostId(event.host_id()),
|
||||
localPlayerId(event.player_id()), isLocalGame(_tabSupervisor->getIsLocalGame()), spectator(event.spectator()),
|
||||
judge(event.judge()), gameStateKnown(false), resuming(event.resuming()), currentPhase(-1), activeCard(nullptr),
|
||||
gameClosed(false), replay(nullptr), replayPlayButton(nullptr), replayFastForwardButton(nullptr),
|
||||
aReplaySkipForward(nullptr), aReplaySkipBackward(nullptr), aReplaySkipForwardBig(nullptr),
|
||||
aReplaySkipBackwardBig(nullptr), replayDock(nullptr)
|
||||
{
|
||||
loadLocalButton->setText(tr("Load deck..."));
|
||||
loadRemoteButton->setText(tr("Load remote deck..."));
|
||||
readyStartButton->setText(tr("Ready to start"));
|
||||
updateSideboardLockButtonText();
|
||||
// THIS CTOR IS USED ON GAMES
|
||||
gameInfo.set_started(false);
|
||||
|
||||
createCardInfoDock();
|
||||
createPlayerListDock();
|
||||
createMessageDock();
|
||||
createPlayAreaWidget();
|
||||
createDeckViewContainerWidget();
|
||||
|
||||
addDockWidget(Qt::RightDockWidgetArea, cardInfoDock);
|
||||
addDockWidget(Qt::RightDockWidgetArea, playerListDock);
|
||||
addDockWidget(Qt::RightDockWidgetArea, messageLayoutDock);
|
||||
|
||||
mainWidget = new QStackedWidget(this);
|
||||
mainWidget->addWidget(deckViewContainerWidget);
|
||||
mainWidget->addWidget(gamePlayAreaWidget);
|
||||
mainWidget->setContentsMargins(0, 0, 0, 0);
|
||||
setCentralWidget(mainWidget);
|
||||
|
||||
createMenuItems();
|
||||
createViewMenuItems();
|
||||
retranslateUi();
|
||||
connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts()));
|
||||
refreshShortcuts();
|
||||
|
||||
// append game to rooms game list for others to see
|
||||
for (int i = gameInfo.game_types_size() - 1; i >= 0; i--)
|
||||
gameTypes.append(roomGameTypes.find(gameInfo.game_types(i)).value());
|
||||
|
||||
this->installEventFilter(this);
|
||||
QTimer::singleShot(0, this, SLOT(loadLayout()));
|
||||
}
|
||||
|
||||
void DeckViewContainer::setButtonsVisible(bool _visible)
|
||||
void TabGame::addMentionTag(const QString &value)
|
||||
{
|
||||
loadLocalButton->setVisible(_visible);
|
||||
loadRemoteButton->setVisible(_visible);
|
||||
readyStartButton->setVisible(_visible);
|
||||
sideboardLockButton->setVisible(_visible);
|
||||
sayEdit->insert(value + " ");
|
||||
sayEdit->setFocus();
|
||||
}
|
||||
|
||||
void DeckViewContainer::updateSideboardLockButtonText()
|
||||
void TabGame::linkCardToChat(const QString &cardName)
|
||||
{
|
||||
if (sideboardLockButton->getState()) {
|
||||
sideboardLockButton->setText(tr("Sideboard unlocked"));
|
||||
} else {
|
||||
sideboardLockButton->setText(tr("Sideboard locked"));
|
||||
sayEdit->insert("[[" + cardName + "]] ");
|
||||
sayEdit->setFocus();
|
||||
}
|
||||
|
||||
void TabGame::emitUserEvent()
|
||||
{
|
||||
bool globalEvent = !spectator || SettingsCache::instance().getSpectatorNotificationsEnabled();
|
||||
emit userEvent(globalEvent);
|
||||
updatePlayerListDockTitle();
|
||||
}
|
||||
|
||||
TabGame::~TabGame()
|
||||
{
|
||||
delete replay;
|
||||
}
|
||||
|
||||
void TabGame::updatePlayerListDockTitle()
|
||||
{
|
||||
QString tabText = " | " + (replay ? tr("Replay") : tr("Game")) + " #" + QString::number(gameInfo.game_id());
|
||||
QString userCountInfo = QString(" %1/%2").arg(players.size()).arg(gameInfo.max_players());
|
||||
playerListDock->setWindowTitle(tr("Player List") + userCountInfo +
|
||||
(playerListDock->isWindow() ? tabText : QString()));
|
||||
}
|
||||
|
||||
bool TabGame::isMainPlayerConceded() const
|
||||
{
|
||||
Player *player = players.value(localPlayerId, nullptr);
|
||||
return player && player->getConceded();
|
||||
}
|
||||
|
||||
void TabGame::retranslateUi()
|
||||
{
|
||||
QString tabText = " | " + (replay ? tr("Replay") : tr("Game")) + " #" + QString::number(gameInfo.game_id());
|
||||
|
||||
updatePlayerListDockTitle();
|
||||
cardInfoDock->setWindowTitle(tr("Card Info") + (cardInfoDock->isWindow() ? tabText : QString()));
|
||||
messageLayoutDock->setWindowTitle(tr("Messages") + (messageLayoutDock->isWindow() ? tabText : QString()));
|
||||
if (replayDock)
|
||||
replayDock->setWindowTitle(tr("Replay Timeline") + (replayDock->isWindow() ? tabText : QString()));
|
||||
|
||||
if (phasesMenu) {
|
||||
for (int i = 0; i < phaseActions.size(); ++i)
|
||||
phaseActions[i]->setText(phasesToolbar->getLongPhaseName(i));
|
||||
phasesMenu->setTitle(tr("&Phases"));
|
||||
}
|
||||
// setting text on a button removes its shortcut
|
||||
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
|
||||
sideboardLockButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/sideboardLockButton"));
|
||||
}
|
||||
|
||||
void DeckViewContainer::refreshShortcuts()
|
||||
{
|
||||
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
|
||||
loadLocalButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadLocalButton"));
|
||||
loadRemoteButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadRemoteButton"));
|
||||
readyStartButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/readyStartButton"));
|
||||
sideboardLockButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/sideboardLockButton"));
|
||||
gameMenu->setTitle(tr("&Game"));
|
||||
if (aNextPhase) {
|
||||
aNextPhase->setText(tr("Next &phase"));
|
||||
}
|
||||
if (aNextPhaseAction) {
|
||||
aNextPhaseAction->setText(tr("Next phase with &action"));
|
||||
}
|
||||
if (aNextTurn) {
|
||||
aNextTurn->setText(tr("Next &turn"));
|
||||
}
|
||||
if (aReverseTurn) {
|
||||
aReverseTurn->setText(tr("Reverse turn order"));
|
||||
}
|
||||
if (aRemoveLocalArrows) {
|
||||
aRemoveLocalArrows->setText(tr("&Remove all local arrows"));
|
||||
}
|
||||
if (aRotateViewCW) {
|
||||
aRotateViewCW->setText(tr("Rotate View Cl&ockwise"));
|
||||
}
|
||||
if (aRotateViewCCW) {
|
||||
aRotateViewCCW->setText(tr("Rotate View Co&unterclockwise"));
|
||||
}
|
||||
if (aGameInfo)
|
||||
aGameInfo->setText(tr("Game &information"));
|
||||
if (aConcede) {
|
||||
if (isMainPlayerConceded()) {
|
||||
aConcede->setText(tr("Un&concede"));
|
||||
} else {
|
||||
aConcede->setText(tr("&Concede"));
|
||||
}
|
||||
}
|
||||
if (aLeaveGame) {
|
||||
aLeaveGame->setText(tr("&Leave game"));
|
||||
}
|
||||
if (aCloseReplay) {
|
||||
aCloseReplay->setText(tr("C&lose replay"));
|
||||
}
|
||||
if (aFocusChat) {
|
||||
aFocusChat->setText(tr("&Focus Chat"));
|
||||
}
|
||||
if (sayLabel) {
|
||||
sayLabel->setText(tr("&Say:"));
|
||||
}
|
||||
|
||||
viewMenu->setTitle(tr("&View"));
|
||||
cardInfoDockMenu->setTitle(tr("Card Info"));
|
||||
messageLayoutDockMenu->setTitle(tr("Messages"));
|
||||
playerListDockMenu->setTitle(tr("Player List"));
|
||||
|
||||
aCardInfoDockVisible->setText(tr("Visible"));
|
||||
aCardInfoDockFloating->setText(tr("Floating"));
|
||||
|
||||
aMessageLayoutDockVisible->setText(tr("Visible"));
|
||||
aMessageLayoutDockFloating->setText(tr("Floating"));
|
||||
|
||||
aPlayerListDockVisible->setText(tr("Visible"));
|
||||
aPlayerListDockFloating->setText(tr("Floating"));
|
||||
|
||||
if (replayDock) {
|
||||
replayDockMenu->setTitle(tr("Replay Timeline"));
|
||||
aReplayDockVisible->setText(tr("Visible"));
|
||||
aReplayDockFloating->setText(tr("Floating"));
|
||||
}
|
||||
|
||||
aResetLayout->setText(tr("Reset layout"));
|
||||
|
||||
cardInfoFrameWidget->retranslateUi();
|
||||
|
||||
QMapIterator<int, Player *> i(players);
|
||||
while (i.hasNext())
|
||||
i.next().value()->retranslateUi();
|
||||
QMapIterator<int, DeckViewContainer *> j(deckViewContainers);
|
||||
while (j.hasNext())
|
||||
j.next().value()->retranslateUi();
|
||||
|
||||
scene->retranslateUi();
|
||||
}
|
||||
|
||||
void TabGame::refreshShortcuts()
|
||||
@@ -278,341 +399,15 @@ void TabGame::refreshShortcuts()
|
||||
}
|
||||
}
|
||||
|
||||
void DeckViewContainer::loadLocalDeck()
|
||||
void TabGame::closeRequest(bool forced)
|
||||
{
|
||||
QFileDialog dialog(this, tr("Load deck"));
|
||||
dialog.setDirectory(SettingsCache::instance().getDeckPath());
|
||||
dialog.setNameFilters(DeckLoader::fileNameFilters);
|
||||
if (!dialog.exec())
|
||||
return;
|
||||
|
||||
QString fileName = dialog.selectedFiles().at(0);
|
||||
DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(fileName);
|
||||
QString deckString;
|
||||
DeckLoader deck;
|
||||
|
||||
bool error = !deck.loadFromFile(fileName, fmt);
|
||||
if (!error) {
|
||||
deckString = deck.writeToString_Native();
|
||||
error = deckString.length() > MAX_FILE_LENGTH;
|
||||
}
|
||||
if (error) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("The selected file could not be loaded."));
|
||||
if (!forced && !leaveGame()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Command_DeckSelect cmd;
|
||||
cmd.set_deck(deckString.toStdString());
|
||||
PendingCommand *pend = parentGame->prepareGameCommand(cmd);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(deckSelectFinished(const Response &)));
|
||||
parentGame->sendGameCommand(pend, playerId);
|
||||
}
|
||||
|
||||
void DeckViewContainer::loadRemoteDeck()
|
||||
{
|
||||
DlgLoadRemoteDeck dlg(parentGame->getClientForPlayer(playerId), this);
|
||||
if (dlg.exec()) {
|
||||
Command_DeckSelect cmd;
|
||||
cmd.set_deck_id(dlg.getDeckId());
|
||||
PendingCommand *pend = parentGame->prepareGameCommand(cmd);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(deckSelectFinished(const Response &)));
|
||||
parentGame->sendGameCommand(pend, playerId);
|
||||
}
|
||||
}
|
||||
|
||||
void DeckViewContainer::deckSelectFinished(const Response &r)
|
||||
{
|
||||
const Response_DeckDownload &resp = r.GetExtension(Response_DeckDownload::ext);
|
||||
DeckLoader newDeck(QString::fromStdString(resp.deck()));
|
||||
// TODO CHANGE THIS TO BE SELECTED BY UUID
|
||||
PictureLoader::cacheCardPixmaps(CardDatabaseManager::getInstance()->getCards(newDeck.getCardList()));
|
||||
setDeck(newDeck);
|
||||
}
|
||||
|
||||
void DeckViewContainer::readyStart()
|
||||
{
|
||||
Command_ReadyStart cmd;
|
||||
cmd.set_ready(!readyStartButton->getState());
|
||||
parentGame->sendGameCommand(cmd, playerId);
|
||||
}
|
||||
|
||||
void DeckViewContainer::sideboardLockButtonClicked()
|
||||
{
|
||||
Command_SetSideboardLock cmd;
|
||||
cmd.set_locked(sideboardLockButton->getState());
|
||||
|
||||
parentGame->sendGameCommand(cmd, playerId);
|
||||
}
|
||||
|
||||
void DeckViewContainer::sideboardPlanChanged()
|
||||
{
|
||||
Command_SetSideboardPlan cmd;
|
||||
const QList<MoveCard_ToZone> &newPlan = deckView->getSideboardPlan();
|
||||
for (const auto &i : newPlan)
|
||||
cmd.add_move_list()->CopyFrom(i);
|
||||
parentGame->sendGameCommand(cmd, playerId);
|
||||
}
|
||||
|
||||
void DeckViewContainer::setReadyStart(bool ready)
|
||||
{
|
||||
readyStartButton->setState(ready);
|
||||
deckView->setLocked(ready || !sideboardLockButton->getState());
|
||||
sideboardLockButton->setEnabled(!readyStartButton->getState() && readyStartButton->isEnabled());
|
||||
}
|
||||
|
||||
void DeckViewContainer::setSideboardLocked(bool locked)
|
||||
{
|
||||
sideboardLockButton->setState(!locked);
|
||||
deckView->setLocked(readyStartButton->getState() || !sideboardLockButton->getState());
|
||||
if (locked)
|
||||
deckView->resetSideboardPlan();
|
||||
}
|
||||
|
||||
void DeckViewContainer::setDeck(const DeckLoader &deck)
|
||||
{
|
||||
deckView->setDeck(deck);
|
||||
readyStartButton->setEnabled(true);
|
||||
sideboardLockButton->setState(false);
|
||||
sideboardLockButton->setEnabled(true);
|
||||
}
|
||||
|
||||
TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay)
|
||||
: Tab(_tabSupervisor), secondsElapsed(0), hostId(-1), localPlayerId(-1),
|
||||
isLocalGame(_tabSupervisor->getIsLocalGame()), spectator(true), judge(false), gameStateKnown(false),
|
||||
resuming(false), currentPhase(-1), activeCard(nullptr), gameClosed(false), replay(_replay), currentReplayStep(0),
|
||||
sayLabel(nullptr), sayEdit(nullptr)
|
||||
{
|
||||
// THIS CTOR IS USED ON REPLAY
|
||||
gameInfo.CopyFrom(replay->game_info());
|
||||
gameInfo.set_spectators_omniscient(true);
|
||||
|
||||
// Create list: event number -> time [ms]
|
||||
// Distribute simultaneous events evenly across 1 second.
|
||||
unsigned int lastEventTimestamp = 0;
|
||||
const int eventCount = replay->event_list_size();
|
||||
for (int i = 0; i < eventCount; ++i) {
|
||||
int j = i + 1;
|
||||
while ((j < eventCount) && (replay->event_list(j).seconds_elapsed() == lastEventTimestamp))
|
||||
++j;
|
||||
|
||||
const int numberEventsThisSecond = j - i;
|
||||
for (int k = 0; k < numberEventsThisSecond; ++k)
|
||||
replayTimeline.append(replay->event_list(i + k).seconds_elapsed() * 1000 +
|
||||
(int)((qreal)k / (qreal)numberEventsThisSecond * 1000));
|
||||
|
||||
if (j < eventCount)
|
||||
lastEventTimestamp = replay->event_list(j).seconds_elapsed();
|
||||
i += numberEventsThisSecond - 1;
|
||||
}
|
||||
|
||||
createCardInfoDock(true);
|
||||
createPlayerListDock(true);
|
||||
createMessageDock(true);
|
||||
createPlayAreaWidget(true);
|
||||
createDeckViewContainerWidget(true);
|
||||
createReplayDock();
|
||||
|
||||
addDockWidget(Qt::RightDockWidgetArea, cardInfoDock);
|
||||
addDockWidget(Qt::RightDockWidgetArea, playerListDock);
|
||||
addDockWidget(Qt::RightDockWidgetArea, messageLayoutDock);
|
||||
addDockWidget(Qt::BottomDockWidgetArea, replayDock);
|
||||
|
||||
mainWidget = new QStackedWidget(this);
|
||||
mainWidget->addWidget(deckViewContainerWidget);
|
||||
mainWidget->addWidget(gamePlayAreaWidget);
|
||||
setCentralWidget(mainWidget);
|
||||
|
||||
createReplayMenuItems();
|
||||
createViewMenuItems();
|
||||
retranslateUi();
|
||||
connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts()));
|
||||
refreshShortcuts();
|
||||
messageLog->logReplayStarted(gameInfo.game_id());
|
||||
|
||||
this->installEventFilter(this);
|
||||
QTimer::singleShot(0, this, SLOT(loadLayout()));
|
||||
}
|
||||
|
||||
TabGame::TabGame(TabSupervisor *_tabSupervisor,
|
||||
QList<AbstractClient *> &_clients,
|
||||
const Event_GameJoined &event,
|
||||
const QMap<int, QString> &_roomGameTypes)
|
||||
: Tab(_tabSupervisor), clients(_clients), gameInfo(event.game_info()), roomGameTypes(_roomGameTypes),
|
||||
hostId(event.host_id()), localPlayerId(event.player_id()), isLocalGame(_tabSupervisor->getIsLocalGame()),
|
||||
spectator(event.spectator()), judge(event.judge()), gameStateKnown(false), resuming(event.resuming()),
|
||||
currentPhase(-1), activeCard(nullptr), gameClosed(false), replay(nullptr), replayPlayButton(nullptr),
|
||||
replayFastForwardButton(nullptr), aReplaySkipForward(nullptr), aReplaySkipBackward(nullptr),
|
||||
aReplaySkipForwardBig(nullptr), aReplaySkipBackwardBig(nullptr), replayDock(nullptr)
|
||||
{
|
||||
// THIS CTOR IS USED ON GAMES
|
||||
gameInfo.set_started(false);
|
||||
|
||||
createCardInfoDock();
|
||||
createPlayerListDock();
|
||||
createMessageDock();
|
||||
createPlayAreaWidget();
|
||||
createDeckViewContainerWidget();
|
||||
|
||||
addDockWidget(Qt::RightDockWidgetArea, cardInfoDock);
|
||||
addDockWidget(Qt::RightDockWidgetArea, playerListDock);
|
||||
addDockWidget(Qt::RightDockWidgetArea, messageLayoutDock);
|
||||
|
||||
mainWidget = new QStackedWidget(this);
|
||||
mainWidget->addWidget(deckViewContainerWidget);
|
||||
mainWidget->addWidget(gamePlayAreaWidget);
|
||||
mainWidget->setContentsMargins(0, 0, 0, 0);
|
||||
setCentralWidget(mainWidget);
|
||||
|
||||
createMenuItems();
|
||||
createViewMenuItems();
|
||||
retranslateUi();
|
||||
connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts()));
|
||||
refreshShortcuts();
|
||||
|
||||
// append game to rooms game list for others to see
|
||||
for (int i = gameInfo.game_types_size() - 1; i >= 0; i--)
|
||||
gameTypes.append(roomGameTypes.find(gameInfo.game_types(i)).value());
|
||||
|
||||
this->installEventFilter(this);
|
||||
QTimer::singleShot(0, this, SLOT(loadLayout()));
|
||||
}
|
||||
|
||||
void TabGame::addMentionTag(const QString &value)
|
||||
{
|
||||
sayEdit->insert(value + " ");
|
||||
sayEdit->setFocus();
|
||||
}
|
||||
|
||||
void TabGame::linkCardToChat(const QString &cardName)
|
||||
{
|
||||
sayEdit->insert("[[" + cardName + "]] ");
|
||||
sayEdit->setFocus();
|
||||
}
|
||||
|
||||
void TabGame::emitUserEvent()
|
||||
{
|
||||
bool globalEvent = !spectator || SettingsCache::instance().getSpectatorNotificationsEnabled();
|
||||
emit userEvent(globalEvent);
|
||||
updatePlayerListDockTitle();
|
||||
}
|
||||
|
||||
TabGame::~TabGame()
|
||||
{
|
||||
delete replay;
|
||||
|
||||
QMapIterator<int, Player *> i(players);
|
||||
while (i.hasNext()) {
|
||||
delete i.next().value();
|
||||
}
|
||||
|
||||
players.clear();
|
||||
|
||||
emit gameClosing(this);
|
||||
}
|
||||
|
||||
void TabGame::updatePlayerListDockTitle()
|
||||
{
|
||||
QString tabText = " | " + (replay ? tr("Replay") : tr("Game")) + " #" + QString::number(gameInfo.game_id());
|
||||
QString userCountInfo = QString(" %1/%2").arg(players.size()).arg(gameInfo.max_players());
|
||||
playerListDock->setWindowTitle(tr("Player List") + userCountInfo +
|
||||
(playerListDock->isWindow() ? tabText : QString()));
|
||||
}
|
||||
|
||||
void TabGame::retranslateUi()
|
||||
{
|
||||
QString tabText = " | " + (replay ? tr("Replay") : tr("Game")) + " #" + QString::number(gameInfo.game_id());
|
||||
|
||||
updatePlayerListDockTitle();
|
||||
cardInfoDock->setWindowTitle(tr("Card Info") + (cardInfoDock->isWindow() ? tabText : QString()));
|
||||
messageLayoutDock->setWindowTitle(tr("Messages") + (messageLayoutDock->isWindow() ? tabText : QString()));
|
||||
if (replayDock)
|
||||
replayDock->setWindowTitle(tr("Replay Timeline") + (replayDock->isWindow() ? tabText : QString()));
|
||||
|
||||
if (phasesMenu) {
|
||||
for (int i = 0; i < phaseActions.size(); ++i)
|
||||
phaseActions[i]->setText(phasesToolbar->getLongPhaseName(i));
|
||||
phasesMenu->setTitle(tr("&Phases"));
|
||||
}
|
||||
|
||||
gameMenu->setTitle(tr("&Game"));
|
||||
if (aNextPhase) {
|
||||
aNextPhase->setText(tr("Next &phase"));
|
||||
}
|
||||
if (aNextPhaseAction) {
|
||||
aNextPhaseAction->setText(tr("Next phase with &action"));
|
||||
}
|
||||
if (aNextTurn) {
|
||||
aNextTurn->setText(tr("Next &turn"));
|
||||
}
|
||||
if (aReverseTurn) {
|
||||
aReverseTurn->setText(tr("Reverse turn order"));
|
||||
}
|
||||
if (aRemoveLocalArrows) {
|
||||
aRemoveLocalArrows->setText(tr("&Remove all local arrows"));
|
||||
}
|
||||
if (aRotateViewCW) {
|
||||
aRotateViewCW->setText(tr("Rotate View Cl&ockwise"));
|
||||
}
|
||||
if (aRotateViewCCW) {
|
||||
aRotateViewCCW->setText(tr("Rotate View Co&unterclockwise"));
|
||||
}
|
||||
if (aGameInfo)
|
||||
aGameInfo->setText(tr("Game &information"));
|
||||
if (aConcede) {
|
||||
aConcede->setText(tr("&Concede"));
|
||||
}
|
||||
if (aLeaveGame) {
|
||||
aLeaveGame->setText(tr("&Leave game"));
|
||||
}
|
||||
if (aCloseReplay) {
|
||||
aCloseReplay->setText(tr("C&lose replay"));
|
||||
}
|
||||
if (aFocusChat) {
|
||||
aFocusChat->setText(tr("&Focus Chat"));
|
||||
}
|
||||
if (sayLabel) {
|
||||
sayLabel->setText(tr("&Say:"));
|
||||
}
|
||||
|
||||
viewMenu->setTitle(tr("&View"));
|
||||
cardInfoDockMenu->setTitle(tr("Card Info"));
|
||||
messageLayoutDockMenu->setTitle(tr("Messages"));
|
||||
playerListDockMenu->setTitle(tr("Player List"));
|
||||
|
||||
aCardInfoDockVisible->setText(tr("Visible"));
|
||||
aCardInfoDockFloating->setText(tr("Floating"));
|
||||
|
||||
aMessageLayoutDockVisible->setText(tr("Visible"));
|
||||
aMessageLayoutDockFloating->setText(tr("Floating"));
|
||||
|
||||
aPlayerListDockVisible->setText(tr("Visible"));
|
||||
aPlayerListDockFloating->setText(tr("Floating"));
|
||||
|
||||
if (replayDock) {
|
||||
replayDockMenu->setTitle(tr("Replay Timeline"));
|
||||
aReplayDockVisible->setText(tr("Visible"));
|
||||
aReplayDockFloating->setText(tr("Floating"));
|
||||
}
|
||||
|
||||
aResetLayout->setText(tr("Reset layout"));
|
||||
|
||||
cardInfoFrameWidget->retranslateUi();
|
||||
|
||||
QMapIterator<int, Player *> i(players);
|
||||
while (i.hasNext())
|
||||
i.next().value()->retranslateUi();
|
||||
QMapIterator<int, DeckViewContainer *> j(deckViewContainers);
|
||||
while (j.hasNext())
|
||||
j.next().value()->retranslateUi();
|
||||
|
||||
scene->retranslateUi();
|
||||
}
|
||||
|
||||
void TabGame::closeRequest()
|
||||
{
|
||||
actLeaveGame();
|
||||
close();
|
||||
}
|
||||
|
||||
void TabGame::replayNextEvent(Player::EventProcessingOptions options)
|
||||
@@ -703,19 +498,23 @@ void TabGame::actConcede()
|
||||
}
|
||||
}
|
||||
|
||||
void TabGame::actLeaveGame()
|
||||
/**
|
||||
* Confirms the leave game and sends the leave game command, if applicable.
|
||||
*
|
||||
* @return True if the leave game is confirmed
|
||||
*/
|
||||
bool TabGame::leaveGame()
|
||||
{
|
||||
if (!gameClosed) {
|
||||
if (!spectator)
|
||||
if (QMessageBox::question(this, tr("Leave game"), tr("Are you sure you want to leave this game?"),
|
||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes)
|
||||
return;
|
||||
return false;
|
||||
|
||||
if (!replay)
|
||||
sendGameCommand(Command_LeaveGame());
|
||||
}
|
||||
scene->clearViews();
|
||||
deleteLater();
|
||||
return true;
|
||||
}
|
||||
|
||||
void TabGame::actSay()
|
||||
@@ -723,6 +522,12 @@ void TabGame::actSay()
|
||||
if (completer->popup()->isVisible())
|
||||
return;
|
||||
|
||||
if (sayEdit->text().startsWith("/card ")) {
|
||||
cardInfoFrameWidget->setCard(sayEdit->text().mid(6));
|
||||
sayEdit->clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sayEdit->text().isEmpty()) {
|
||||
Command_GameSay cmd;
|
||||
cmd.set_message(sayEdit->text().toStdString());
|
||||
@@ -834,6 +639,15 @@ Player *TabGame::addPlayer(int playerId, const ServerInfo_User &info)
|
||||
connect(deckView, SIGNAL(newCardAdded(AbstractCardItem *)), this, SLOT(newCardAdded(AbstractCardItem *)));
|
||||
deckViewContainers.insert(playerId, deckView);
|
||||
deckViewContainerLayout->addWidget(deckView);
|
||||
|
||||
// auto load deck for player if that debug setting is enabled
|
||||
QString deckPath = SettingsCache::instance().debug().getDeckPathForPlayer(newPlayer->getName());
|
||||
if (!deckPath.isEmpty()) {
|
||||
QTimer::singleShot(0, this, [deckView, deckPath] {
|
||||
deckView->loadDeckFromFile(deckPath);
|
||||
deckView->readyAndUpdate();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
gameMenu->insertMenu(playersSeparator, newPlayer->getPlayerMenu());
|
||||
@@ -858,6 +672,9 @@ Player *TabGame::addPlayer(int playerId, const ServerInfo_User &info)
|
||||
}
|
||||
}
|
||||
|
||||
// update menu text when player concedes so that "concede" gets updated to "unconcede"
|
||||
connect(newPlayer, &Player::playerCountChanged, this, &TabGame::retranslateUi);
|
||||
|
||||
emit playerAdded(newPlayer);
|
||||
return newPlayer;
|
||||
}
|
||||
@@ -936,7 +753,7 @@ void TabGame::processGameEventContainer(const GameEventContainer &cont,
|
||||
default: {
|
||||
Player *player = players.value(playerId, 0);
|
||||
if (!player) {
|
||||
qDebug() << "unhandled game event: invalid player id";
|
||||
qCDebug(TabGameLog) << "unhandled game event: invalid player id";
|
||||
break;
|
||||
}
|
||||
player->processGameEvent(eventType, event, context, options);
|
||||
@@ -1065,8 +882,7 @@ void TabGame::closeGame()
|
||||
void TabGame::eventSpectatorSay(const Event_GameSay &event, int eventPlayerId, const GameEventContext & /*context*/)
|
||||
{
|
||||
const ServerInfo_User &userInfo = spectators.value(eventPlayerId);
|
||||
messageLog->logSpectatorSay(QString::fromStdString(userInfo.name()), UserLevelFlags(userInfo.user_level()),
|
||||
QString::fromStdString(userInfo.privlevel()), QString::fromStdString(event.message()));
|
||||
messageLog->logSpectatorSay(userInfo, QString::fromStdString(event.message()));
|
||||
}
|
||||
|
||||
void TabGame::eventSpectatorLeave(const Event_Leave &event, int eventPlayerId, const GameEventContext & /*context*/)
|
||||
@@ -1273,6 +1089,7 @@ void TabGame::eventLeave(const Event_Leave &event, int eventPlayerId, const Game
|
||||
players.remove(eventPlayerId);
|
||||
emit playerRemoved(player);
|
||||
player->clear();
|
||||
scene->removePlayer(player);
|
||||
player->deleteLater();
|
||||
|
||||
// Rearrange all remaining zones so that attachment relationship updates take place
|
||||
@@ -1381,7 +1198,7 @@ void TabGame::eventSetActivePhase(const Event_SetActivePhase &event,
|
||||
void TabGame::newCardAdded(AbstractCardItem *card)
|
||||
{
|
||||
connect(card, SIGNAL(hovered(AbstractCardItem *)), cardInfoFrameWidget, SLOT(setCard(AbstractCardItem *)));
|
||||
connect(card, SIGNAL(showCardInfoPopup(QPoint, QString)), this, SLOT(showCardInfoPopup(QPoint, QString)));
|
||||
connect(card, &AbstractCardItem::showCardInfoPopup, this, &TabGame::showCardInfoPopup);
|
||||
connect(card, SIGNAL(deleteCardInfoPopup(QString)), this, SLOT(deleteCardInfoPopup(QString)));
|
||||
connect(card, SIGNAL(cardShiftClicked(QString)), this, SLOT(linkCardToChat(QString)));
|
||||
}
|
||||
@@ -1489,7 +1306,7 @@ void TabGame::createMenuItems()
|
||||
aConcede = new QAction(this);
|
||||
connect(aConcede, SIGNAL(triggered()), this, SLOT(actConcede()));
|
||||
aLeaveGame = new QAction(this);
|
||||
connect(aLeaveGame, SIGNAL(triggered()), this, SLOT(actLeaveGame()));
|
||||
connect(aLeaveGame, &QAction::triggered, this, [this] { closeRequest(); });
|
||||
aFocusChat = new QAction(this);
|
||||
connect(aFocusChat, SIGNAL(triggered()), sayEdit, SLOT(setFocus()));
|
||||
aCloseReplay = nullptr;
|
||||
@@ -1539,7 +1356,7 @@ void TabGame::createReplayMenuItems()
|
||||
aFocusChat = nullptr;
|
||||
aLeaveGame = nullptr;
|
||||
aCloseReplay = new QAction(this);
|
||||
connect(aCloseReplay, SIGNAL(triggered()), this, SLOT(actLeaveGame()));
|
||||
connect(aCloseReplay, &QAction::triggered, this, [this] { closeRequest(); });
|
||||
|
||||
phasesMenu = nullptr;
|
||||
gameMenu = new QMenu(this);
|
||||
@@ -1861,9 +1678,9 @@ void TabGame::createPlayerListDock(bool bReplay)
|
||||
|
||||
void TabGame::createMessageDock(bool bReplay)
|
||||
{
|
||||
messageLog = new MessageLogWidget(tabSupervisor, tabSupervisor, this);
|
||||
messageLog = new MessageLogWidget(tabSupervisor, this);
|
||||
connect(messageLog, SIGNAL(cardNameHovered(QString)), cardInfoFrameWidget, SLOT(setCard(QString)));
|
||||
connect(messageLog, SIGNAL(showCardInfoPopup(QPoint, QString)), this, SLOT(showCardInfoPopup(QPoint, QString)));
|
||||
connect(messageLog, &MessageLogWidget::showCardInfoPopup, this, &TabGame::showCardInfoPopup);
|
||||
connect(messageLog, SIGNAL(deleteCardInfoPopup(QString)), this, SLOT(deleteCardInfoPopup(QString)));
|
||||
|
||||
if (!bReplay) {
|
||||
|
||||
@@ -3,25 +3,28 @@
|
||||
|
||||
#include "../../client/tearoff_menu.h"
|
||||
#include "../../game/player/player.h"
|
||||
#include "../ui/widgets/visual_deck_storage/visual_deck_storage_widget.h"
|
||||
#include "pb/event_leave.pb.h"
|
||||
#include "pb/serverinfo_game.pb.h"
|
||||
#include "tab.h"
|
||||
|
||||
#include <QCompleter>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMap>
|
||||
#include <QPushButton>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(TabGameLog, "tab_game");
|
||||
|
||||
class UserListProxy;
|
||||
class DeckViewContainer;
|
||||
class AbstractClient;
|
||||
class CardDatabase;
|
||||
class GameView;
|
||||
class DeckView;
|
||||
class GameScene;
|
||||
class CardInfoFrameWidget;
|
||||
class MessageLogWidget;
|
||||
class QTimer;
|
||||
class QSplitter;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class QToolButton;
|
||||
class QMenu;
|
||||
class ZoneViewLayout;
|
||||
@@ -51,7 +54,6 @@ class Event_ReverseTurn;
|
||||
class CardZone;
|
||||
class AbstractCardItem;
|
||||
class CardItem;
|
||||
class TabGame;
|
||||
class DeckLoader;
|
||||
class QVBoxLayout;
|
||||
class QHBoxLayout;
|
||||
@@ -62,63 +64,13 @@ class LineEditCompleter;
|
||||
class QDockWidget;
|
||||
class QStackedWidget;
|
||||
|
||||
class ToggleButton : public QPushButton
|
||||
{
|
||||
Q_OBJECT
|
||||
private:
|
||||
bool state;
|
||||
signals:
|
||||
void stateChanged();
|
||||
|
||||
public:
|
||||
ToggleButton(QWidget *parent = nullptr);
|
||||
bool getState() const
|
||||
{
|
||||
return state;
|
||||
}
|
||||
void setState(bool _state);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event);
|
||||
};
|
||||
|
||||
class DeckViewContainer : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
private:
|
||||
QPushButton *loadLocalButton, *loadRemoteButton;
|
||||
ToggleButton *readyStartButton, *sideboardLockButton;
|
||||
DeckView *deckView;
|
||||
TabGame *parentGame;
|
||||
int playerId;
|
||||
private slots:
|
||||
void loadLocalDeck();
|
||||
void loadRemoteDeck();
|
||||
void readyStart();
|
||||
void deckSelectFinished(const Response &r);
|
||||
void sideboardPlanChanged();
|
||||
void sideboardLockButtonClicked();
|
||||
void updateSideboardLockButtonText();
|
||||
void refreshShortcuts();
|
||||
signals:
|
||||
void newCardAdded(AbstractCardItem *card);
|
||||
void notIdle();
|
||||
|
||||
public:
|
||||
DeckViewContainer(int _playerId, TabGame *parent);
|
||||
void retranslateUi();
|
||||
void setButtonsVisible(bool _visible);
|
||||
void setReadyStart(bool ready);
|
||||
void setSideboardLocked(bool locked);
|
||||
void setDeck(const DeckLoader &deck);
|
||||
};
|
||||
|
||||
class TabGame : public Tab
|
||||
{
|
||||
Q_OBJECT
|
||||
private:
|
||||
QTimer *gameTimer;
|
||||
int secondsElapsed;
|
||||
const UserListProxy *userListProxy;
|
||||
QList<AbstractClient *> clients;
|
||||
ServerInfo_Game gameInfo;
|
||||
QMap<int, QString> roomGameTypes;
|
||||
@@ -176,9 +128,12 @@ private:
|
||||
|
||||
Player *addPlayer(int playerId, const ServerInfo_User &info);
|
||||
|
||||
bool isMainPlayerConceded() const;
|
||||
|
||||
void startGame(bool resuming);
|
||||
void stopGame();
|
||||
void closeGame();
|
||||
bool leaveGame();
|
||||
|
||||
void eventSpectatorSay(const Event_GameSay &event, int eventPlayerId, const GameEventContext &context);
|
||||
void eventSpectatorLeave(const Event_Leave &event, int eventPlayerId, const GameEventContext &context);
|
||||
@@ -232,7 +187,6 @@ private slots:
|
||||
|
||||
void actGameInfo();
|
||||
void actConcede();
|
||||
void actLeaveGame();
|
||||
void actRemoveLocalArrows();
|
||||
void actRotateViewCW();
|
||||
void actRotateViewCCW();
|
||||
@@ -268,7 +222,7 @@ public:
|
||||
~TabGame() override;
|
||||
void retranslateUi() override;
|
||||
void updatePlayerListDockTitle();
|
||||
void closeRequest() override;
|
||||
void closeRequest(bool forced = false) override;
|
||||
const QMap<int, Player *> &getPlayers() const
|
||||
{
|
||||
return players;
|
||||
|
||||
@@ -21,8 +21,7 @@
|
||||
#include <QtGui>
|
||||
#include <QtWidgets>
|
||||
|
||||
TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, QWidget *parent)
|
||||
: Tab(_tabSupervisor, parent), client(_client)
|
||||
TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client)
|
||||
{
|
||||
roomTable = new QTableWidget();
|
||||
roomTable->setColumnCount(6);
|
||||
|
||||
@@ -53,10 +53,10 @@ private slots:
|
||||
void restartLayout();
|
||||
|
||||
public:
|
||||
TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, QWidget *parent = nullptr);
|
||||
~TabLog();
|
||||
void retranslateUi();
|
||||
QString getTabText() const
|
||||
TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client);
|
||||
~TabLog() override;
|
||||
void retranslateUi() override;
|
||||
QString getTabText() const override
|
||||
{
|
||||
return tr("Logs");
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "../../main.h"
|
||||
#include "../../server/chat_view/chat_view.h"
|
||||
#include "../../server/pending_command.h"
|
||||
#include "../../server/user/user_list_manager.h"
|
||||
#include "../../settings/cache_settings.h"
|
||||
#include "../game_logic/abstract_client.h"
|
||||
#include "../sound_engine.h"
|
||||
@@ -25,8 +26,8 @@ TabMessage::TabMessage(TabSupervisor *_tabSupervisor,
|
||||
: Tab(_tabSupervisor), client(_client), ownUserInfo(new ServerInfo_User(_ownUserInfo)),
|
||||
otherUserInfo(new ServerInfo_User(_otherUserInfo)), userOnline(true)
|
||||
{
|
||||
chatView = new ChatView(tabSupervisor, tabSupervisor, 0, true);
|
||||
connect(chatView, SIGNAL(showCardInfoPopup(QPoint, QString)), this, SLOT(showCardInfoPopup(QPoint, QString)));
|
||||
chatView = new ChatView(tabSupervisor, 0, true);
|
||||
connect(chatView, &ChatView::showCardInfoPopup, this, &TabMessage::showCardInfoPopup);
|
||||
connect(chatView, SIGNAL(deleteCardInfoPopup(QString)), this, SLOT(deleteCardInfoPopup(QString)));
|
||||
connect(chatView, SIGNAL(addMentionTag(QString)), this, SLOT(addMentionTag(QString)));
|
||||
sayEdit = new LineEditUnfocusable;
|
||||
@@ -38,7 +39,7 @@ TabMessage::TabMessage(TabSupervisor *_tabSupervisor,
|
||||
vbox->addWidget(sayEdit);
|
||||
|
||||
aLeave = new QAction(this);
|
||||
connect(aLeave, SIGNAL(triggered()), this, SLOT(actLeave()));
|
||||
connect(aLeave, &QAction::triggered, this, [this] { closeRequest(); });
|
||||
|
||||
messageMenu = new QMenu(this);
|
||||
messageMenu->addAction(aLeave);
|
||||
@@ -53,7 +54,6 @@ TabMessage::TabMessage(TabSupervisor *_tabSupervisor,
|
||||
|
||||
TabMessage::~TabMessage()
|
||||
{
|
||||
emit talkClosing(this);
|
||||
delete ownUserInfo;
|
||||
delete otherUserInfo;
|
||||
}
|
||||
@@ -86,9 +86,10 @@ QString TabMessage::getTabText() const
|
||||
return tr("%1 - Private chat").arg(QString::fromStdString(otherUserInfo->name()));
|
||||
}
|
||||
|
||||
void TabMessage::closeRequest()
|
||||
void TabMessage::closeRequest(bool /*forced*/)
|
||||
{
|
||||
actLeave();
|
||||
emit talkClosing(this);
|
||||
close();
|
||||
}
|
||||
|
||||
void TabMessage::sendMessage()
|
||||
@@ -114,19 +115,11 @@ void TabMessage::messageSent(const Response &response)
|
||||
"This user is ignoring you, they cannot see your messages in main chat and you cannot join their games."));
|
||||
}
|
||||
|
||||
void TabMessage::actLeave()
|
||||
{
|
||||
deleteLater();
|
||||
}
|
||||
|
||||
void TabMessage::processUserMessageEvent(const Event_UserMessage &event)
|
||||
{
|
||||
auto userInfo = event.sender_name() == otherUserInfo->name() ? otherUserInfo : ownUserInfo;
|
||||
const UserLevelFlags userLevel(userInfo->user_level());
|
||||
const QString userPriv = QString::fromStdString(userInfo->privlevel());
|
||||
|
||||
chatView->appendMessage(QString::fromStdString(event.message()), {}, QString::fromStdString(event.sender_name()),
|
||||
userLevel, userPriv, true);
|
||||
chatView->appendMessage(QString::fromStdString(event.message()), {}, *userInfo, true);
|
||||
if (tabSupervisor->currentIndex() != tabSupervisor->indexOf(this))
|
||||
soundEngine->playSound("private_message");
|
||||
if (SettingsCache::instance().getShowMessagePopup() && shouldShowSystemPopup(event))
|
||||
@@ -152,7 +145,7 @@ void TabMessage::showSystemPopup(const Event_UserMessage &event)
|
||||
event.message().c_str());
|
||||
connect(trayIcon, SIGNAL(messageClicked()), this, SLOT(messageClicked()));
|
||||
} else {
|
||||
qDebug() << "Error: trayIcon is NULL. TabMessage::showSystemPopup failed";
|
||||
qCDebug(TabMessageLog) << "Error: trayIcon is NULL. TabMessage::showSystemPopup failed";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
|
||||
#include "tab.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(TabMessageLog, "tab_message");
|
||||
|
||||
class AbstractClient;
|
||||
class ChatView;
|
||||
class LineEditUnfocusable;
|
||||
@@ -29,7 +33,6 @@ signals:
|
||||
void maximizeClient();
|
||||
private slots:
|
||||
void sendMessage();
|
||||
void actLeave();
|
||||
void messageSent(const Response &response);
|
||||
void addMentionTag(QString mentionTag);
|
||||
void messageClicked();
|
||||
@@ -39,12 +42,12 @@ public:
|
||||
AbstractClient *_client,
|
||||
const ServerInfo_User &_ownUserInfo,
|
||||
const ServerInfo_User &_otherUserInfo);
|
||||
~TabMessage();
|
||||
void retranslateUi();
|
||||
void closeRequest();
|
||||
void tabActivated();
|
||||
~TabMessage() override;
|
||||
void retranslateUi() override;
|
||||
void closeRequest(bool forced = false) override;
|
||||
void tabActivated() override;
|
||||
QString getUserName() const;
|
||||
QString getTabText() const;
|
||||
QString getTabText() const override;
|
||||
|
||||
void processUserMessageEvent(const Event_UserMessage &event);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QDesktopServices>
|
||||
#include <QFileSystemModel>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
@@ -23,9 +24,11 @@
|
||||
#include <QMessageBox>
|
||||
#include <QToolBar>
|
||||
#include <QTreeView>
|
||||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
TabReplays::TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client)
|
||||
TabReplays::TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User *currentUserInfo)
|
||||
: Tab(_tabSupervisor), client(_client)
|
||||
{
|
||||
localDirModel = new QFileSystemModel(this);
|
||||
localDirModel->setRootPath(SettingsCache::instance().getReplaysPath());
|
||||
@@ -36,16 +39,31 @@ TabReplays::TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client) :
|
||||
localDirView->setColumnHidden(1, true);
|
||||
localDirView->setRootIndex(localDirModel->index(localDirModel->rootPath(), 0));
|
||||
localDirView->setSortingEnabled(true);
|
||||
localDirView->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
localDirView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
localDirView->header()->setSortIndicator(0, Qt::AscendingOrder);
|
||||
|
||||
leftToolBar = new QToolBar;
|
||||
// Left side layout
|
||||
/* put an invisible dummy QToolBar in the leftmost column so that the main toolbar is centered.
|
||||
* Really ugly workaround, but I couldn't figure out the proper way to make it centered */
|
||||
QToolBar *dummyToolBar = new QToolBar(this);
|
||||
QSizePolicy sizePolicy = dummyToolBar->sizePolicy();
|
||||
sizePolicy.setRetainSizeWhenHidden(true);
|
||||
dummyToolBar->setSizePolicy(sizePolicy);
|
||||
dummyToolBar->setVisible(false);
|
||||
|
||||
leftToolBar = new QToolBar(this);
|
||||
leftToolBar->setOrientation(Qt::Horizontal);
|
||||
leftToolBar->setIconSize(QSize(32, 32));
|
||||
QHBoxLayout *leftToolBarLayout = new QHBoxLayout;
|
||||
leftToolBarLayout->addStretch();
|
||||
leftToolBarLayout->addWidget(leftToolBar);
|
||||
leftToolBarLayout->addStretch();
|
||||
|
||||
QToolBar *leftRightmostToolBar = new QToolBar(this);
|
||||
leftRightmostToolBar->setOrientation(Qt::Horizontal);
|
||||
leftRightmostToolBar->setIconSize(QSize(32, 32));
|
||||
|
||||
QGridLayout *leftToolBarLayout = new QGridLayout;
|
||||
leftToolBarLayout->addWidget(dummyToolBar, 0, 0, Qt::AlignLeft);
|
||||
leftToolBarLayout->addWidget(leftToolBar, 0, 1, Qt::AlignHCenter);
|
||||
leftToolBarLayout->addWidget(leftRightmostToolBar, 0, 2, Qt::AlignRight);
|
||||
|
||||
QVBoxLayout *leftVbox = new QVBoxLayout;
|
||||
leftVbox->addWidget(localDirView);
|
||||
@@ -53,6 +71,7 @@ TabReplays::TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client) :
|
||||
leftGroupBox = new QGroupBox;
|
||||
leftGroupBox->setLayout(leftVbox);
|
||||
|
||||
// Right side layout
|
||||
rightToolBar = new QToolBar;
|
||||
rightToolBar->setOrientation(Qt::Horizontal);
|
||||
rightToolBar->setIconSize(QSize(32, 32));
|
||||
@@ -69,17 +88,31 @@ TabReplays::TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client) :
|
||||
rightGroupBox = new QGroupBox;
|
||||
rightGroupBox->setLayout(rightVbox);
|
||||
|
||||
// combine layouts
|
||||
QHBoxLayout *hbox = new QHBoxLayout;
|
||||
hbox->addWidget(leftGroupBox);
|
||||
hbox->addWidget(rightGroupBox);
|
||||
|
||||
// Left side actions
|
||||
aOpenLocalReplay = new QAction(this);
|
||||
aOpenLocalReplay->setIcon(QPixmap("theme:icons/view"));
|
||||
connect(aOpenLocalReplay, SIGNAL(triggered()), this, SLOT(actOpenLocalReplay()));
|
||||
connect(localDirView, SIGNAL(doubleClicked(const QModelIndex &)), this, SLOT(actOpenLocalReplay()));
|
||||
aRenameLocal = new QAction(this);
|
||||
aRenameLocal->setIcon(QPixmap("theme:icons/pencil"));
|
||||
connect(aRenameLocal, &QAction::triggered, this, &TabReplays::actRenameLocal);
|
||||
aNewLocalFolder = new QAction(this);
|
||||
aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
|
||||
connect(aNewLocalFolder, &QAction::triggered, this, &TabReplays::actNewLocalFolder);
|
||||
aDeleteLocalReplay = new QAction(this);
|
||||
aDeleteLocalReplay->setIcon(QPixmap("theme:icons/remove_row"));
|
||||
connect(aDeleteLocalReplay, SIGNAL(triggered()), this, SLOT(actDeleteLocalReplay()));
|
||||
|
||||
aOpenReplaysFolder = new QAction(this);
|
||||
aOpenReplaysFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_DirOpenIcon));
|
||||
connect(aOpenReplaysFolder, &QAction::triggered, this, &TabReplays::actOpenReplaysFolder);
|
||||
|
||||
// Right side actions
|
||||
aOpenRemoteReplay = new QAction(this);
|
||||
aOpenRemoteReplay->setIcon(QPixmap("theme:icons/view"));
|
||||
connect(aOpenRemoteReplay, SIGNAL(triggered()), this, SLOT(actOpenRemoteReplay()));
|
||||
@@ -94,8 +127,14 @@ TabReplays::TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client) :
|
||||
aDeleteRemoteReplay->setIcon(QPixmap("theme:icons/remove_row"));
|
||||
connect(aDeleteRemoteReplay, SIGNAL(triggered()), this, SLOT(actDeleteRemoteReplay()));
|
||||
|
||||
// Add actions to toolbars
|
||||
leftToolBar->addAction(aOpenLocalReplay);
|
||||
leftToolBar->addAction(aRenameLocal);
|
||||
leftToolBar->addAction(aNewLocalFolder);
|
||||
leftToolBar->addAction(aDeleteLocalReplay);
|
||||
|
||||
leftRightmostToolBar->addAction(aOpenReplaysFolder);
|
||||
|
||||
rightToolBar->addAction(aOpenRemoteReplay);
|
||||
rightToolBar->addAction(aDownload);
|
||||
rightToolBar->addAction(aKeep);
|
||||
@@ -109,6 +148,10 @@ TabReplays::TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client) :
|
||||
|
||||
connect(client, SIGNAL(replayAddedEventReceived(const Event_ReplayAdded &)), this,
|
||||
SLOT(replayAddedEventReceived(const Event_ReplayAdded &)));
|
||||
|
||||
connect(client, &AbstractClient::userInfoChanged, this, &TabReplays::handleConnected);
|
||||
connect(client, &AbstractClient::statusChanged, this, &TabReplays::handleConnectionChanged);
|
||||
setRemoteEnabled(currentUserInfo && currentUserInfo->user_level() & ServerInfo_User::IsRegistered);
|
||||
}
|
||||
|
||||
void TabReplays::retranslateUi()
|
||||
@@ -117,59 +160,173 @@ void TabReplays::retranslateUi()
|
||||
rightGroupBox->setTitle(tr("Server replay storage"));
|
||||
|
||||
aOpenLocalReplay->setText(tr("Watch replay"));
|
||||
aRenameLocal->setText(tr("Rename"));
|
||||
aNewLocalFolder->setText(tr("New folder"));
|
||||
aDeleteLocalReplay->setText(tr("Delete"));
|
||||
aOpenReplaysFolder->setText(tr("Open replays folder"));
|
||||
aOpenRemoteReplay->setText(tr("Watch replay"));
|
||||
aDownload->setText(tr("Download replay"));
|
||||
aKeep->setText(tr("Toggle expiration lock"));
|
||||
aDeleteRemoteReplay->setText(tr("Delete"));
|
||||
}
|
||||
|
||||
void TabReplays::handleConnected(const ServerInfo_User &userInfo)
|
||||
{
|
||||
setRemoteEnabled(userInfo.user_level() & ServerInfo_User::IsRegistered);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is only responsible for handling the disconnect. The connect is already handled elsewhere
|
||||
*/
|
||||
void TabReplays::handleConnectionChanged(ClientStatus status)
|
||||
{
|
||||
if (status == StatusDisconnected) {
|
||||
setRemoteEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
void TabReplays::setRemoteEnabled(bool enabled)
|
||||
{
|
||||
aOpenRemoteReplay->setEnabled(enabled);
|
||||
aDownload->setEnabled(enabled);
|
||||
aKeep->setEnabled(enabled);
|
||||
aDeleteRemoteReplay->setEnabled(enabled);
|
||||
|
||||
if (enabled) {
|
||||
serverDirView->refreshTree();
|
||||
} else {
|
||||
serverDirView->clearTree();
|
||||
}
|
||||
}
|
||||
|
||||
void TabReplays::actLocalDoubleClick(const QModelIndex &curLeft)
|
||||
{
|
||||
if (!localDirModel->isDir(curLeft)) {
|
||||
actOpenLocalReplay();
|
||||
}
|
||||
}
|
||||
|
||||
void TabReplays::actOpenLocalReplay()
|
||||
{
|
||||
QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
|
||||
for (const auto &curLeft : curLefts) {
|
||||
if (localDirModel->isDir(curLeft))
|
||||
continue;
|
||||
QString filePath = localDirModel->filePath(curLeft);
|
||||
|
||||
QFile f(filePath);
|
||||
if (!f.open(QIODevice::ReadOnly))
|
||||
continue;
|
||||
QByteArray _data = f.readAll();
|
||||
f.close();
|
||||
|
||||
GameReplay *replay = new GameReplay;
|
||||
replay->ParseFromArray(_data.data(), _data.size());
|
||||
|
||||
emit openReplay(replay);
|
||||
}
|
||||
}
|
||||
|
||||
void TabReplays::actRenameLocal()
|
||||
{
|
||||
QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
|
||||
for (const auto &curLeft : curLefts) {
|
||||
const QFileInfo info = localDirModel->fileInfo(curLeft);
|
||||
|
||||
const QString oldName = info.baseName();
|
||||
const QString title = info.isDir() ? tr("Rename local folder") : tr("Rename local file");
|
||||
|
||||
bool ok;
|
||||
QString newName = QInputDialog::getText(this, title, tr("New name:"), QLineEdit::Normal, oldName, &ok);
|
||||
if (!ok) { // terminate all remaining selections if user cancels
|
||||
return;
|
||||
}
|
||||
if (newName.isEmpty() || oldName == newName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString newFileName = newName;
|
||||
if (!info.suffix().isEmpty()) {
|
||||
newFileName += "." + info.suffix();
|
||||
}
|
||||
const QString newFilePath = QFileInfo(info.dir(), newFileName).filePath();
|
||||
|
||||
if (!QFile::rename(info.filePath(), newFilePath)) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Rename failed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TabReplays::actNewLocalFolder()
|
||||
{
|
||||
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
|
||||
if (localDirModel->isDir(curLeft))
|
||||
|
||||
QModelIndex dirIndex;
|
||||
if (curLeft.isValid() && !localDirModel->isDir(curLeft)) {
|
||||
dirIndex = curLeft.parent();
|
||||
} else {
|
||||
dirIndex = curLeft;
|
||||
}
|
||||
|
||||
bool ok;
|
||||
QString folderName =
|
||||
QInputDialog::getText(this, tr("New folder"), tr("Name of new folder:"), QLineEdit::Normal, "", &ok);
|
||||
if (!ok || folderName.isEmpty())
|
||||
return;
|
||||
QString filePath = localDirModel->filePath(curLeft);
|
||||
|
||||
QFile f(filePath);
|
||||
if (!f.open(QIODevice::ReadOnly))
|
||||
return;
|
||||
QByteArray _data = f.readAll();
|
||||
f.close();
|
||||
|
||||
GameReplay *replay = new GameReplay;
|
||||
replay->ParseFromArray(_data.data(), _data.size());
|
||||
|
||||
emit openReplay(replay);
|
||||
localDirModel->mkdir(dirIndex, folderName);
|
||||
}
|
||||
|
||||
void TabReplays::actDeleteLocalReplay()
|
||||
{
|
||||
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
|
||||
if (!curLeft.isValid())
|
||||
return;
|
||||
QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
|
||||
|
||||
if (QMessageBox::warning(this, tr("Delete local file"),
|
||||
tr("Are you sure you want to delete \"%1\"?").arg(localDirModel->fileName(curLeft)),
|
||||
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
|
||||
if (curLefts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
localDirModel->remove(curLeft);
|
||||
if (QMessageBox::warning(this, tr("Delete local file"), tr("Are you sure you want to delete the selected files?"),
|
||||
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto &curLeft : curLefts) {
|
||||
if (curLeft.isValid()) {
|
||||
localDirModel->remove(curLeft);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TabReplays::actOpenReplaysFolder()
|
||||
{
|
||||
QString dir = localDirModel->rootPath();
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(dir));
|
||||
}
|
||||
|
||||
void TabReplays::actRemoteDoubleClick(const QModelIndex &curRight)
|
||||
{
|
||||
if (serverDirView->getReplay(curRight)) {
|
||||
actOpenRemoteReplay();
|
||||
}
|
||||
}
|
||||
|
||||
void TabReplays::actOpenRemoteReplay()
|
||||
{
|
||||
ServerInfo_Replay const *curRight = serverDirView->getCurrentReplay();
|
||||
if (!curRight)
|
||||
return;
|
||||
auto const curRights = serverDirView->getSelectedReplays();
|
||||
|
||||
Command_ReplayDownload cmd;
|
||||
cmd.set_replay_id(curRight->replay_id());
|
||||
for (const auto curRight : curRights) {
|
||||
if (!curRight) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(openRemoteReplayFinished(const Response &)));
|
||||
client->sendCommand(pend);
|
||||
Command_ReplayDownload cmd;
|
||||
cmd.set_replay_id(curRight->replay_id());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(openRemoteReplayFinished(const Response &)));
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
}
|
||||
|
||||
void TabReplays::openRemoteReplayFinished(const Response &r)
|
||||
@@ -186,34 +343,46 @@ void TabReplays::openRemoteReplayFinished(const Response &r)
|
||||
|
||||
void TabReplays::actDownload()
|
||||
{
|
||||
QString filePath;
|
||||
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
|
||||
if (!curLeft.isValid())
|
||||
filePath = localDirModel->rootPath();
|
||||
else {
|
||||
while (!localDirModel->isDir(curLeft))
|
||||
curLeft = curLeft.parent();
|
||||
filePath = localDirModel->filePath(curLeft);
|
||||
while (!localDirModel->isDir(curLeft)) {
|
||||
curLeft = curLeft.parent();
|
||||
}
|
||||
|
||||
ServerInfo_Replay const *curRight = serverDirView->getCurrentReplay();
|
||||
|
||||
if (!curRight) {
|
||||
QMessageBox::information(this, tr("Downloading Replays"),
|
||||
tr("Folder download is not yet supported. Please download replays individually."));
|
||||
return;
|
||||
for (const auto curRight : serverDirView->selectionModel()->selectedRows()) {
|
||||
downloadNodeAtIndex(curLeft, curRight);
|
||||
}
|
||||
}
|
||||
|
||||
filePath += QString("/replay_%1.cor").arg(curRight->replay_id());
|
||||
void TabReplays::downloadNodeAtIndex(const QModelIndex &curLeft, const QModelIndex &curRight)
|
||||
{
|
||||
if (const auto replayMatch = serverDirView->getReplayMatch(curRight)) {
|
||||
// node at index is a folder
|
||||
const QString name =
|
||||
QString::number(replayMatch->game_id()) + "_" + QString::fromStdString(replayMatch->game_name());
|
||||
|
||||
Command_ReplayDownload cmd;
|
||||
cmd.set_replay_id(curRight->replay_id());
|
||||
const auto dirIndex = curLeft.isValid() ? curLeft : localDirModel->index(localDirModel->rootPath());
|
||||
const auto newDirIndex = localDirModel->mkdir(dirIndex, name);
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
pend->setExtraData(filePath);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(downloadFinished(Response, CommandContainer, QVariant)));
|
||||
client->sendCommand(pend);
|
||||
int rows = serverDirView->model()->rowCount(curRight);
|
||||
for (int i = 0; i < rows; i++) {
|
||||
const auto childIndex = serverDirView->model()->index(i, 0, curRight);
|
||||
downloadNodeAtIndex(newDirIndex, childIndex);
|
||||
}
|
||||
} else if (const auto replay = serverDirView->getReplay(curRight)) {
|
||||
// node at index is a replay
|
||||
const QString dirPath = curLeft.isValid() ? localDirModel->filePath(curLeft) : localDirModel->rootPath();
|
||||
const QString filePath = dirPath + QString("/replay_%1.cor").arg(replay->replay_id());
|
||||
|
||||
Command_ReplayDownload cmd;
|
||||
cmd.set_replay_id(replay->replay_id());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
pend->setExtraData(filePath);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(downloadFinished(Response, CommandContainer, QVariant)));
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
// node at index was invalid
|
||||
}
|
||||
|
||||
void TabReplays::downloadFinished(const Response &r,
|
||||
@@ -235,18 +404,22 @@ void TabReplays::downloadFinished(const Response &r,
|
||||
|
||||
void TabReplays::actKeepRemoteReplay()
|
||||
{
|
||||
ServerInfo_ReplayMatch const *curRight = serverDirView->getCurrentReplayMatch();
|
||||
if (!curRight)
|
||||
const auto curRights = serverDirView->getSelectedReplayMatches();
|
||||
|
||||
if (curRights.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Command_ReplayModifyMatch cmd;
|
||||
cmd.set_game_id(curRight->game_id());
|
||||
cmd.set_do_not_hide(!curRight->do_not_hide());
|
||||
for (const auto curRight : curRights) {
|
||||
Command_ReplayModifyMatch cmd;
|
||||
cmd.set_game_id(curRight->game_id());
|
||||
cmd.set_do_not_hide(!curRight->do_not_hide());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(keepRemoteReplayFinished(Response, CommandContainer)));
|
||||
client->sendCommand(pend);
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(keepRemoteReplayFinished(Response, CommandContainer)));
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
}
|
||||
|
||||
void TabReplays::keepRemoteReplayFinished(const Response &r, const CommandContainer &commandContainer)
|
||||
@@ -265,21 +438,27 @@ void TabReplays::keepRemoteReplayFinished(const Response &r, const CommandContai
|
||||
|
||||
void TabReplays::actDeleteRemoteReplay()
|
||||
{
|
||||
ServerInfo_ReplayMatch const *curRight = serverDirView->getCurrentReplayMatch();
|
||||
if (!curRight)
|
||||
const auto curRights = serverDirView->getSelectedReplayMatches();
|
||||
|
||||
if (curRights.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (QMessageBox::warning(this, tr("Delete remote replay"),
|
||||
tr("Are you sure you want to delete the replay of game %1?").arg(curRight->game_id()),
|
||||
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
|
||||
tr("Are you sure you want to delete the selected replays?"),
|
||||
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
|
||||
Command_ReplayDeleteMatch cmd;
|
||||
cmd.set_game_id(curRight->game_id());
|
||||
for (const auto curRight : curRights) {
|
||||
Command_ReplayDeleteMatch cmd;
|
||||
cmd.set_game_id(curRight->game_id());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(deleteRemoteReplayFinished(Response, CommandContainer)));
|
||||
client->sendCommand(pend);
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
|
||||
SLOT(deleteRemoteReplayFinished(Response, CommandContainer)));
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
}
|
||||
|
||||
void TabReplays::deleteRemoteReplayFinished(const Response &r, const CommandContainer &commandContainer)
|
||||
@@ -294,5 +473,11 @@ void TabReplays::deleteRemoteReplayFinished(const Response &r, const CommandCont
|
||||
|
||||
void TabReplays::replayAddedEventReceived(const Event_ReplayAdded &event)
|
||||
{
|
||||
serverDirView->addMatchInfo(event.match_info());
|
||||
if (event.has_match_info()) {
|
||||
// 99.9% of events will have match info (Normal Workflow)
|
||||
serverDirView->addMatchInfo(event.match_info());
|
||||
} else {
|
||||
// When a Moderator force adds a replay, we need to refresh their view
|
||||
serverDirView->refreshTree();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#ifndef TAB_REPLAYS_H
|
||||
#define TAB_REPLAYS_H
|
||||
|
||||
#include "../game_logic/abstract_client.h"
|
||||
#include "tab.h"
|
||||
|
||||
class ServerInfo_User;
|
||||
class Response;
|
||||
class AbstractClient;
|
||||
class QTreeView;
|
||||
@@ -25,12 +27,27 @@ private:
|
||||
RemoteReplayList_TreeWidget *serverDirView;
|
||||
QGroupBox *leftGroupBox, *rightGroupBox;
|
||||
|
||||
QAction *aOpenLocalReplay, *aDeleteLocalReplay, *aOpenRemoteReplay, *aDownload, *aKeep, *aDeleteRemoteReplay;
|
||||
private slots:
|
||||
void actOpenLocalReplay();
|
||||
QAction *aOpenLocalReplay, *aRenameLocal, *aNewLocalFolder, *aDeleteLocalReplay;
|
||||
QAction *aOpenReplaysFolder;
|
||||
QAction *aOpenRemoteReplay, *aDownload, *aKeep, *aDeleteRemoteReplay;
|
||||
|
||||
void setRemoteEnabled(bool enabled);
|
||||
|
||||
void downloadNodeAtIndex(const QModelIndex &curLeft, const QModelIndex &curRight);
|
||||
|
||||
private slots:
|
||||
void handleConnected(const ServerInfo_User &userInfo);
|
||||
void handleConnectionChanged(ClientStatus status);
|
||||
|
||||
void actLocalDoubleClick(const QModelIndex &curLeft);
|
||||
void actRenameLocal();
|
||||
void actOpenLocalReplay();
|
||||
void actNewLocalFolder();
|
||||
void actDeleteLocalReplay();
|
||||
|
||||
void actOpenReplaysFolder();
|
||||
|
||||
void actRemoteDoubleClick(const QModelIndex &curLeft);
|
||||
void actOpenRemoteReplay();
|
||||
void openRemoteReplayFinished(const Response &r);
|
||||
|
||||
@@ -48,11 +65,11 @@ signals:
|
||||
void openReplay(GameReplay *replay);
|
||||
|
||||
public:
|
||||
TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client);
|
||||
void retranslateUi();
|
||||
QString getTabText() const
|
||||
TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User *currentUserInfo);
|
||||
void retranslateUi() override;
|
||||
QString getTabText() const override
|
||||
{
|
||||
return tr("Game replays");
|
||||
return tr("Game Replays");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
#include "../../main.h"
|
||||
#include "../../server/chat_view/chat_view.h"
|
||||
#include "../../server/pending_command.h"
|
||||
#include "../../server/user/user_list.h"
|
||||
#include "../../server/user/user_list_manager.h"
|
||||
#include "../../server/user/user_list_widget.h"
|
||||
#include "../../settings/cache_settings.h"
|
||||
#include "get_pb_extension.h"
|
||||
#include "pb/event_join_room.pb.h"
|
||||
@@ -37,7 +38,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
|
||||
ServerInfo_User *_ownUser,
|
||||
const ServerInfo_Room &info)
|
||||
: Tab(_tabSupervisor), client(_client), roomId(info.room_id()), roomName(QString::fromStdString(info.name())),
|
||||
ownUser(_ownUser)
|
||||
ownUser(_ownUser), userListProxy(_tabSupervisor->getUserListManager())
|
||||
{
|
||||
const int gameTypeListSize = info.gametype_list_size();
|
||||
for (int i = 0; i < gameTypeListSize; ++i)
|
||||
@@ -47,15 +48,15 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
|
||||
QMap<int, GameTypeMap> tempMap;
|
||||
tempMap.insert(info.room_id(), gameTypes);
|
||||
gameSelector = new GameSelector(client, tabSupervisor, this, QMap<int, QString>(), tempMap, true, true);
|
||||
userList = new UserList(tabSupervisor, client, UserList::RoomList);
|
||||
userList = new UserListWidget(tabSupervisor, client, UserListWidget::RoomList);
|
||||
connect(userList, SIGNAL(openMessageDialog(const QString &, bool)), this,
|
||||
SIGNAL(openMessageDialog(const QString &, bool)));
|
||||
|
||||
chatView = new ChatView(tabSupervisor, tabSupervisor, nullptr, true, this);
|
||||
chatView = new ChatView(tabSupervisor, nullptr, true, this);
|
||||
connect(chatView, SIGNAL(showMentionPopup(const QString &)), this, SLOT(actShowMentionPopup(const QString &)));
|
||||
connect(chatView, SIGNAL(messageClickedSignal()), this, SLOT(focusTab()));
|
||||
connect(chatView, SIGNAL(openMessageDialog(QString, bool)), this, SIGNAL(openMessageDialog(QString, bool)));
|
||||
connect(chatView, SIGNAL(showCardInfoPopup(QPoint, QString)), this, SLOT(showCardInfoPopup(QPoint, QString)));
|
||||
connect(chatView, &ChatView::showCardInfoPopup, this, &TabRoom::showCardInfoPopup);
|
||||
connect(chatView, SIGNAL(deleteCardInfoPopup(QString)), this, SLOT(deleteCardInfoPopup(QString)));
|
||||
connect(chatView, SIGNAL(addMentionTag(QString)), this, SLOT(addMentionTag(QString)));
|
||||
connect(&SettingsCache::instance(), SIGNAL(chatMentionCompleterChanged()), this, SLOT(actCompleterChanged()));
|
||||
@@ -101,7 +102,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
|
||||
hbox->addWidget(userList, 1);
|
||||
|
||||
aLeaveRoom = new QAction(this);
|
||||
connect(aLeaveRoom, SIGNAL(triggered()), this, SLOT(actLeaveRoom()));
|
||||
connect(aLeaveRoom, &QAction::triggered, this, [this] { closeRequest(); });
|
||||
|
||||
roomMenu = new QMenu(this);
|
||||
roomMenu->addAction(aLeaveRoom);
|
||||
@@ -135,11 +136,6 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
|
||||
setCentralWidget(mainWidget);
|
||||
}
|
||||
|
||||
TabRoom::~TabRoom()
|
||||
{
|
||||
emit roomClosing(this);
|
||||
}
|
||||
|
||||
void TabRoom::retranslateUi()
|
||||
{
|
||||
gameSelector->retranslateUi();
|
||||
@@ -175,9 +171,11 @@ void TabRoom::actShowPopup(const QString &message)
|
||||
}
|
||||
}
|
||||
|
||||
void TabRoom::closeRequest()
|
||||
void TabRoom::closeRequest(bool /*forced*/)
|
||||
{
|
||||
actLeaveRoom();
|
||||
sendRoomCommand(prepareRoomCommand(Command_LeaveRoom()));
|
||||
emit roomClosing(this);
|
||||
close();
|
||||
}
|
||||
|
||||
void TabRoom::tabActivated()
|
||||
@@ -216,12 +214,6 @@ void TabRoom::sayFinished(const Response &response)
|
||||
chatView->appendMessage(tr("You are flooding the chat. Please wait a couple of seconds."));
|
||||
}
|
||||
|
||||
void TabRoom::actLeaveRoom()
|
||||
{
|
||||
sendRoomCommand(prepareRoomCommand(Command_LeaveRoom()));
|
||||
deleteLater();
|
||||
}
|
||||
|
||||
void TabRoom::actClearChat()
|
||||
{
|
||||
chatView->clearChat();
|
||||
@@ -291,17 +283,15 @@ void TabRoom::processRoomSayEvent(const Event_RoomSay &event)
|
||||
QString senderName = QString::fromStdString(event.name());
|
||||
QString message = QString::fromStdString(event.message());
|
||||
|
||||
if (tabSupervisor->getUserListsTab()->getIgnoreList()->getUsers().contains(senderName))
|
||||
if (userListProxy->isUserIgnored(senderName))
|
||||
return;
|
||||
|
||||
UserListTWI *twi = userList->getUsers().value(senderName);
|
||||
UserLevelFlags userLevel;
|
||||
QString userPrivLevel;
|
||||
ServerInfo_User userInfo = {};
|
||||
if (twi) {
|
||||
userLevel = UserLevelFlags(twi->getUserInfo().user_level());
|
||||
userPrivLevel = QString::fromStdString(twi->getUserInfo().privlevel());
|
||||
userInfo = twi->getUserInfo();
|
||||
if (SettingsCache::instance().getIgnoreUnregisteredUsers() &&
|
||||
!userLevel.testFlag(ServerInfo_User::IsRegistered))
|
||||
!UserLevelFlags(userInfo.user_level()).testFlag(ServerInfo_User::IsRegistered))
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -314,7 +304,7 @@ void TabRoom::processRoomSayEvent(const Event_RoomSay &event)
|
||||
QString(QDateTime::fromMSecsSinceEpoch(event.time_of()).toLocalTime().toString("d MMM yyyy HH:mm:ss")) +
|
||||
"] " + message;
|
||||
|
||||
chatView->appendMessage(message, event.message_type(), senderName, userLevel, userPrivLevel, true);
|
||||
chatView->appendMessage(message, event.message_type(), userInfo, true);
|
||||
emit userEvent(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include <QKeyEvent>
|
||||
#include <QMap>
|
||||
|
||||
class UserListProxy;
|
||||
class UserListManager;
|
||||
namespace google
|
||||
{
|
||||
namespace protobuf
|
||||
@@ -17,7 +19,7 @@ class Message;
|
||||
}
|
||||
} // namespace google
|
||||
class AbstractClient;
|
||||
class UserList;
|
||||
class UserListWidget;
|
||||
class QLabel;
|
||||
class ChatView;
|
||||
class QPushButton;
|
||||
@@ -48,7 +50,8 @@ private:
|
||||
QMap<int, QString> gameTypes;
|
||||
|
||||
GameSelector *gameSelector;
|
||||
UserList *userList;
|
||||
UserListWidget *userList;
|
||||
const UserListProxy *userListProxy;
|
||||
ChatView *chatView;
|
||||
QLabel *sayLabel;
|
||||
LineEditCompleter *sayEdit;
|
||||
@@ -70,7 +73,6 @@ signals:
|
||||
private slots:
|
||||
void sendMessage();
|
||||
void sayFinished(const Response &response);
|
||||
void actLeaveRoom();
|
||||
void actClearChat();
|
||||
void actOpenChatSettings();
|
||||
void addMentionTag(QString mentionTag);
|
||||
@@ -91,10 +93,9 @@ public:
|
||||
AbstractClient *_client,
|
||||
ServerInfo_User *_ownUser,
|
||||
const ServerInfo_Room &info);
|
||||
~TabRoom();
|
||||
void retranslateUi();
|
||||
void closeRequest();
|
||||
void tabActivated();
|
||||
void retranslateUi() override;
|
||||
void closeRequest(bool forced = false) override;
|
||||
void tabActivated() override;
|
||||
void processRoomEvent(const RoomEvent &event);
|
||||
int getRoomId() const
|
||||
{
|
||||
@@ -108,7 +109,7 @@ public:
|
||||
{
|
||||
return roomName;
|
||||
}
|
||||
QString getTabText() const
|
||||
QString getTabText() const override
|
||||
{
|
||||
return roomName;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "../../client/game_logic/abstract_client.h"
|
||||
#include "../../server/pending_command.h"
|
||||
#include "../../server/user/user_list.h"
|
||||
#include "../../server/user/user_list_widget.h"
|
||||
#include "pb/event_list_rooms.pb.h"
|
||||
#include "pb/event_server_message.pb.h"
|
||||
#include "pb/response_join_room.pb.h"
|
||||
@@ -138,8 +138,7 @@ void RoomSelector::joinClicked()
|
||||
emit joinRoomRequest(id, true);
|
||||
}
|
||||
|
||||
TabServer::TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client, QWidget *parent)
|
||||
: Tab(_tabSupervisor, parent), client(_client)
|
||||
TabServer::TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client)
|
||||
{
|
||||
roomSelector = new RoomSelector(client);
|
||||
serverInfoBox = new QTextBrowser;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
class AbstractClient;
|
||||
class QTextEdit;
|
||||
class QLabel;
|
||||
class UserList;
|
||||
class UserListWidget;
|
||||
class QPushButton;
|
||||
|
||||
class Event_ListRooms;
|
||||
@@ -34,7 +34,7 @@ signals:
|
||||
void joinRoomRequest(int, bool setCurrent);
|
||||
|
||||
public:
|
||||
RoomSelector(AbstractClient *_client, QWidget *parent = nullptr);
|
||||
explicit RoomSelector(AbstractClient *_client, QWidget *parent = nullptr);
|
||||
void retranslateUi();
|
||||
};
|
||||
|
||||
@@ -55,9 +55,9 @@ private:
|
||||
bool shouldEmitUpdate = false;
|
||||
|
||||
public:
|
||||
TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client, QWidget *parent = nullptr);
|
||||
void retranslateUi();
|
||||
QString getTabText() const
|
||||
TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client);
|
||||
void retranslateUi() override;
|
||||
QString getTabText() const override
|
||||
{
|
||||
return tr("Server");
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
#include "../../client/game_logic/abstract_client.h"
|
||||
#include "../../main.h"
|
||||
#include "../../server/user/user_list.h"
|
||||
#include "../../server/user/user_list_manager.h"
|
||||
#include "../../server/user/user_list_widget.h"
|
||||
#include "../../settings/cache_settings.h"
|
||||
#include "../ui/pixel_map_generator.h"
|
||||
#include "pb/event_game_joined.pb.h"
|
||||
#include "pb/event_notify_user.pb.h"
|
||||
#include "pb/event_user_message.pb.h"
|
||||
#include "pb/game_event_container.pb.h"
|
||||
#include "pb/moderator_commands.pb.h"
|
||||
#include "pb/game_replay.pb.h"
|
||||
#include "pb/room_commands.pb.h"
|
||||
#include "pb/room_event.pb.h"
|
||||
#include "pb/serverinfo_room.pb.h"
|
||||
@@ -24,9 +25,9 @@
|
||||
#include "tab_replays.h"
|
||||
#include "tab_room.h"
|
||||
#include "tab_server.h"
|
||||
#include "visual_deck_storage/tab_deck_storage_visual.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
#include <QSystemTrayIcon>
|
||||
@@ -47,15 +48,15 @@ CloseButton::CloseButton(QWidget *parent) : QAbstractButton(parent)
|
||||
{
|
||||
setFocusPolicy(Qt::NoFocus);
|
||||
setCursor(Qt::ArrowCursor);
|
||||
resize(sizeHint());
|
||||
resize(this->sizeHint());
|
||||
}
|
||||
|
||||
QSize CloseButton::sizeHint() const
|
||||
{
|
||||
ensurePolished();
|
||||
int width = style()->pixelMetric(QStyle::PM_TabCloseIndicatorWidth, 0, this);
|
||||
int height = style()->pixelMetric(QStyle::PM_TabCloseIndicatorHeight, 0, this);
|
||||
return QSize(width, height);
|
||||
int width = style()->pixelMetric(QStyle::PM_TabCloseIndicatorWidth, nullptr, this);
|
||||
int height = style()->pixelMetric(QStyle::PM_TabCloseIndicatorHeight, nullptr, this);
|
||||
return {width, height};
|
||||
}
|
||||
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
@@ -87,10 +88,9 @@ void CloseButton::paintEvent(QPaintEvent * /*event*/)
|
||||
if (isDown())
|
||||
opt.state |= QStyle::State_Sunken;
|
||||
|
||||
if (const QTabBar *tb = qobject_cast<const QTabBar *>(parent())) {
|
||||
if (const auto *tb = qobject_cast<const QTabBar *>(parent())) {
|
||||
int index = tb->currentIndex();
|
||||
QTabBar::ButtonPosition position =
|
||||
(QTabBar::ButtonPosition)style()->styleHint(QStyle::SH_TabBar_CloseButtonPosition, 0, tb);
|
||||
auto position = (QTabBar::ButtonPosition)style()->styleHint(QStyle::SH_TabBar_CloseButtonPosition, nullptr, tb);
|
||||
if (tb->tabButton(index, position) == this)
|
||||
opt.state |= QStyle::State_Selected;
|
||||
}
|
||||
@@ -98,9 +98,10 @@ void CloseButton::paintEvent(QPaintEvent * /*event*/)
|
||||
style()->drawPrimitive(QStyle::PE_IndicatorTabClose, &opt, &p, this);
|
||||
}
|
||||
|
||||
TabSupervisor::TabSupervisor(AbstractClient *_client, QWidget *parent)
|
||||
: QTabWidget(parent), userInfo(0), client(_client), tabServer(0), tabUserLists(0), tabDeckStorage(0), tabReplays(0),
|
||||
tabAdmin(0), tabLog(0)
|
||||
TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *parent)
|
||||
: QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabVisualDeckStorage(nullptr),
|
||||
tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr), tabReplays(nullptr), tabAdmin(nullptr),
|
||||
tabLog(nullptr), isLocalGame(false)
|
||||
{
|
||||
setElideMode(Qt::ElideRight);
|
||||
setMovable(true);
|
||||
@@ -112,18 +113,56 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QWidget *parent)
|
||||
tabBar()->setStyle(new MacOSTabFixStyle);
|
||||
#endif
|
||||
|
||||
connect(this, SIGNAL(currentChanged(int)), this, SLOT(updateCurrent(int)));
|
||||
userListManager = new UserListManager(client, this);
|
||||
|
||||
connect(client, SIGNAL(roomEventReceived(const RoomEvent &)), this, SLOT(processRoomEvent(const RoomEvent &)));
|
||||
connect(client, SIGNAL(gameEventContainerReceived(const GameEventContainer &)), this,
|
||||
SLOT(processGameEventContainer(const GameEventContainer &)));
|
||||
connect(client, SIGNAL(gameJoinedEventReceived(const Event_GameJoined &)), this,
|
||||
SLOT(gameJoined(const Event_GameJoined &)));
|
||||
connect(client, SIGNAL(userMessageEventReceived(const Event_UserMessage &)), this,
|
||||
SLOT(processUserMessageEvent(const Event_UserMessage &)));
|
||||
connect(client, SIGNAL(maxPingTime(int, int)), this, SLOT(updatePingTime(int, int)));
|
||||
connect(client, SIGNAL(notifyUserEventReceived(const Event_NotifyUser &)), this,
|
||||
SLOT(processNotifyUserEvent(const Event_NotifyUser &)));
|
||||
// connect tab changes
|
||||
connect(this, &TabSupervisor::currentChanged, this, &TabSupervisor::updateCurrent);
|
||||
|
||||
// connect client
|
||||
connect(client, &AbstractClient::roomEventReceived, this, &TabSupervisor::processRoomEvent);
|
||||
connect(client, &AbstractClient::gameEventContainerReceived, this, &TabSupervisor::processGameEventContainer);
|
||||
connect(client, &AbstractClient::gameJoinedEventReceived, this, &TabSupervisor::gameJoined);
|
||||
connect(client, &AbstractClient::userMessageEventReceived, this, &TabSupervisor::processUserMessageEvent);
|
||||
connect(client, &AbstractClient::maxPingTime, this, &TabSupervisor::updatePingTime);
|
||||
connect(client, &AbstractClient::notifyUserEventReceived, this, &TabSupervisor::processNotifyUserEvent);
|
||||
|
||||
// create tabs menu actions
|
||||
aTabDeckEditor = new QAction(this);
|
||||
connect(aTabDeckEditor, &QAction::triggered, this, [this] { addDeckEditorTab(nullptr); });
|
||||
|
||||
aTabVisualDeckStorage = new QAction(this);
|
||||
aTabVisualDeckStorage->setCheckable(true);
|
||||
connect(aTabVisualDeckStorage, &QAction::triggered, this, &TabSupervisor::actTabVisualDeckStorage);
|
||||
|
||||
aTabServer = new QAction(this);
|
||||
aTabServer->setCheckable(true);
|
||||
connect(aTabServer, &QAction::triggered, this, &TabSupervisor::actTabServer);
|
||||
|
||||
aTabAccount = new QAction(this);
|
||||
aTabAccount->setCheckable(true);
|
||||
connect(aTabAccount, &QAction::triggered, this, &TabSupervisor::actTabAccount);
|
||||
|
||||
aTabDeckStorage = new QAction(this);
|
||||
aTabDeckStorage->setCheckable(true);
|
||||
connect(aTabDeckStorage, &QAction::triggered, this, &TabSupervisor::actTabDeckStorage);
|
||||
|
||||
aTabReplays = new QAction(this);
|
||||
aTabReplays->setCheckable(true);
|
||||
connect(aTabReplays, &QAction::triggered, this, &TabSupervisor::actTabReplays);
|
||||
|
||||
aTabAdmin = new QAction(this);
|
||||
aTabAdmin->setCheckable(true);
|
||||
connect(aTabAdmin, &QAction::triggered, this, &TabSupervisor::actTabAdmin);
|
||||
|
||||
aTabLog = new QAction(this);
|
||||
aTabLog->setCheckable(true);
|
||||
connect(aTabLog, &QAction::triggered, this, &TabSupervisor::actTabLog);
|
||||
|
||||
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
|
||||
&TabSupervisor::refreshShortcuts);
|
||||
refreshShortcuts();
|
||||
|
||||
resetTabsMenu();
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
@@ -135,12 +174,23 @@ TabSupervisor::~TabSupervisor()
|
||||
|
||||
void TabSupervisor::retranslateUi()
|
||||
{
|
||||
// tab menu actions
|
||||
aTabDeckEditor->setText(tr("Deck Editor"));
|
||||
aTabVisualDeckStorage->setText(tr("&Visual Deck Storage"));
|
||||
aTabServer->setText(tr("Server"));
|
||||
aTabAccount->setText(tr("Account"));
|
||||
aTabDeckStorage->setText(tr("Deck Storage"));
|
||||
aTabReplays->setText(tr("Game Replays"));
|
||||
aTabAdmin->setText(tr("Administration"));
|
||||
aTabLog->setText(tr("Logs"));
|
||||
|
||||
// tabs
|
||||
QList<Tab *> tabs;
|
||||
tabs.append(tabServer);
|
||||
tabs.append(tabReplays);
|
||||
tabs.append(tabDeckStorage);
|
||||
tabs.append(tabAdmin);
|
||||
tabs.append(tabUserLists);
|
||||
tabs.append(tabAccount);
|
||||
tabs.append(tabLog);
|
||||
QMapIterator<int, TabRoom *> roomIterator(roomTabs);
|
||||
while (roomIterator.hasNext())
|
||||
@@ -158,14 +208,21 @@ void TabSupervisor::retranslateUi()
|
||||
while (messageIterator.hasNext())
|
||||
tabs.append(messageIterator.next().value());
|
||||
|
||||
for (int i = 0; i < tabs.size(); ++i)
|
||||
if (tabs[i]) {
|
||||
int idx = indexOf(tabs[i]);
|
||||
QString tabText = tabs[i]->getTabText();
|
||||
for (auto &tab : tabs) {
|
||||
if (tab) {
|
||||
int idx = indexOf(tab);
|
||||
QString tabText = tab->getTabText();
|
||||
setTabText(idx, sanitizeTabName(tabText));
|
||||
setTabToolTip(idx, sanitizeHtml(tabText));
|
||||
tabs[i]->retranslateUi();
|
||||
tab->retranslateUi();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TabSupervisor::refreshShortcuts()
|
||||
{
|
||||
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
|
||||
aTabDeckEditor->setShortcuts(shortcuts.getShortcut("MainWindow/aDeckEditor"));
|
||||
}
|
||||
|
||||
bool TabSupervisor::closeRequest()
|
||||
@@ -178,7 +235,7 @@ bool TabSupervisor::closeRequest()
|
||||
}
|
||||
}
|
||||
|
||||
foreach (TabDeckEditor *tab, deckEditorTabs) {
|
||||
for (TabDeckEditor *tab : deckEditorTabs) {
|
||||
if (!tab->confirmClose())
|
||||
return false;
|
||||
}
|
||||
@@ -191,71 +248,123 @@ AbstractClient *TabSupervisor::getClient() const
|
||||
return localClients.isEmpty() ? client : localClients.first();
|
||||
}
|
||||
|
||||
QString TabSupervisor::sanitizeTabName(QString dirty) const
|
||||
QString TabSupervisor::sanitizeTabName(QString dirty)
|
||||
{
|
||||
return dirty.replace("&", "&&");
|
||||
}
|
||||
|
||||
QString TabSupervisor::sanitizeHtml(QString dirty) const
|
||||
QString TabSupervisor::sanitizeHtml(QString dirty)
|
||||
{
|
||||
return dirty.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """);
|
||||
}
|
||||
|
||||
int TabSupervisor::myAddTab(Tab *tab)
|
||||
/**
|
||||
* If the action is not in the target checked state, then set it to that state by triggering the action.
|
||||
* If the action is already in the target checked state, then do nothing.
|
||||
*
|
||||
* This allows us to programmatically trigger a QAction::triggered signal for a specific checked state.
|
||||
*/
|
||||
static void checkAndTrigger(QAction *checkableAction, bool checked)
|
||||
{
|
||||
connect(tab, SIGNAL(userEvent(bool)), this, SLOT(tabUserEvent(bool)));
|
||||
connect(tab, SIGNAL(tabTextChanged(Tab *, QString)), this, SLOT(updateTabText(Tab *, QString)));
|
||||
if (checkableAction->isChecked() != checked) {
|
||||
checkableAction->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the always-available tabs, depending on settings.
|
||||
*/
|
||||
void TabSupervisor::initStartupTabs()
|
||||
{
|
||||
addDeckEditorTab(nullptr);
|
||||
|
||||
checkAndTrigger(aTabVisualDeckStorage, SettingsCache::instance().getTabVisualDeckStorageOpen());
|
||||
checkAndTrigger(aTabDeckStorage, SettingsCache::instance().getTabDeckStorageOpen());
|
||||
checkAndTrigger(aTabReplays, SettingsCache::instance().getTabReplaysOpen());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the tab to the TabSupervisor's tab bar.
|
||||
*
|
||||
* @param tab The Tab to add
|
||||
* @param manager The menu action that corresponds to this tab, if this is a single-instance managed tab. Pass in
|
||||
* nullptr if this is not a managed tab.
|
||||
* @return The index of the added tab in the tab widget's tab menu
|
||||
*/
|
||||
int TabSupervisor::myAddTab(Tab *tab, QAction *manager)
|
||||
{
|
||||
connect(tab, &TabGame::userEvent, this, &TabSupervisor::tabUserEvent);
|
||||
connect(tab, &TabGame::tabTextChanged, this, &TabSupervisor::updateTabText);
|
||||
|
||||
QString tabText = tab->getTabText();
|
||||
int idx = addTab(tab, sanitizeTabName(tabText));
|
||||
setTabToolTip(idx, sanitizeHtml(tabText));
|
||||
|
||||
addCloseButtonToTab(tab, idx, manager);
|
||||
|
||||
return idx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a usable close button to the tab.
|
||||
*
|
||||
* @param tab The Tab
|
||||
* @param tabIndex The tab bar index of the tab
|
||||
* @param manager The menu action that corresponds to this tab, if this is a single-instance managed tab. Pass in
|
||||
* nullptr if this is not a managed tab.
|
||||
*/
|
||||
void TabSupervisor::addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager)
|
||||
{
|
||||
auto closeSide = static_cast<QTabBar::ButtonPosition>(
|
||||
tabBar()->style()->styleHint(QStyle::SH_TabBar_CloseButtonPosition, nullptr, tabBar()));
|
||||
auto *closeButton = new CloseButton(tab);
|
||||
if (manager) {
|
||||
// If managed, all close requests should go through the menu action
|
||||
connect(closeButton, &CloseButton::clicked, this, [manager] { checkAndTrigger(manager, false); });
|
||||
} else {
|
||||
connect(closeButton, &CloseButton::clicked, tab, [tab] { tab->closeRequest(); });
|
||||
}
|
||||
tabBar()->setTabButton(tabIndex, closeSide, closeButton);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the tabs menu to the tabs that are always available
|
||||
*/
|
||||
void TabSupervisor::resetTabsMenu()
|
||||
{
|
||||
tabsMenu->clear();
|
||||
tabsMenu->addAction(aTabDeckEditor);
|
||||
tabsMenu->addSeparator();
|
||||
tabsMenu->addAction(aTabVisualDeckStorage);
|
||||
tabsMenu->addAction(aTabDeckStorage);
|
||||
tabsMenu->addAction(aTabReplays);
|
||||
}
|
||||
|
||||
void TabSupervisor::start(const ServerInfo_User &_userInfo)
|
||||
{
|
||||
isLocalGame = false;
|
||||
userInfo = new ServerInfo_User(_userInfo);
|
||||
|
||||
tabServer = new TabServer(this, client);
|
||||
connect(tabServer, SIGNAL(roomJoined(const ServerInfo_Room &, bool)), this,
|
||||
SLOT(addRoomTab(const ServerInfo_Room &, bool)));
|
||||
myAddTab(tabServer);
|
||||
userListManager->handleConnect();
|
||||
|
||||
tabUserLists = new TabUserLists(this, client, *userInfo);
|
||||
connect(tabUserLists, SIGNAL(openMessageDialog(const QString &, bool)), this,
|
||||
SLOT(addMessageTab(const QString &, bool)));
|
||||
connect(tabUserLists, SIGNAL(userJoined(ServerInfo_User)), this, SLOT(processUserJoined(ServerInfo_User)));
|
||||
connect(tabUserLists, SIGNAL(userLeft(const QString &)), this, SLOT(processUserLeft(const QString &)));
|
||||
myAddTab(tabUserLists);
|
||||
resetTabsMenu();
|
||||
|
||||
tabsMenu->addSeparator();
|
||||
tabsMenu->addAction(aTabServer);
|
||||
tabsMenu->addAction(aTabAccount);
|
||||
|
||||
checkAndTrigger(aTabServer, SettingsCache::instance().getTabServerOpen());
|
||||
checkAndTrigger(aTabAccount, SettingsCache::instance().getTabAccountOpen());
|
||||
|
||||
updatePingTime(0, -1);
|
||||
|
||||
if (userInfo->user_level() & ServerInfo_User::IsRegistered) {
|
||||
tabDeckStorage = new TabDeckStorage(this, client);
|
||||
connect(tabDeckStorage, SIGNAL(openDeckEditor(const DeckLoader *)), this,
|
||||
SLOT(addDeckEditorTab(const DeckLoader *)));
|
||||
myAddTab(tabDeckStorage);
|
||||
|
||||
tabReplays = new TabReplays(this, client);
|
||||
connect(tabReplays, SIGNAL(openReplay(GameReplay *)), this, SLOT(openReplay(GameReplay *)));
|
||||
myAddTab(tabReplays);
|
||||
} else {
|
||||
tabDeckStorage = 0;
|
||||
tabReplays = 0;
|
||||
}
|
||||
|
||||
if (userInfo->user_level() & ServerInfo_User::IsModerator) {
|
||||
tabAdmin = new TabAdmin(this, client, (userInfo->user_level() & ServerInfo_User::IsAdmin));
|
||||
connect(tabAdmin, SIGNAL(adminLockChanged(bool)), this, SIGNAL(adminLockChanged(bool)));
|
||||
myAddTab(tabAdmin);
|
||||
tabsMenu->addSeparator();
|
||||
tabsMenu->addAction(aTabAdmin);
|
||||
tabsMenu->addAction(aTabLog);
|
||||
|
||||
tabLog = new TabLog(this, client);
|
||||
myAddTab(tabLog);
|
||||
} else {
|
||||
tabAdmin = 0;
|
||||
tabLog = 0;
|
||||
checkAndTrigger(aTabAdmin, SettingsCache::instance().getTabAdminOpen());
|
||||
checkAndTrigger(aTabLog, SettingsCache::instance().getTabLogOpen());
|
||||
}
|
||||
|
||||
retranslateUi();
|
||||
@@ -263,70 +372,186 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
|
||||
|
||||
void TabSupervisor::startLocal(const QList<AbstractClient *> &_clients)
|
||||
{
|
||||
tabUserLists = 0;
|
||||
tabDeckStorage = 0;
|
||||
tabReplays = 0;
|
||||
tabAdmin = 0;
|
||||
tabLog = 0;
|
||||
resetTabsMenu();
|
||||
|
||||
tabAccount = nullptr;
|
||||
tabAdmin = nullptr;
|
||||
tabLog = nullptr;
|
||||
isLocalGame = true;
|
||||
userInfo = new ServerInfo_User;
|
||||
localClients = _clients;
|
||||
for (int i = 0; i < localClients.size(); ++i)
|
||||
connect(localClients[i], SIGNAL(gameEventContainerReceived(const GameEventContainer &)), this,
|
||||
SLOT(processGameEventContainer(const GameEventContainer &)));
|
||||
connect(localClients.first(), SIGNAL(gameJoinedEventReceived(const Event_GameJoined &)), this,
|
||||
SLOT(localGameJoined(const Event_GameJoined &)));
|
||||
connect(localClients[i], &AbstractClient::gameEventContainerReceived, this,
|
||||
&TabSupervisor::processGameEventContainer);
|
||||
connect(localClients.first(), &AbstractClient::gameJoinedEventReceived, this, &TabSupervisor::localGameJoined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this when Cockatrice disconnects from the server in order to clean up.
|
||||
*/
|
||||
void TabSupervisor::stop()
|
||||
{
|
||||
if ((!client) && localClients.isEmpty())
|
||||
return;
|
||||
|
||||
resetTabsMenu();
|
||||
|
||||
if (!localClients.isEmpty()) {
|
||||
for (int i = 0; i < localClients.size(); ++i)
|
||||
localClients[i]->deleteLater();
|
||||
for (auto &localClient : localClients) {
|
||||
localClient->deleteLater();
|
||||
}
|
||||
localClients.clear();
|
||||
|
||||
emit localGameEnded();
|
||||
} else {
|
||||
if (tabUserLists)
|
||||
tabUserLists->deleteLater();
|
||||
if (tabServer)
|
||||
tabServer->deleteLater();
|
||||
if (tabDeckStorage)
|
||||
tabDeckStorage->deleteLater();
|
||||
if (tabReplays)
|
||||
tabReplays->deleteLater();
|
||||
if (tabAdmin)
|
||||
tabAdmin->deleteLater();
|
||||
if (tabLog)
|
||||
tabLog->deleteLater();
|
||||
if (tabAccount) {
|
||||
tabAccount->closeRequest(true);
|
||||
}
|
||||
if (tabServer) {
|
||||
tabServer->closeRequest(true);
|
||||
}
|
||||
if (tabAdmin) {
|
||||
tabAdmin->closeRequest(true);
|
||||
}
|
||||
if (tabLog) {
|
||||
tabLog->closeRequest(true);
|
||||
}
|
||||
}
|
||||
tabUserLists = 0;
|
||||
tabServer = 0;
|
||||
tabDeckStorage = 0;
|
||||
tabReplays = 0;
|
||||
tabAdmin = 0;
|
||||
tabLog = 0;
|
||||
|
||||
QMapIterator<int, TabRoom *> roomIterator(roomTabs);
|
||||
while (roomIterator.hasNext())
|
||||
roomIterator.next().value()->deleteLater();
|
||||
roomTabs.clear();
|
||||
QList<Tab *> tabsToDelete;
|
||||
|
||||
QMapIterator<int, TabGame *> gameIterator(gameTabs);
|
||||
while (gameIterator.hasNext())
|
||||
gameIterator.next().value()->deleteLater();
|
||||
gameTabs.clear();
|
||||
for (auto i = roomTabs.cbegin(), end = roomTabs.cend(); i != end; ++i) {
|
||||
tabsToDelete << i.value();
|
||||
}
|
||||
|
||||
QListIterator<TabGame *> replayIterator(replayTabs);
|
||||
while (replayIterator.hasNext())
|
||||
replayIterator.next()->deleteLater();
|
||||
replayTabs.clear();
|
||||
for (auto i = gameTabs.cbegin(), end = gameTabs.cend(); i != end; ++i) {
|
||||
tabsToDelete << i.value();
|
||||
}
|
||||
|
||||
for (auto i = messageTabs.cbegin(), end = messageTabs.cend(); i != end; ++i) {
|
||||
tabsToDelete << i.value();
|
||||
}
|
||||
|
||||
for (const auto tab : tabsToDelete) {
|
||||
tab->closeRequest(true);
|
||||
}
|
||||
|
||||
userListManager->handleDisconnect();
|
||||
|
||||
delete userInfo;
|
||||
userInfo = 0;
|
||||
userInfo = nullptr;
|
||||
}
|
||||
|
||||
void TabSupervisor::actTabVisualDeckStorage(bool checked)
|
||||
{
|
||||
SettingsCache::instance().setTabVisualDeckStorageOpen(checked);
|
||||
if (checked && !tabVisualDeckStorage) {
|
||||
tabVisualDeckStorage = new TabDeckStorageVisual(this);
|
||||
myAddTab(tabVisualDeckStorage, aTabVisualDeckStorage);
|
||||
connect(tabVisualDeckStorage, &Tab::closed, this, [this] {
|
||||
tabVisualDeckStorage = nullptr;
|
||||
aTabVisualDeckStorage->setChecked(false);
|
||||
});
|
||||
} else if (!checked && tabVisualDeckStorage) {
|
||||
tabVisualDeckStorage->closeRequest();
|
||||
}
|
||||
}
|
||||
|
||||
void TabSupervisor::actTabServer(bool checked)
|
||||
{
|
||||
SettingsCache::instance().setTabServerOpen(checked);
|
||||
if (checked && !tabServer) {
|
||||
tabServer = new TabServer(this, client);
|
||||
connect(tabServer, &TabServer::roomJoined, this, &TabSupervisor::addRoomTab);
|
||||
myAddTab(tabServer, aTabServer);
|
||||
connect(tabServer, &Tab::closed, this, [this] {
|
||||
tabServer = nullptr;
|
||||
aTabServer->setChecked(false);
|
||||
});
|
||||
} else if (!checked && tabServer) {
|
||||
tabServer->closeRequest();
|
||||
}
|
||||
}
|
||||
|
||||
void TabSupervisor::actTabAccount(bool checked)
|
||||
{
|
||||
SettingsCache::instance().setTabAccountOpen(checked);
|
||||
if (checked && !tabAccount) {
|
||||
tabAccount = new TabAccount(this, client, *userInfo);
|
||||
connect(tabAccount, &TabAccount::openMessageDialog, this, &TabSupervisor::addMessageTab);
|
||||
connect(tabAccount, &TabAccount::userJoined, this, &TabSupervisor::processUserJoined);
|
||||
connect(tabAccount, &TabAccount::userLeft, this, &TabSupervisor::processUserLeft);
|
||||
myAddTab(tabAccount, aTabAccount);
|
||||
connect(tabAccount, &Tab::closed, this, [this] {
|
||||
tabAccount = nullptr;
|
||||
aTabAccount->setChecked(false);
|
||||
});
|
||||
} else if (!checked && tabAccount) {
|
||||
tabAccount->closeRequest();
|
||||
}
|
||||
}
|
||||
|
||||
void TabSupervisor::actTabDeckStorage(bool checked)
|
||||
{
|
||||
SettingsCache::instance().setTabDeckStorageOpen(checked);
|
||||
if (checked && !tabDeckStorage) {
|
||||
tabDeckStorage = new TabDeckStorage(this, client, userInfo);
|
||||
connect(tabDeckStorage, &TabDeckStorage::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
|
||||
myAddTab(tabDeckStorage, aTabDeckStorage);
|
||||
connect(tabDeckStorage, &Tab::closed, this, [this] {
|
||||
tabDeckStorage = nullptr;
|
||||
aTabDeckStorage->setChecked(false);
|
||||
});
|
||||
} else if (!checked && tabDeckStorage) {
|
||||
tabDeckStorage->closeRequest();
|
||||
}
|
||||
}
|
||||
|
||||
void TabSupervisor::actTabReplays(bool checked)
|
||||
{
|
||||
SettingsCache::instance().setTabReplaysOpen(checked);
|
||||
if (checked && !tabReplays) {
|
||||
tabReplays = new TabReplays(this, client, userInfo);
|
||||
connect(tabReplays, &TabReplays::openReplay, this, &TabSupervisor::openReplay);
|
||||
myAddTab(tabReplays, aTabReplays);
|
||||
connect(tabReplays, &Tab::closed, this, [this] {
|
||||
tabReplays = nullptr;
|
||||
aTabReplays->setChecked(false);
|
||||
});
|
||||
} else if (!checked && tabReplays) {
|
||||
tabReplays->closeRequest();
|
||||
}
|
||||
}
|
||||
|
||||
void TabSupervisor::actTabAdmin(bool checked)
|
||||
{
|
||||
SettingsCache::instance().setTabAdminOpen(checked);
|
||||
if (checked && !tabAdmin) {
|
||||
tabAdmin = new TabAdmin(this, client, (userInfo->user_level() & ServerInfo_User::IsAdmin));
|
||||
connect(tabAdmin, &TabAdmin::adminLockChanged, this, &TabSupervisor::adminLockChanged);
|
||||
myAddTab(tabAdmin, aTabAdmin);
|
||||
connect(tabAdmin, &Tab::closed, this, [this] {
|
||||
tabAdmin = nullptr;
|
||||
aTabAdmin->setChecked(false);
|
||||
});
|
||||
} else if (!checked && tabAdmin) {
|
||||
tabAdmin->closeRequest();
|
||||
}
|
||||
}
|
||||
|
||||
void TabSupervisor::actTabLog(bool checked)
|
||||
{
|
||||
SettingsCache::instance().setTabLogOpen(checked);
|
||||
if (checked && !tabLog) {
|
||||
tabLog = new TabLog(this, client);
|
||||
myAddTab(tabLog, aTabLog);
|
||||
connect(tabLog, &Tab::closed, this, [this] {
|
||||
tabLog = nullptr;
|
||||
aTabAdmin->setChecked(false);
|
||||
});
|
||||
} else if (!checked && tabLog) {
|
||||
tabLog->closeRequest();
|
||||
}
|
||||
}
|
||||
|
||||
void TabSupervisor::updatePingTime(int value, int max)
|
||||
@@ -339,22 +564,6 @@ void TabSupervisor::updatePingTime(int value, int max)
|
||||
setTabIcon(indexOf(tabServer), QIcon(PingPixmapGenerator::generatePixmap(15, value, max)));
|
||||
}
|
||||
|
||||
void TabSupervisor::closeButtonPressed()
|
||||
{
|
||||
Tab *tab = static_cast<Tab *>(static_cast<CloseButton *>(sender())->property("tab").value<QObject *>());
|
||||
tab->closeRequest();
|
||||
}
|
||||
|
||||
void TabSupervisor::addCloseButtonToTab(Tab *tab, int tabIndex)
|
||||
{
|
||||
QTabBar::ButtonPosition closeSide =
|
||||
(QTabBar::ButtonPosition)tabBar()->style()->styleHint(QStyle::SH_TabBar_CloseButtonPosition, 0, tabBar());
|
||||
CloseButton *closeButton = new CloseButton;
|
||||
connect(closeButton, SIGNAL(clicked()), this, SLOT(closeButtonPressed()));
|
||||
closeButton->setProperty("tab", QVariant::fromValue((QObject *)tab));
|
||||
tabBar()->setTabButton(tabIndex, closeSide, closeButton);
|
||||
}
|
||||
|
||||
void TabSupervisor::gameJoined(const Event_GameJoined &event)
|
||||
{
|
||||
QMap<int, QString> roomGameTypes;
|
||||
@@ -366,23 +575,21 @@ void TabSupervisor::gameJoined(const Event_GameJoined &event)
|
||||
roomGameTypes.insert(event.game_types(i).game_type_id(),
|
||||
QString::fromStdString(event.game_types(i).description()));
|
||||
|
||||
TabGame *tab = new TabGame(this, QList<AbstractClient *>() << client, event, roomGameTypes);
|
||||
connect(tab, SIGNAL(gameClosing(TabGame *)), this, SLOT(gameLeft(TabGame *)));
|
||||
connect(tab, SIGNAL(openMessageDialog(const QString &, bool)), this, SLOT(addMessageTab(const QString &, bool)));
|
||||
connect(tab, SIGNAL(openDeckEditor(const DeckLoader *)), this, SLOT(addDeckEditorTab(const DeckLoader *)));
|
||||
int tabIndex = myAddTab(tab);
|
||||
addCloseButtonToTab(tab, tabIndex);
|
||||
auto *tab = new TabGame(this, QList<AbstractClient *>() << client, event, roomGameTypes);
|
||||
connect(tab, &TabGame::gameClosing, this, &TabSupervisor::gameLeft);
|
||||
connect(tab, &TabGame::openMessageDialog, this, &TabSupervisor::addMessageTab);
|
||||
connect(tab, &TabGame::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
|
||||
myAddTab(tab);
|
||||
gameTabs.insert(event.game_info().game_id(), tab);
|
||||
setCurrentWidget(tab);
|
||||
}
|
||||
|
||||
void TabSupervisor::localGameJoined(const Event_GameJoined &event)
|
||||
{
|
||||
TabGame *tab = new TabGame(this, localClients, event, QMap<int, QString>());
|
||||
connect(tab, SIGNAL(gameClosing(TabGame *)), this, SLOT(gameLeft(TabGame *)));
|
||||
connect(tab, SIGNAL(openDeckEditor(const DeckLoader *)), this, SLOT(addDeckEditorTab(const DeckLoader *)));
|
||||
int tabIndex = myAddTab(tab);
|
||||
addCloseButtonToTab(tab, tabIndex);
|
||||
auto *tab = new TabGame(this, localClients, event, QMap<int, QString>());
|
||||
connect(tab, &TabGame::gameClosing, this, &TabSupervisor::gameLeft);
|
||||
connect(tab, &TabGame::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
|
||||
myAddTab(tab);
|
||||
gameTabs.insert(event.game_info().game_id(), tab);
|
||||
setCurrentWidget(tab);
|
||||
|
||||
@@ -407,12 +614,11 @@ void TabSupervisor::gameLeft(TabGame *tab)
|
||||
|
||||
void TabSupervisor::addRoomTab(const ServerInfo_Room &info, bool setCurrent)
|
||||
{
|
||||
TabRoom *tab = new TabRoom(this, client, userInfo, info);
|
||||
connect(tab, SIGNAL(maximizeClient()), this, SLOT(maximizeMainWindow()));
|
||||
connect(tab, SIGNAL(roomClosing(TabRoom *)), this, SLOT(roomLeft(TabRoom *)));
|
||||
connect(tab, SIGNAL(openMessageDialog(const QString &, bool)), this, SLOT(addMessageTab(const QString &, bool)));
|
||||
int tabIndex = myAddTab(tab);
|
||||
addCloseButtonToTab(tab, tabIndex);
|
||||
auto *tab = new TabRoom(this, client, userInfo, info);
|
||||
connect(tab, &TabRoom::maximizeClient, this, &TabSupervisor::maximizeMainWindow);
|
||||
connect(tab, &TabRoom::roomClosing, this, &TabSupervisor::roomLeft);
|
||||
connect(tab, &TabRoom::openMessageDialog, this, &TabSupervisor::addMessageTab);
|
||||
myAddTab(tab);
|
||||
roomTabs.insert(info.room_id(), tab);
|
||||
if (setCurrent)
|
||||
setCurrentWidget(tab);
|
||||
@@ -429,10 +635,9 @@ void TabSupervisor::roomLeft(TabRoom *tab)
|
||||
|
||||
void TabSupervisor::openReplay(GameReplay *replay)
|
||||
{
|
||||
TabGame *replayTab = new TabGame(this, replay);
|
||||
connect(replayTab, SIGNAL(gameClosing(TabGame *)), this, SLOT(replayLeft(TabGame *)));
|
||||
int tabIndex = myAddTab(replayTab);
|
||||
addCloseButtonToTab(replayTab, tabIndex);
|
||||
auto *replayTab = new TabGame(this, replay);
|
||||
connect(replayTab, &TabGame::gameClosing, this, &TabSupervisor::replayLeft);
|
||||
myAddTab(replayTab);
|
||||
replayTabs.append(replayTab);
|
||||
setCurrentWidget(replayTab);
|
||||
}
|
||||
@@ -451,11 +656,11 @@ TabMessage *TabSupervisor::addMessageTab(const QString &receiverName, bool focus
|
||||
return nullptr;
|
||||
|
||||
ServerInfo_User otherUser;
|
||||
UserListTWI *twi = tabUserLists->getAllUsersList()->getUsers().value(receiverName);
|
||||
if (twi)
|
||||
otherUser = twi->getUserInfo();
|
||||
else
|
||||
if (auto user = userListManager->getOnlineUser(receiverName)) {
|
||||
otherUser = ServerInfo_User(*user);
|
||||
} else {
|
||||
otherUser.set_name(receiverName.toStdString());
|
||||
}
|
||||
|
||||
TabMessage *tab;
|
||||
tab = messageTabs.value(QString::fromStdString(otherUser.name()));
|
||||
@@ -466,10 +671,9 @@ TabMessage *TabSupervisor::addMessageTab(const QString &receiverName, bool focus
|
||||
}
|
||||
|
||||
tab = new TabMessage(this, client, *userInfo, otherUser);
|
||||
connect(tab, SIGNAL(talkClosing(TabMessage *)), this, SLOT(talkLeft(TabMessage *)));
|
||||
connect(tab, SIGNAL(maximizeClient()), this, SLOT(maximizeMainWindow()));
|
||||
int tabIndex = myAddTab(tab);
|
||||
addCloseButtonToTab(tab, tabIndex);
|
||||
connect(tab, &TabMessage::talkClosing, this, &TabSupervisor::talkLeft);
|
||||
connect(tab, &TabMessage::maximizeClient, this, &TabSupervisor::maximizeMainWindow);
|
||||
myAddTab(tab);
|
||||
messageTabs.insert(receiverName, tab);
|
||||
if (focus)
|
||||
setCurrentWidget(tab);
|
||||
@@ -492,18 +696,29 @@ void TabSupervisor::talkLeft(TabMessage *tab)
|
||||
|
||||
TabDeckEditor *TabSupervisor::addDeckEditorTab(const DeckLoader *deckToOpen)
|
||||
{
|
||||
TabDeckEditor *tab = new TabDeckEditor(this);
|
||||
auto *tab = new TabDeckEditor(this);
|
||||
if (deckToOpen)
|
||||
tab->setDeck(new DeckLoader(*deckToOpen));
|
||||
connect(tab, SIGNAL(deckEditorClosing(TabDeckEditor *)), this, SLOT(deckEditorClosed(TabDeckEditor *)));
|
||||
connect(tab, SIGNAL(openDeckEditor(const DeckLoader *)), this, SLOT(addDeckEditorTab(const DeckLoader *)));
|
||||
int tabIndex = myAddTab(tab);
|
||||
addCloseButtonToTab(tab, tabIndex);
|
||||
connect(tab, &TabDeckEditor::deckEditorClosing, this, &TabSupervisor::deckEditorClosed);
|
||||
connect(tab, &TabDeckEditor::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
|
||||
myAddTab(tab);
|
||||
deckEditorTabs.append(tab);
|
||||
setCurrentWidget(tab);
|
||||
return tab;
|
||||
}
|
||||
|
||||
TabEdhRec *TabSupervisor::addEdhrecTab(const CardInfoPtr &cardToQuery, bool isCommander)
|
||||
{
|
||||
auto *tab = new TabEdhRec(this);
|
||||
if (cardToQuery) {
|
||||
tab->setCard(cardToQuery, isCommander);
|
||||
}
|
||||
|
||||
myAddTab(tab);
|
||||
setCurrentWidget(tab);
|
||||
return tab;
|
||||
}
|
||||
|
||||
void TabSupervisor::deckEditorClosed(TabDeckEditor *tab)
|
||||
{
|
||||
if (tab == currentWidget())
|
||||
@@ -544,7 +759,7 @@ void TabSupervisor::processGameEventContainer(const GameEventContainer &cont)
|
||||
if (tab)
|
||||
tab->processGameEventContainer(cont, qobject_cast<AbstractClient *>(sender()), {});
|
||||
else
|
||||
qDebug() << "gameEvent: invalid gameId";
|
||||
qCDebug(TabSupervisorLog) << "gameEvent: invalid gameId";
|
||||
}
|
||||
|
||||
void TabSupervisor::processUserMessageEvent(const Event_UserMessage &event)
|
||||
@@ -554,9 +769,9 @@ void TabSupervisor::processUserMessageEvent(const Event_UserMessage &event)
|
||||
if (!tab)
|
||||
tab = messageTabs.value(QString::fromStdString(event.receiver_name()));
|
||||
if (!tab) {
|
||||
UserListTWI *twi = tabUserLists->getAllUsersList()->getUsers().value(senderName);
|
||||
if (twi) {
|
||||
UserLevelFlags userLevel = UserLevelFlags(twi->getUserInfo().user_level());
|
||||
const ServerInfo_User *onlineUserInfo = userListManager->getOnlineUser(senderName);
|
||||
if (onlineUserInfo) {
|
||||
auto userLevel = UserLevelFlags(onlineUserInfo->user_level());
|
||||
if (SettingsCache::instance().getIgnoreUnregisteredUserMessages() &&
|
||||
!userLevel.testFlag(ServerInfo_User::IsRegistered))
|
||||
// Flags are additive, so reg/mod/admin are all IsRegistered
|
||||
@@ -571,9 +786,9 @@ void TabSupervisor::processUserMessageEvent(const Event_UserMessage &event)
|
||||
|
||||
void TabSupervisor::actShowPopup(const QString &message)
|
||||
{
|
||||
qDebug() << "ACT SHOW POPUP";
|
||||
qCDebug(TabSupervisorLog) << "ACT SHOW POPUP";
|
||||
if (trayIcon && (QApplication::activeWindow() == nullptr || QApplication::focusWidget() == nullptr)) {
|
||||
qDebug() << "LAUNCHING POPUP";
|
||||
qCDebug(TabSupervisorLog) << "LAUNCHING POPUP";
|
||||
// disconnect(trayIcon, SIGNAL(messageClicked()), nullptr, nullptr);
|
||||
trayIcon->showMessage(message, tr("Click to view"));
|
||||
// connect(trayIcon, SIGNAL(messageClicked()), chatView, SLOT(actMessageClicked()));
|
||||
@@ -590,15 +805,15 @@ void TabSupervisor::processUserLeft(const QString &userName)
|
||||
void TabSupervisor::processUserJoined(const ServerInfo_User &userInfoJoined)
|
||||
{
|
||||
QString userName = QString::fromStdString(userInfoJoined.name());
|
||||
if (isUserBuddy(userName)) {
|
||||
Tab *tab = static_cast<Tab *>(getUserListsTab());
|
||||
|
||||
if (tab != currentWidget()) {
|
||||
tab->setContentsChanged(true);
|
||||
QPixmap avatarPixmap =
|
||||
UserLevelPixmapGenerator::generatePixmap(13, (UserLevelFlags)userInfoJoined.user_level(), true,
|
||||
QString::fromStdString(userInfoJoined.privlevel()));
|
||||
setTabIcon(indexOf(tab), QPixmap(avatarPixmap));
|
||||
if (userListManager->isUserBuddy(userName)) {
|
||||
if (auto *tab = getTabAccount()) {
|
||||
if (tab != currentWidget()) {
|
||||
tab->setContentsChanged(true);
|
||||
QIcon avatarIcon = UserLevelPixmapGenerator::generateIcon(
|
||||
13, (UserLevelFlags)userInfoJoined.user_level(), userInfoJoined.pawn_colors(), true,
|
||||
QString::fromStdString(userInfoJoined.privlevel()));
|
||||
setTabIcon(indexOf(tab), avatarIcon);
|
||||
}
|
||||
}
|
||||
|
||||
if (SettingsCache::instance().getBuddyConnectNotificationsEnabled()) {
|
||||
@@ -686,57 +901,6 @@ void TabSupervisor::processNotifyUserEvent(const Event_NotifyUser &event)
|
||||
}
|
||||
}
|
||||
|
||||
bool TabSupervisor::isOwnUserRegistered() const
|
||||
{
|
||||
return userInfo != nullptr && (userInfo->user_level() & ServerInfo_User::IsRegistered) != 0;
|
||||
}
|
||||
|
||||
QString TabSupervisor::getOwnUsername() const
|
||||
{
|
||||
return userInfo != nullptr ? QString::fromStdString(userInfo->name()) : QString();
|
||||
}
|
||||
|
||||
bool TabSupervisor::isUserBuddy(const QString &userName) const
|
||||
{
|
||||
if (!getUserListsTab())
|
||||
return false;
|
||||
if (!getUserListsTab()->getBuddyList())
|
||||
return false;
|
||||
QMap<QString, UserListTWI *> buddyList = getUserListsTab()->getBuddyList()->getUsers();
|
||||
bool senderIsBuddy = buddyList.contains(userName);
|
||||
return senderIsBuddy;
|
||||
}
|
||||
|
||||
bool TabSupervisor::isUserIgnored(const QString &userName) const
|
||||
{
|
||||
if (!getUserListsTab())
|
||||
return false;
|
||||
if (!getUserListsTab()->getIgnoreList())
|
||||
return false;
|
||||
QMap<QString, UserListTWI *> buddyList = getUserListsTab()->getIgnoreList()->getUsers();
|
||||
bool senderIsBuddy = buddyList.contains(userName);
|
||||
return senderIsBuddy;
|
||||
}
|
||||
|
||||
const ServerInfo_User *TabSupervisor::getOnlineUser(const QString &userName) const
|
||||
{
|
||||
if (!getUserListsTab())
|
||||
return nullptr;
|
||||
if (!getUserListsTab()->getAllUsersList())
|
||||
return nullptr;
|
||||
QMap<QString, UserListTWI *> userList = getUserListsTab()->getAllUsersList()->getUsers();
|
||||
const QString &userNameToMatchLower = userName.toLower();
|
||||
QMap<QString, UserListTWI *>::iterator i;
|
||||
|
||||
for (i = userList.begin(); i != userList.end(); ++i)
|
||||
if (i.key().toLower() == userNameToMatchLower) {
|
||||
const ServerInfo_User &_userInfo = i.value()->getUserInfo();
|
||||
return &_userInfo;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
bool TabSupervisor::switchToGameTabIfAlreadyExists(const int gameId)
|
||||
{
|
||||
bool isGameTabExists = false;
|
||||
|
||||
@@ -2,14 +2,20 @@
|
||||
#define TAB_SUPERVISOR_H
|
||||
|
||||
#include "../../deck/deck_loader.h"
|
||||
#include "../../server/chat_view/user_list_proxy.h"
|
||||
#include "../../server/user/user_list_proxy.h"
|
||||
#include "api/edhrec/tab_edhrec.h"
|
||||
#include "visual_deck_storage/tab_deck_storage_visual.h"
|
||||
|
||||
#include <QAbstractButton>
|
||||
#include <QCommonStyle>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMap>
|
||||
#include <QProxyStyle>
|
||||
#include <QTabWidget>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(TabSupervisorLog, "tab_supervisor");
|
||||
|
||||
class UserListManager;
|
||||
class QMenu;
|
||||
class AbstractClient;
|
||||
class Tab;
|
||||
@@ -20,7 +26,7 @@ class TabDeckStorage;
|
||||
class TabReplays;
|
||||
class TabAdmin;
|
||||
class TabMessage;
|
||||
class TabUserLists;
|
||||
class TabAccount;
|
||||
class TabDeckEditor;
|
||||
class TabLog;
|
||||
class RoomEvent;
|
||||
@@ -37,39 +43,42 @@ class MacOSTabFixStyle : public QProxyStyle
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
QRect subElementRect(SubElement, const QStyleOption *, const QWidget *) const;
|
||||
QRect subElementRect(SubElement, const QStyleOption *, const QWidget *) const override;
|
||||
};
|
||||
|
||||
class CloseButton : public QAbstractButton
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
CloseButton(QWidget *parent = nullptr);
|
||||
QSize sizeHint() const;
|
||||
inline QSize minimumSizeHint() const
|
||||
explicit CloseButton(QWidget *parent = nullptr);
|
||||
QSize sizeHint() const override;
|
||||
inline QSize minimumSizeHint() const override
|
||||
{
|
||||
return sizeHint();
|
||||
}
|
||||
|
||||
protected:
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
void enterEvent(QEnterEvent *event);
|
||||
void enterEvent(QEnterEvent *event) override;
|
||||
#else
|
||||
void enterEvent(QEvent *event);
|
||||
void enterEvent(QEvent *event) override;
|
||||
#endif
|
||||
void leaveEvent(QEvent *event);
|
||||
void paintEvent(QPaintEvent *event);
|
||||
void leaveEvent(QEvent *event) override;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
};
|
||||
|
||||
class TabSupervisor : public QTabWidget, public UserlistProxy
|
||||
class TabSupervisor : public QTabWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
private:
|
||||
ServerInfo_User *userInfo;
|
||||
AbstractClient *client;
|
||||
UserListManager *userListManager;
|
||||
QList<AbstractClient *> localClients;
|
||||
QMenu *tabsMenu;
|
||||
TabDeckStorageVisual *tabVisualDeckStorage;
|
||||
TabServer *tabServer;
|
||||
TabUserLists *tabUserLists;
|
||||
TabAccount *tabAccount;
|
||||
TabDeckStorage *tabDeckStorage;
|
||||
TabReplays *tabReplays;
|
||||
TabAdmin *tabAdmin;
|
||||
@@ -79,16 +88,22 @@ private:
|
||||
QList<TabGame *> replayTabs;
|
||||
QMap<QString, TabMessage *> messageTabs;
|
||||
QList<TabDeckEditor *> deckEditorTabs;
|
||||
int myAddTab(Tab *tab);
|
||||
void addCloseButtonToTab(Tab *tab, int tabIndex);
|
||||
QString sanitizeTabName(QString dirty) const;
|
||||
QString sanitizeHtml(QString dirty) const;
|
||||
bool isLocalGame;
|
||||
|
||||
QAction *aTabDeckEditor, *aTabVisualDeckStorage, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays,
|
||||
*aTabAdmin, *aTabLog;
|
||||
|
||||
int myAddTab(Tab *tab, QAction *manager = nullptr);
|
||||
void addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager);
|
||||
static QString sanitizeTabName(QString dirty);
|
||||
static QString sanitizeHtml(QString dirty);
|
||||
void resetTabsMenu();
|
||||
|
||||
public:
|
||||
TabSupervisor(AbstractClient *_client, QWidget *parent = nullptr);
|
||||
~TabSupervisor();
|
||||
explicit TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *parent = nullptr);
|
||||
~TabSupervisor() override;
|
||||
void retranslateUi();
|
||||
void initStartupTabs();
|
||||
void start(const ServerInfo_User &userInfo);
|
||||
void startLocal(const QList<AbstractClient *> &_clients);
|
||||
void stop();
|
||||
@@ -100,28 +115,31 @@ public:
|
||||
{
|
||||
return gameTabs.size();
|
||||
}
|
||||
TabUserLists *getUserListsTab() const
|
||||
TabAccount *getTabAccount() const
|
||||
{
|
||||
return tabUserLists;
|
||||
return tabAccount;
|
||||
}
|
||||
ServerInfo_User *getUserInfo() const
|
||||
{
|
||||
return userInfo;
|
||||
}
|
||||
AbstractClient *getClient() const;
|
||||
const UserListManager *getUserListManager() const
|
||||
{
|
||||
return userListManager;
|
||||
}
|
||||
const QMap<int, TabRoom *> &getRoomTabs() const
|
||||
{
|
||||
return roomTabs;
|
||||
}
|
||||
QList<TabDeckEditor *> getDeckEditorTabs() const
|
||||
{
|
||||
return deckEditorTabs;
|
||||
}
|
||||
bool getAdminLocked() const;
|
||||
bool closeRequest();
|
||||
bool isOwnUserRegistered() const;
|
||||
QString getOwnUsername() const;
|
||||
bool isUserBuddy(const QString &userName) const;
|
||||
bool isUserIgnored(const QString &userName) const;
|
||||
const ServerInfo_User *getOnlineUser(const QString &userName) const;
|
||||
bool switchToGameTabIfAlreadyExists(const int gameId);
|
||||
void actShowPopup(const QString &message);
|
||||
static void actShowPopup(const QString &message);
|
||||
signals:
|
||||
void setMenu(const QList<QMenu *> &newMenuList = QList<QMenu *>());
|
||||
void localGameEnded();
|
||||
@@ -130,10 +148,20 @@ signals:
|
||||
|
||||
public slots:
|
||||
TabDeckEditor *addDeckEditorTab(const DeckLoader *deckToOpen);
|
||||
TabEdhRec *addEdhrecTab(const CardInfoPtr &cardToQuery, bool isCommander = false);
|
||||
void openReplay(GameReplay *replay);
|
||||
void maximizeMainWindow();
|
||||
private slots:
|
||||
void closeButtonPressed();
|
||||
void refreshShortcuts();
|
||||
|
||||
void actTabVisualDeckStorage(bool checked);
|
||||
void actTabServer(bool checked);
|
||||
void actTabAccount(bool checked);
|
||||
void actTabDeckStorage(bool checked);
|
||||
void actTabReplays(bool checked);
|
||||
void actTabAdmin(bool checked);
|
||||
void actTabLog(bool checked);
|
||||
|
||||
void updateCurrent(int index);
|
||||
void updatePingTime(int value, int max);
|
||||
void gameJoined(const Event_GameJoined &event);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#include "tab_deck_storage_visual.h"
|
||||
|
||||
#include "../../../game/cards/card_database_model.h"
|
||||
#include "../../ui/widgets/cards/deck_preview_card_picture_widget.h"
|
||||
#include "../../ui/widgets/visual_deck_storage/visual_deck_storage_widget.h"
|
||||
#include "../tab_supervisor.h"
|
||||
#include "pb/command_deck_del.pb.h"
|
||||
|
||||
#include <QMouseEvent>
|
||||
|
||||
TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor)
|
||||
: Tab(_tabSupervisor), visualDeckStorageWidget(new VisualDeckStorageWidget(this))
|
||||
{
|
||||
connect(this, &TabDeckStorageVisual::openDeckEditor, tabSupervisor, &TabSupervisor::addDeckEditorTab);
|
||||
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::deckPreviewDoubleClicked, this,
|
||||
&TabDeckStorageVisual::actOpenLocalDeck);
|
||||
|
||||
auto *widget = new QWidget(this);
|
||||
auto *layout = new QVBoxLayout(widget);
|
||||
widget->setLayout(layout);
|
||||
this->setCentralWidget(widget);
|
||||
layout->addWidget(visualDeckStorageWidget);
|
||||
}
|
||||
|
||||
void TabDeckStorageVisual::actOpenLocalDeck(QMouseEvent * /*event*/, DeckPreviewWidget *instance)
|
||||
{
|
||||
DeckLoader deckLoader;
|
||||
if (!deckLoader.loadFromFile(instance->filePath, DeckLoader::getFormatFromName(instance->filePath), true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit openDeckEditor(&deckLoader);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#ifndef TAB_DECK_STORAGE_VISUAL_H
|
||||
#define TAB_DECK_STORAGE_VISUAL_H
|
||||
|
||||
#include "../tab.h"
|
||||
|
||||
class AbstractClient;
|
||||
class CommandContainer;
|
||||
class DeckLoader;
|
||||
class DeckPreviewWidget;
|
||||
class QFileSystemModel;
|
||||
class QGroupBox;
|
||||
class QToolBar;
|
||||
class QTreeView;
|
||||
class QTreeWidget;
|
||||
class QTreeWidgetItem;
|
||||
class Response;
|
||||
class VisualDeckStorageWidget;
|
||||
|
||||
class TabDeckStorageVisual final : public Tab
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TabDeckStorageVisual(TabSupervisor *_tabSupervisor);
|
||||
void retranslateUi() override{};
|
||||
[[nodiscard]] QString getTabText() const override
|
||||
{
|
||||
return tr("Visual Deck Storage");
|
||||
}
|
||||
|
||||
public slots:
|
||||
void actOpenLocalDeck(QMouseEvent * /*event*/, DeckPreviewWidget *instance);
|
||||
|
||||
signals:
|
||||
void openDeckEditor(const DeckLoader *deckLoader);
|
||||
|
||||
private:
|
||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -14,7 +14,7 @@ TappedOutInterface::TappedOutInterface(CardDatabase &_cardDatabase, QObject *par
|
||||
: QObject(parent), cardDatabase(_cardDatabase)
|
||||
{
|
||||
manager = new QNetworkAccessManager(this);
|
||||
connect(manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(queryFinished(QNetworkReply *)));
|
||||
connect(manager, &QNetworkAccessManager::finished, this, &TappedOutInterface::queryFinished);
|
||||
}
|
||||
|
||||
void TappedOutInterface::queryFinished(QNetworkReply *reply)
|
||||
@@ -33,7 +33,7 @@ void TappedOutInterface::queryFinished(QNetworkReply *reply)
|
||||
* can be extracted from the header. The http status is a 302 "redirect".
|
||||
*/
|
||||
QString deckUrl = reply->rawHeader("Location");
|
||||
qDebug() << "Tappedout: good reply, http status" << httpStatus << "location" << deckUrl;
|
||||
qCDebug(TappedOutInterfaceLog) << "Tappedout: good reply, http status" << httpStatus << "location" << deckUrl;
|
||||
QDesktopServices::openUrl("https://tappedout.net" + deckUrl);
|
||||
} else {
|
||||
/*
|
||||
@@ -57,8 +57,8 @@ void TappedOutInterface::queryFinished(QNetworkReply *reply)
|
||||
}
|
||||
|
||||
QString errorMessage = errorMessageList.join("\n");
|
||||
qDebug() << "Tappedout: bad reply, http status" << httpStatus << "size" << data.size() << "message"
|
||||
<< errorMessage;
|
||||
qCDebug(TappedOutInterfaceLog) << "Tappedout: bad reply, http status" << httpStatus << "size" << data.size()
|
||||
<< "message" << errorMessage;
|
||||
|
||||
QMessageBox::critical(nullptr, tr("Error"), errorMessage);
|
||||
}
|
||||
@@ -115,7 +115,7 @@ struct CopyMainOrSide
|
||||
}
|
||||
};
|
||||
|
||||
void TappedOutInterface::copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard)
|
||||
void TappedOutInterface::copyDeckSplitMainAndSide(DeckList &source, DeckList &mainboard, DeckList &sideboard)
|
||||
{
|
||||
CopyMainOrSide copyMainOrSide(cardDatabase, mainboard, sideboard);
|
||||
source.forEachCard(copyMainOrSide);
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
#include "../game/cards/card_database.h"
|
||||
#include "decklist.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QObject>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(TappedOutInterfaceLog, "tapped_out_interface");
|
||||
|
||||
class QByteArray;
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
@@ -24,13 +27,13 @@ private:
|
||||
QNetworkAccessManager *manager;
|
||||
|
||||
CardDatabase &cardDatabase;
|
||||
void copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard);
|
||||
void copyDeckSplitMainAndSide(DeckList &source, DeckList &mainboard, DeckList &sideboard);
|
||||
private slots:
|
||||
void queryFinished(QNetworkReply *reply);
|
||||
void getAnalyzeRequestData(DeckList *deck, QByteArray *data);
|
||||
|
||||
public:
|
||||
TappedOutInterface(CardDatabase &_cardDatabase, QObject *parent = nullptr);
|
||||
explicit TappedOutInterface(CardDatabase &_cardDatabase, QObject *parent = nullptr);
|
||||
void analyzeDeck(DeckList *deck);
|
||||
};
|
||||
|
||||
|
||||
@@ -20,8 +20,12 @@
|
||||
* @param hSpacing The horizontal spacing between items.
|
||||
* @param vSpacing The vertical spacing between items.
|
||||
*/
|
||||
FlowLayout::FlowLayout(QWidget *parent, const int margin, const int hSpacing, const int vSpacing)
|
||||
: QLayout(parent), horizontalMargin(hSpacing), verticalMargin(vSpacing)
|
||||
FlowLayout::FlowLayout(QWidget *parent,
|
||||
const Qt::Orientation _flowDirection,
|
||||
const int margin,
|
||||
const int hSpacing,
|
||||
const int vSpacing)
|
||||
: QLayout(parent), flowDirection(_flowDirection), horizontalMargin(hSpacing), verticalMargin(vSpacing)
|
||||
{
|
||||
setContentsMargins(margin, margin, margin, margin);
|
||||
}
|
||||
@@ -62,27 +66,51 @@ bool FlowLayout::hasHeightForWidth() const
|
||||
*/
|
||||
int FlowLayout::heightForWidth(const int width) const
|
||||
{
|
||||
int height = 0;
|
||||
int rowWidth = 0;
|
||||
int rowHeight = 0;
|
||||
if (flowDirection == Qt::Vertical) {
|
||||
int height = 0;
|
||||
int rowWidth = 0;
|
||||
int rowHeight = 0;
|
||||
|
||||
for (const QLayoutItem *item : items) {
|
||||
if (item == nullptr || item->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
for (const QLayoutItem *item : items) {
|
||||
if (item == nullptr || item->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int itemWidth = item->sizeHint().width() + horizontalSpacing();
|
||||
if (rowWidth + itemWidth > width) { // Start a new row if the row width exceeds available width
|
||||
height += rowHeight + verticalSpacing();
|
||||
rowWidth = itemWidth;
|
||||
rowHeight = item->sizeHint().height() + verticalSpacing();
|
||||
} else {
|
||||
rowWidth += itemWidth;
|
||||
rowHeight = qMax(rowHeight, item->sizeHint().height());
|
||||
int itemWidth = item->sizeHint().width() + horizontalSpacing();
|
||||
if (rowWidth + itemWidth > width) {
|
||||
height += rowHeight + verticalSpacing();
|
||||
rowWidth = itemWidth;
|
||||
rowHeight = item->sizeHint().height();
|
||||
} else {
|
||||
rowWidth += itemWidth;
|
||||
rowHeight = qMax(rowHeight, item->sizeHint().height());
|
||||
}
|
||||
}
|
||||
height += rowHeight; // Add height of the last row
|
||||
return height;
|
||||
} else {
|
||||
int width = 0;
|
||||
int colWidth = 0;
|
||||
int colHeight = 0;
|
||||
|
||||
for (const QLayoutItem *item : items) {
|
||||
if (item == nullptr || item->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int itemHeight = item->sizeHint().height();
|
||||
if (colHeight + itemHeight > width) {
|
||||
width += colWidth;
|
||||
colHeight = itemHeight;
|
||||
colWidth = item->sizeHint().width();
|
||||
} else {
|
||||
colHeight += itemHeight;
|
||||
colWidth = qMax(colWidth, item->sizeHint().width());
|
||||
}
|
||||
}
|
||||
width += colWidth; // Add width of the last column
|
||||
return width;
|
||||
}
|
||||
height += rowHeight; // Add the final row's height
|
||||
return height;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,132 +121,420 @@ void FlowLayout::setGeometry(const QRect &rect)
|
||||
{
|
||||
QLayout::setGeometry(rect); // Sets the geometry of the layout based on the given rectangle.
|
||||
|
||||
int left, top, right, bottom;
|
||||
getContentsMargins(&left, &top, &right, &bottom); // Retrieves the layout's content margins.
|
||||
if (flowDirection == Qt::Horizontal) {
|
||||
// If we have a parent scroll area, we're clamped to that, else we use our own rectangle.
|
||||
const int availableWidth = getParentScrollAreaWidth() == 0 ? rect.width() : getParentScrollAreaWidth();
|
||||
|
||||
// Adjust the rectangle to exclude margins.
|
||||
const QRect adjustedRect = rect.adjusted(+left, +top, -right, -bottom);
|
||||
const int totalHeight = layoutAllRows(rect.x(), rect.y(), availableWidth);
|
||||
|
||||
// Calculate the available width for items, considering either the adjusted rectangle's width
|
||||
// or the parent scroll area width, if applicable.
|
||||
const int availableWidth = qMax(adjustedRect.width(), getParentScrollAreaWidth());
|
||||
if (QWidget *parentWidgetPtr = parentWidget()) {
|
||||
parentWidgetPtr->setFixedSize(availableWidth, totalHeight);
|
||||
}
|
||||
} else {
|
||||
const int availableHeight = qMax(rect.height(), getParentScrollAreaHeight());
|
||||
|
||||
// Arrange all rows of items within the available width and get the total height used.
|
||||
const int totalHeight = layoutAllRows(adjustedRect.x(), adjustedRect.y(), availableWidth);
|
||||
const int totalWidth = layoutAllColumns(rect.x(), rect.y(), availableHeight);
|
||||
|
||||
// If the layout's parent is a QWidget, update its minimum size to ensure it can accommodate
|
||||
// the arranged items' dimensions.
|
||||
if (QWidget *parentWidgetPtr = parentWidget()) {
|
||||
parentWidgetPtr->setMinimumSize(availableWidth, totalHeight);
|
||||
if (QWidget *parentWidgetPtr = parentWidget()) {
|
||||
parentWidgetPtr->setFixedSize(totalWidth, availableHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Arranges items in rows based on the available width.
|
||||
* Items are added to a row until the row's width exceeds `availableWidth`.
|
||||
* Then, a new row is started.
|
||||
* @param originX The starting x-coordinate for the row layout.
|
||||
* @param originY The starting y-coordinate for the row layout.
|
||||
* @param availableWidth The available width to lay out items.
|
||||
* @return The y-coordinate of the final row's end position.
|
||||
* @brief Lays out items into rows according to the available width, starting from a given origin.
|
||||
* Each row is arranged within `availableWidth`, wrapping to a new row as necessary.
|
||||
* @param originX The x-coordinate for the layout start position.
|
||||
* @param originY The y-coordinate for the layout start position.
|
||||
* @param availableWidth The width within which each row is constrained.
|
||||
* @return The total height after arranging all rows.
|
||||
*/
|
||||
int FlowLayout::layoutAllRows(const int originX, const int originY, const int availableWidth)
|
||||
{
|
||||
QVector<QLayoutItem *> rowItems; // Temporary storage for items in the current row.
|
||||
int currentXPosition = originX; // Tracks the x-coordinate for placing items in the current row.
|
||||
int currentYPosition = originY; // Tracks the y-coordinate, updated after each row.
|
||||
QVector<QLayoutItem *> rowItems; // Holds items for the current row
|
||||
int currentXPosition = originX; // Tracks the x-coordinate while placing items
|
||||
int currentYPosition = originY; // Tracks the y-coordinate, moving down after each row
|
||||
|
||||
int rowHeight = 0; // Tracks the maximum height of items in the current row.
|
||||
int rowHeight = 0; // Tracks the maximum height of items in the current row
|
||||
|
||||
// Iterate through all layout items to arrange them.
|
||||
for (QLayoutItem *item : items) {
|
||||
if (item == nullptr || item->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QSize itemSize = item->sizeHint(); // The suggested size for the item.
|
||||
const int itemWidth = itemSize.width() + horizontalSpacing();
|
||||
QSize itemSize = item->sizeHint(); // The suggested size for the current item
|
||||
int itemWidth = itemSize.width() + horizontalSpacing(); // Item width plus spacing
|
||||
|
||||
// Check if the item fits in the current row's remaining width.
|
||||
// Check if the current item fits in the remaining width of the current row
|
||||
if (currentXPosition + itemWidth > availableWidth) {
|
||||
// If not, layout the current row and start a new row.
|
||||
// If not, layout the current row and start a new row
|
||||
layoutSingleRow(rowItems, originX, currentYPosition);
|
||||
rowItems.clear(); // Clear the temporary storage for the new row.
|
||||
currentXPosition = originX; // Reset x-position to the start of the new row.
|
||||
currentYPosition += rowHeight + verticalSpacing(); // Move y-position down for the new row.
|
||||
rowHeight = 0; // Reset row height for the new row.
|
||||
rowItems.clear(); // Reset the list for the new row
|
||||
currentXPosition = originX; // Reset x-position to the row's start
|
||||
currentYPosition += rowHeight + verticalSpacing(); // Move y-position down to the next row
|
||||
rowHeight = 0; // Reset row height for the new row
|
||||
}
|
||||
|
||||
// Add the item to the current row.
|
||||
// Add the item to the current row
|
||||
rowItems.append(item);
|
||||
rowHeight = qMax(rowHeight, itemSize.height()); // Update the row height to the tallest item.
|
||||
currentXPosition += itemSize.width() + horizontalSpacing(); // Move x-position for the next item.
|
||||
rowHeight = qMax(rowHeight, itemSize.height()); // Update the row's height to the tallest item
|
||||
currentXPosition += itemWidth + horizontalSpacing(); // Move x-position for the next item
|
||||
}
|
||||
|
||||
// Layout the final row if there are remaining items.
|
||||
// Layout the final row if there are any remaining items
|
||||
layoutSingleRow(rowItems, originX, currentYPosition);
|
||||
|
||||
currentYPosition += rowHeight; // Add the final row's height
|
||||
return currentYPosition;
|
||||
// Return the total height used, including the last row's height
|
||||
return currentYPosition + rowHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Helper function for arranging a single row of items within specified bounds.
|
||||
* @param rowItems Items to be arranged in the row.
|
||||
* @param x The x-coordinate for starting the row.
|
||||
* @param y The y-coordinate for starting the row.
|
||||
* @brief Arranges a single row of items within specified x and y starting positions.
|
||||
* @param rowItems A list of items to be arranged in the row.
|
||||
* @param x The starting x-coordinate for the row.
|
||||
* @param y The starting y-coordinate for the row.
|
||||
*/
|
||||
void FlowLayout::layoutSingleRow(const QVector<QLayoutItem *> &rowItems, int x, const int y)
|
||||
{
|
||||
// Iterate through each item in the row and position it.
|
||||
for (QLayoutItem *item : rowItems) {
|
||||
if (item == nullptr || item->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QSize itemMaxSize = item->widget()->maximumSize(); // Get the item's maximum allowable size.
|
||||
// Constrain the item's width and height to its size hint or maximum size.
|
||||
int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width());
|
||||
int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height());
|
||||
// Set the item's geometry based on the calculated size and position.
|
||||
// Get the maximum allowed size for the item
|
||||
QSize itemMaxSize = item->widget()->maximumSize();
|
||||
// Constrain the item's width and height to its size hint or maximum size
|
||||
const int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width());
|
||||
const int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height());
|
||||
// Set the item's geometry based on the computed size and position
|
||||
item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight)));
|
||||
// Move the x-position for the next item, including horizontal spacing.
|
||||
// Move the x-position to the right, leaving space for horizontal spacing
|
||||
x += itemWidth + horizontalSpacing();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the preferred size for this layout.
|
||||
* @return The maximum of all item size hints as a QSize.
|
||||
* @brief Lays out items into columns according to the available height, starting from a given origin.
|
||||
* Each column is arranged within `availableHeight`, wrapping to a new column as necessary.
|
||||
* @param originX The x-coordinate for the layout start position.
|
||||
* @param originY The y-coordinate for the layout start position.
|
||||
* @param availableHeight The height within which each column is constrained.
|
||||
* @return The total width after arranging all columns.
|
||||
*/
|
||||
QSize FlowLayout::sizeHint() const
|
||||
int FlowLayout::layoutAllColumns(const int originX, const int originY, const int availableHeight)
|
||||
{
|
||||
QSize size;
|
||||
for (const QLayoutItem *item : items) {
|
||||
if (item != nullptr && !item->isEmpty()) {
|
||||
size = size.expandedTo(item->sizeHint());
|
||||
QVector<QLayoutItem *> colItems; // Holds items for the current column
|
||||
int currentXPosition = originX; // Tracks the x-coordinate while placing items
|
||||
int currentYPosition = originY; // Tracks the y-coordinate, resetting for each new column
|
||||
|
||||
int colWidth = 0; // Tracks the maximum width of items in the current column
|
||||
|
||||
for (QLayoutItem *item : items) {
|
||||
if (item == nullptr || item->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QSize itemSize = item->sizeHint(); // The suggested size for the current item
|
||||
|
||||
// Check if the current item fits in the remaining height of the current column
|
||||
if (currentYPosition + itemSize.height() > availableHeight) {
|
||||
// If not, layout the current column and start a new column
|
||||
layoutSingleColumn(colItems, currentXPosition, originY);
|
||||
colItems.clear(); // Reset the list for the new column
|
||||
currentYPosition = originY; // Reset y-position to the column's start
|
||||
currentXPosition += colWidth; // Move x-position to the next column
|
||||
colWidth = 0; // Reset column width for the new column
|
||||
}
|
||||
|
||||
// Add the item to the current column
|
||||
colItems.append(item);
|
||||
colWidth = qMax(colWidth, itemSize.width()); // Update the column's width to the widest item
|
||||
currentYPosition += itemSize.height(); // Move y-position for the next item
|
||||
}
|
||||
return size.isValid() ? size : QSize(0, 0);
|
||||
|
||||
// Layout the final column if there are any remaining items
|
||||
layoutSingleColumn(colItems, currentXPosition, originY);
|
||||
|
||||
// Return the total width used, including the last column's width
|
||||
return currentXPosition + colWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the minimum size required to display all layout items.
|
||||
* @return The minimum QSize needed by the layout.
|
||||
* @brief Arranges a single column of items within specified x and y starting positions.
|
||||
* @param colItems A list of items to be arranged in the column.
|
||||
* @param x The starting x-coordinate for the column.
|
||||
* @param y The starting y-coordinate for the column.
|
||||
*/
|
||||
void FlowLayout::layoutSingleColumn(const QVector<QLayoutItem *> &colItems, const int x, int y)
|
||||
{
|
||||
for (QLayoutItem *item : colItems) {
|
||||
if (item == nullptr) {
|
||||
qCDebug(FlowLayoutLog) << "Item is null.";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item->isEmpty()) {
|
||||
qCDebug(FlowLayoutLog) << "Skipping empty item.";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Debugging: Print the item's widget class name and size hint
|
||||
QWidget *widget = item->widget();
|
||||
if (widget) {
|
||||
qCDebug(FlowLayoutLog) << "Widget class:" << widget->metaObject()->className();
|
||||
qCDebug(FlowLayoutLog) << "Widget size hint:" << widget->sizeHint();
|
||||
qCDebug(FlowLayoutLog) << "Widget maximum size:" << widget->maximumSize();
|
||||
qCDebug(FlowLayoutLog) << "Widget minimum size:" << widget->minimumSize();
|
||||
|
||||
// Debugging: Print child widgets
|
||||
const QObjectList &children = widget->children();
|
||||
qCDebug(FlowLayoutLog) << "Child widgets:";
|
||||
for (QObject *child : children) {
|
||||
if (QWidget *childWidget = qobject_cast<QWidget *>(child)) {
|
||||
qCDebug(FlowLayoutLog) << " - Child widget class:" << childWidget->metaObject()->className();
|
||||
qCDebug(FlowLayoutLog) << " Size hint:" << childWidget->sizeHint();
|
||||
qCDebug(FlowLayoutLog) << " Maximum size:" << childWidget->maximumSize();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
qCDebug(FlowLayoutLog) << "Item does not have a widget.";
|
||||
}
|
||||
|
||||
// Get the maximum allowed size for the item
|
||||
QSize itemMaxSize = widget->maximumSize();
|
||||
// Constrain the item's width and height to its size hint or maximum size
|
||||
const int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width());
|
||||
const int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height());
|
||||
// Debugging: Print the computed geometry
|
||||
qCDebug(FlowLayoutLog) << "Computed geometry: x=" << x << ", y=" << y << ", width=" << itemWidth
|
||||
<< ", height=" << itemHeight;
|
||||
|
||||
// Set the item's geometry based on the computed size and position
|
||||
item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight)));
|
||||
|
||||
// Move the y-position down by the item's height to place the next item below
|
||||
y += itemHeight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the preferred size of the layout based on the flow direction.
|
||||
* @return A QSize representing the ideal dimensions of the layout.
|
||||
*/
|
||||
QSize FlowLayout::sizeHint() const
|
||||
{
|
||||
if (flowDirection == Qt::Horizontal) {
|
||||
return calculateSizeHintHorizontal();
|
||||
} else {
|
||||
return calculateSizeHintVertical();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the minimum size required by the layout based on the flow direction.
|
||||
* @return A QSize representing the minimum required dimensions.
|
||||
*/
|
||||
QSize FlowLayout::minimumSize() const
|
||||
{
|
||||
QSize size;
|
||||
if (flowDirection == Qt::Horizontal) {
|
||||
return calculateMinimumSizeHorizontal();
|
||||
} else {
|
||||
return calculateMinimumSizeVertical();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the size hint for horizontal flow direction.
|
||||
* @return A QSize representing the preferred dimensions.
|
||||
*/
|
||||
QSize FlowLayout::calculateSizeHintHorizontal() const
|
||||
{
|
||||
int maxWidth = 0; // Tracks the maximum width needed
|
||||
int totalHeight = 0; // Tracks the total height across all rows
|
||||
int rowHeight = 0; // Tracks the height of the current row
|
||||
int currentWidth = 0; // Tracks the current row's width
|
||||
|
||||
const int availableWidth = getParentScrollAreaWidth() == 0 ? parentWidget()->width() : getParentScrollAreaWidth();
|
||||
|
||||
qCDebug(FlowLayoutLog) << "Calculating horizontal size hint. Available width:" << availableWidth;
|
||||
|
||||
for (const QLayoutItem *item : items) {
|
||||
if (item != nullptr && !item->isEmpty()) {
|
||||
size = size.expandedTo(item->minimumSize());
|
||||
if (!item || item->isEmpty()) {
|
||||
qCDebug(FlowLayoutLog) << "Skipping empty item.";
|
||||
continue;
|
||||
}
|
||||
|
||||
QSize itemSize = item->sizeHint();
|
||||
int itemWidth = itemSize.width() + horizontalSpacing();
|
||||
qCDebug(FlowLayoutLog) << "Processing item. Size:" << itemSize << "Width with spacing:" << itemWidth;
|
||||
|
||||
if (currentWidth + itemWidth > availableWidth) {
|
||||
qCDebug(FlowLayoutLog) << "Row overflow. Current width:" << currentWidth << "Row height:" << rowHeight;
|
||||
maxWidth = qMax(maxWidth, currentWidth);
|
||||
totalHeight += rowHeight + verticalSpacing();
|
||||
qCDebug(FlowLayoutLog) << "Updated total height:" << totalHeight << "Max width so far:" << maxWidth;
|
||||
|
||||
currentWidth = 0;
|
||||
rowHeight = 0;
|
||||
}
|
||||
|
||||
currentWidth += itemWidth;
|
||||
rowHeight = qMax(rowHeight, itemSize.height());
|
||||
qCDebug(FlowLayoutLog) << "Updated current width:" << currentWidth << "Updated row height:" << rowHeight;
|
||||
}
|
||||
|
||||
size.setWidth(qMin(size.width(), getParentScrollAreaWidth()));
|
||||
size.setHeight(qMin(size.height(), getParentScrollAreaHeight()));
|
||||
// Account for the final row
|
||||
maxWidth = qMax(maxWidth, currentWidth);
|
||||
totalHeight += rowHeight;
|
||||
qCDebug(FlowLayoutLog) << "Final total height:" << totalHeight << "Final max width:" << maxWidth;
|
||||
|
||||
return size.isValid() ? size : QSize(0, 0);
|
||||
return QSize(maxWidth, totalHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the minimum size for horizontal flow direction.
|
||||
* @return A QSize representing the minimum required dimensions.
|
||||
*/
|
||||
QSize FlowLayout::calculateMinimumSizeHorizontal() const
|
||||
{
|
||||
int maxWidth = 0; // Tracks the maximum width of a row
|
||||
int totalHeight = 0; // Tracks the total height across all rows
|
||||
int rowHeight = 0; // Tracks the height of the current row
|
||||
int currentWidth = 0; // Tracks the current row's width
|
||||
|
||||
const int availableWidth = getParentScrollAreaWidth() == 0 ? parentWidget()->width() : getParentScrollAreaWidth();
|
||||
|
||||
qCDebug(FlowLayoutLog) << "Calculating horizontal minimum size. Available width:" << availableWidth;
|
||||
|
||||
for (const QLayoutItem *item : items) {
|
||||
if (!item || item->isEmpty()) {
|
||||
qCDebug(FlowLayoutLog) << "Skipping empty item.";
|
||||
continue;
|
||||
}
|
||||
|
||||
QSize itemMinSize = item->minimumSize();
|
||||
int itemWidth = itemMinSize.width() + horizontalSpacing();
|
||||
qCDebug(FlowLayoutLog) << "Processing item. Minimum size:" << itemMinSize << "Width with spacing:" << itemWidth;
|
||||
|
||||
if (currentWidth + itemWidth > availableWidth) {
|
||||
qCDebug(FlowLayoutLog) << "Row overflow. Current width:" << currentWidth << "Row height:" << rowHeight;
|
||||
maxWidth = qMax(maxWidth, currentWidth);
|
||||
totalHeight += rowHeight + verticalSpacing();
|
||||
qCDebug(FlowLayoutLog) << "Updated total height:" << totalHeight << "Max width so far:" << maxWidth;
|
||||
|
||||
currentWidth = 0;
|
||||
rowHeight = 0;
|
||||
}
|
||||
|
||||
currentWidth += itemWidth;
|
||||
rowHeight = qMax(rowHeight, itemMinSize.height());
|
||||
qCDebug(FlowLayoutLog) << "Updated current width:" << currentWidth << "Updated row height:" << rowHeight;
|
||||
}
|
||||
|
||||
// Account for the final row
|
||||
maxWidth = qMax(maxWidth, currentWidth);
|
||||
totalHeight += rowHeight;
|
||||
qCDebug(FlowLayoutLog) << "Final total height:" << totalHeight << "Final max width:" << maxWidth;
|
||||
|
||||
return QSize(maxWidth, totalHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the size hint for vertical flow direction.
|
||||
* @return A QSize representing the preferred dimensions.
|
||||
*/
|
||||
QSize FlowLayout::calculateSizeHintVertical() const
|
||||
{
|
||||
int totalWidth = 0;
|
||||
int maxHeight = 0;
|
||||
int colWidth = 0;
|
||||
int currentHeight = 0;
|
||||
|
||||
const int availableHeight = qMax(parentWidget()->height(), getParentScrollAreaHeight());
|
||||
|
||||
qCDebug(FlowLayoutLog) << "Calculating vertical size hint. Available height:" << availableHeight;
|
||||
|
||||
for (const QLayoutItem *item : items) {
|
||||
if (!item || item->isEmpty()) {
|
||||
qCDebug(FlowLayoutLog) << "Skipping empty item.";
|
||||
continue;
|
||||
}
|
||||
|
||||
QSize itemSize = item->sizeHint();
|
||||
qCDebug(FlowLayoutLog) << "Processing item. Size:" << itemSize;
|
||||
|
||||
if (currentHeight + itemSize.height() > availableHeight) {
|
||||
qCDebug(FlowLayoutLog) << "Column overflow. Current height:" << currentHeight
|
||||
<< "Column width:" << colWidth;
|
||||
totalWidth += colWidth + horizontalSpacing();
|
||||
maxHeight = qMax(maxHeight, currentHeight);
|
||||
qCDebug(FlowLayoutLog) << "Updated total width:" << totalWidth << "Max height so far:" << maxHeight;
|
||||
|
||||
currentHeight = 0;
|
||||
colWidth = 0;
|
||||
}
|
||||
|
||||
currentHeight += itemSize.height() + verticalSpacing();
|
||||
colWidth = qMax(colWidth, itemSize.width());
|
||||
qCDebug(FlowLayoutLog) << "Updated current height:" << currentHeight << "Updated column width:" << colWidth;
|
||||
}
|
||||
|
||||
// Account for the final column
|
||||
totalWidth += colWidth;
|
||||
maxHeight = qMax(maxHeight, currentHeight);
|
||||
qCDebug(FlowLayoutLog) << "Final total width:" << totalWidth << "Final max height:" << maxHeight;
|
||||
|
||||
return QSize(totalWidth, maxHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the minimum size for vertical flow direction.
|
||||
* @return A QSize representing the minimum required dimensions.
|
||||
*/
|
||||
QSize FlowLayout::calculateMinimumSizeVertical() const
|
||||
{
|
||||
int totalWidth = 0; // Tracks the total width across all columns
|
||||
int maxHeight = 0; // Tracks the maximum height of a column
|
||||
int colWidth = 0; // Tracks the width of the current column
|
||||
int currentHeight = 0; // Tracks the current column's height
|
||||
|
||||
const int availableHeight = qMax(parentWidget()->height(), getParentScrollAreaHeight());
|
||||
|
||||
qCDebug(FlowLayoutLog) << "Calculating vertical minimum size. Available height:" << availableHeight;
|
||||
|
||||
for (const QLayoutItem *item : items) {
|
||||
if (!item || item->isEmpty()) {
|
||||
qCDebug(FlowLayoutLog) << "Skipping empty item.";
|
||||
continue;
|
||||
}
|
||||
|
||||
QSize itemMinSize = item->minimumSize();
|
||||
int itemHeight = itemMinSize.height() + verticalSpacing();
|
||||
qCDebug(FlowLayoutLog) << "Processing item. Minimum size:" << itemMinSize
|
||||
<< "Height with spacing:" << itemHeight;
|
||||
|
||||
if (currentHeight + itemHeight > availableHeight) {
|
||||
qCDebug(FlowLayoutLog) << "Column overflow. Current height:" << currentHeight
|
||||
<< "Column width:" << colWidth;
|
||||
totalWidth += colWidth + horizontalSpacing();
|
||||
maxHeight = qMax(maxHeight, currentHeight);
|
||||
qCDebug(FlowLayoutLog) << "Updated total width:" << totalWidth << "Max height so far:" << maxHeight;
|
||||
|
||||
currentHeight = 0;
|
||||
colWidth = 0;
|
||||
}
|
||||
|
||||
currentHeight += itemHeight;
|
||||
colWidth = qMax(colWidth, itemMinSize.width());
|
||||
qCDebug(FlowLayoutLog) << "Updated current height:" << currentHeight << "Updated column width:" << colWidth;
|
||||
}
|
||||
|
||||
// Account for the final column
|
||||
totalWidth += colWidth;
|
||||
maxHeight = qMax(maxHeight, currentHeight);
|
||||
qCDebug(FlowLayoutLog) << "Final total width:" << totalWidth << "Final max height:" << maxHeight;
|
||||
|
||||
return QSize(totalWidth, maxHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,16 +3,22 @@
|
||||
|
||||
#include <QLayout>
|
||||
#include <QList>
|
||||
#include <QLoggingCategory>
|
||||
#include <QWidget>
|
||||
#include <qstyle.h>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(FlowLayoutLog, "flow_layout");
|
||||
|
||||
class FlowLayout : public QLayout
|
||||
{
|
||||
public:
|
||||
explicit FlowLayout(QWidget *parent = nullptr);
|
||||
FlowLayout(QWidget *parent, int margin, int hSpacing, int vSpacing);
|
||||
FlowLayout(QWidget *parent, Qt::Orientation _flowDirection, int margin = 0, int hSpacing = 0, int vSpacing = 0);
|
||||
~FlowLayout() override;
|
||||
|
||||
QSize calculateMinimumSizeHorizontal() const;
|
||||
QSize calculateSizeHintVertical() const;
|
||||
QSize calculateMinimumSizeVertical() const;
|
||||
void addItem(QLayoutItem *item) override;
|
||||
[[nodiscard]] int count() const override;
|
||||
[[nodiscard]] QLayoutItem *itemAt(int index) const override;
|
||||
@@ -31,11 +37,15 @@ public:
|
||||
void setGeometry(const QRect &rect) override;
|
||||
virtual int layoutAllRows(int originX, int originY, int availableWidth);
|
||||
virtual void layoutSingleRow(const QVector<QLayoutItem *> &rowItems, int x, int y);
|
||||
int layoutAllColumns(int originX, int originY, int availableHeight);
|
||||
void layoutSingleColumn(const QVector<QLayoutItem *> &colItems, int x, int y);
|
||||
[[nodiscard]] QSize sizeHint() const override;
|
||||
[[nodiscard]] QSize minimumSize() const override;
|
||||
QSize calculateSizeHintHorizontal() const;
|
||||
|
||||
protected:
|
||||
QList<QLayoutItem *> items; // List to store layout items
|
||||
Qt::Orientation flowDirection;
|
||||
int horizontalMargin;
|
||||
int verticalMargin;
|
||||
};
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
#include "horizontal_flow_layout.h"
|
||||
|
||||
/**
|
||||
* @brief Constructs a HorizontalFlowLayout instance with the specified parent widget.
|
||||
* This layout arranges items in columns within the given height, automatically adjusting its width.
|
||||
* @param parent The parent widget to which this layout belongs.
|
||||
* @param margin The layout margin.
|
||||
* @param hSpacing The horizontal spacing between items.
|
||||
* @param vSpacing The vertical spacing between items.
|
||||
*/
|
||||
HorizontalFlowLayout::HorizontalFlowLayout(QWidget *parent, const int margin, const int hSpacing, const int vSpacing)
|
||||
: FlowLayout(parent, margin, hSpacing, vSpacing)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Destructor for HorizontalFlowLayout, responsible for cleaning up layout items.
|
||||
*/
|
||||
HorizontalFlowLayout::~HorizontalFlowLayout()
|
||||
{
|
||||
QLayoutItem *item;
|
||||
while ((item = FlowLayout::takeAt(0))) {
|
||||
delete item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the required width to display all items, given a specified height.
|
||||
* This method arranges items into columns and determines the total width needed.
|
||||
* @param height The available height for arranging layout items.
|
||||
* @return The total width required to fit all items, organized in columns constrained by the given height.
|
||||
*/
|
||||
int HorizontalFlowLayout::heightForWidth(const int height) const
|
||||
{
|
||||
int width = 0;
|
||||
int colWidth = 0;
|
||||
int colHeight = 0;
|
||||
|
||||
for (const QLayoutItem *item : items) {
|
||||
if (item == nullptr || item->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int itemHeight = item->sizeHint().height();
|
||||
if (colHeight + itemHeight > height) {
|
||||
width += colWidth;
|
||||
colHeight = itemHeight;
|
||||
colWidth = item->sizeHint().width();
|
||||
} else {
|
||||
colHeight += itemHeight;
|
||||
colWidth = qMax(colWidth, item->sizeHint().width());
|
||||
}
|
||||
}
|
||||
width += colWidth; // Add width of the last column
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the geometry of the layout items, arranging them in columns within the given height.
|
||||
* @param rect The rectangle area defining the layout space.
|
||||
*/
|
||||
void HorizontalFlowLayout::setGeometry(const QRect &rect)
|
||||
{
|
||||
const int availableHeight = qMax(rect.height(), getParentScrollAreaHeight());
|
||||
|
||||
const int totalWidth = layoutAllColumns(rect.x(), rect.y(), availableHeight);
|
||||
|
||||
if (QWidget *parentWidgetPtr = parentWidget()) {
|
||||
parentWidgetPtr->setMinimumSize(totalWidth, availableHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Lays out items into columns according to the available height, starting from a given origin.
|
||||
* Each column is arranged within `availableHeight`, wrapping to a new column as necessary.
|
||||
* @param originX The x-coordinate for the layout start position.
|
||||
* @param originY The y-coordinate for the layout start position.
|
||||
* @param availableHeight The height within which each column is constrained.
|
||||
* @return The total width after arranging all columns.
|
||||
*/
|
||||
int HorizontalFlowLayout::layoutAllColumns(const int originX, const int originY, const int availableHeight)
|
||||
{
|
||||
QVector<QLayoutItem *> colItems; // Holds items for the current column
|
||||
int currentXPosition = originX; // Tracks the x-coordinate while placing items
|
||||
int currentYPosition = originY; // Tracks the y-coordinate, resetting for each new column
|
||||
|
||||
int colWidth = 0; // Tracks the maximum width of items in the current column
|
||||
|
||||
for (QLayoutItem *item : items) {
|
||||
if (item == nullptr || item->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QSize itemSize = item->sizeHint(); // The suggested size for the current item
|
||||
|
||||
// Check if the current item fits in the remaining height of the current column
|
||||
if (currentYPosition + itemSize.height() > availableHeight) {
|
||||
// If not, layout the current column and start a new column
|
||||
layoutSingleColumn(colItems, currentXPosition, originY);
|
||||
colItems.clear(); // Reset the list for the new column
|
||||
currentYPosition = originY; // Reset y-position to the column's start
|
||||
currentXPosition += colWidth; // Move x-position to the next column
|
||||
colWidth = 0; // Reset column width for the new column
|
||||
}
|
||||
|
||||
// Add the item to the current column
|
||||
colItems.append(item);
|
||||
colWidth = qMax(colWidth, itemSize.width()); // Update the column's width to the widest item
|
||||
currentYPosition += itemSize.height(); // Move y-position for the next item
|
||||
}
|
||||
|
||||
// Layout the final column if there are any remaining items
|
||||
layoutSingleColumn(colItems, currentXPosition, originY);
|
||||
|
||||
// Return the total width used, including the last column's width
|
||||
return currentXPosition + colWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Arranges a single column of items within specified x and y starting positions.
|
||||
* @param colItems A list of items to be arranged in the column.
|
||||
* @param x The starting x-coordinate for the column.
|
||||
* @param y The starting y-coordinate for the column.
|
||||
*/
|
||||
void HorizontalFlowLayout::layoutSingleColumn(const QVector<QLayoutItem *> &colItems, const int x, int y)
|
||||
{
|
||||
for (QLayoutItem *item : colItems) {
|
||||
if (item != nullptr && item->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the maximum allowed size for the item
|
||||
QSize itemMaxSize = item->widget()->maximumSize();
|
||||
// Constrain the item's width and height to its size hint or maximum size
|
||||
const int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width());
|
||||
const int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height());
|
||||
// Set the item's geometry based on the computed size and position
|
||||
item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight)));
|
||||
// Move the y-position down by the item's height to place the next item below
|
||||
y += itemHeight;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
#ifndef HORIZONTAL_FLOW_LAYOUT_H
|
||||
#define HORIZONTAL_FLOW_LAYOUT_H
|
||||
|
||||
#include "flow_layout.h"
|
||||
|
||||
class HorizontalFlowLayout : public FlowLayout
|
||||
{
|
||||
public:
|
||||
explicit HorizontalFlowLayout(QWidget *parent = nullptr, int margin = 0, int hSpacing = 0, int vSpacing = 0);
|
||||
~HorizontalFlowLayout() override;
|
||||
|
||||
[[nodiscard]] int heightForWidth(int height) const override;
|
||||
|
||||
void setGeometry(const QRect &rect) override;
|
||||
int layoutAllColumns(int originX, int originY, int availableHeight);
|
||||
static void layoutSingleColumn(const QVector<QLayoutItem *> &colItems, int x, int y);
|
||||
};
|
||||
|
||||
#endif // HORIZONTAL_FLOW_LAYOUT_H
|
||||
@@ -1,144 +0,0 @@
|
||||
#include "vertical_flow_layout.h"
|
||||
|
||||
/**
|
||||
* @brief Constructs a VerticalFlowLayout instance with the specified parent widget.
|
||||
* This layout arranges items in rows within the given width, automatically adjusting its height.
|
||||
* @param parent The parent widget to which this layout belongs.
|
||||
* @param margin The layout margin.
|
||||
* @param hSpacing The horizontal spacing between items.
|
||||
* @param vSpacing The vertical spacing between items.
|
||||
*/
|
||||
VerticalFlowLayout::VerticalFlowLayout(QWidget *parent, const int margin, const int hSpacing, const int vSpacing)
|
||||
: FlowLayout(parent, margin, hSpacing, vSpacing)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Destructor for VerticalFlowLayout, responsible for cleaning up layout items.
|
||||
*/
|
||||
VerticalFlowLayout::~VerticalFlowLayout()
|
||||
{
|
||||
QLayoutItem *item;
|
||||
while ((item = FlowLayout::takeAt(0))) {
|
||||
delete item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the required height to display all items, given a specified width.
|
||||
* This method arranges items into rows and determines the total height needed.
|
||||
* @param width The available width for arranging layout items.
|
||||
* @return The total height required to fit all items, organized in rows constrained by the given width.
|
||||
*/
|
||||
int VerticalFlowLayout::heightForWidth(const int width) const
|
||||
{
|
||||
int height = 0;
|
||||
int rowWidth = 0;
|
||||
int rowHeight = 0;
|
||||
|
||||
for (const QLayoutItem *item : items) {
|
||||
if (item == nullptr || item->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int itemWidth = item->sizeHint().width() + horizontalSpacing();
|
||||
if (rowWidth + itemWidth > width) {
|
||||
height += rowHeight + verticalSpacing();
|
||||
rowWidth = itemWidth;
|
||||
rowHeight = item->sizeHint().height();
|
||||
} else {
|
||||
rowWidth += itemWidth;
|
||||
rowHeight = qMax(rowHeight, item->sizeHint().height());
|
||||
}
|
||||
}
|
||||
height += rowHeight; // Add height of the last row
|
||||
return height;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the geometry of the layout items, arranging them in rows within the given width.
|
||||
* @param rect The rectangle area defining the layout space.
|
||||
*/
|
||||
void VerticalFlowLayout::setGeometry(const QRect &rect)
|
||||
{
|
||||
// If we have a parent scroll area, we're clamped to that, else we use our own rectangle.
|
||||
const int availableWidth = getParentScrollAreaWidth() == 0 ? rect.width() : getParentScrollAreaWidth();
|
||||
|
||||
const int totalHeight = layoutAllRows(rect.x(), rect.y(), availableWidth);
|
||||
|
||||
if (QWidget *parentWidgetPtr = parentWidget()) {
|
||||
parentWidgetPtr->setMinimumSize(availableWidth, totalHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Lays out items into rows according to the available width, starting from a given origin.
|
||||
* Each row is arranged within `availableWidth`, wrapping to a new row as necessary.
|
||||
* @param originX The x-coordinate for the layout start position.
|
||||
* @param originY The y-coordinate for the layout start position.
|
||||
* @param availableWidth The width within which each row is constrained.
|
||||
* @return The total height after arranging all rows.
|
||||
*/
|
||||
int VerticalFlowLayout::layoutAllRows(const int originX, const int originY, const int availableWidth)
|
||||
{
|
||||
QVector<QLayoutItem *> rowItems; // Holds items for the current row
|
||||
int currentXPosition = originX; // Tracks the x-coordinate while placing items
|
||||
int currentYPosition = originY; // Tracks the y-coordinate, moving down after each row
|
||||
|
||||
int rowHeight = 0; // Tracks the maximum height of items in the current row
|
||||
|
||||
for (QLayoutItem *item : items) {
|
||||
if (item == nullptr || item->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QSize itemSize = item->sizeHint(); // The suggested size for the current item
|
||||
int itemWidth = itemSize.width() + horizontalSpacing(); // Item width plus spacing
|
||||
|
||||
// Check if the current item fits in the remaining width of the current row
|
||||
if (currentXPosition + itemWidth > availableWidth) {
|
||||
// If not, layout the current row and start a new row
|
||||
layoutSingleRow(rowItems, originX, currentYPosition);
|
||||
rowItems.clear(); // Reset the list for the new row
|
||||
currentXPosition = originX; // Reset x-position to the row's start
|
||||
currentYPosition += rowHeight + verticalSpacing(); // Move y-position down to the next row
|
||||
rowHeight = 0; // Reset row height for the new row
|
||||
}
|
||||
|
||||
// Add the item to the current row
|
||||
rowItems.append(item);
|
||||
rowHeight = qMax(rowHeight, itemSize.height()); // Update the row's height to the tallest item
|
||||
currentXPosition += itemWidth + horizontalSpacing(); // Move x-position for the next item
|
||||
}
|
||||
|
||||
// Layout the final row if there are any remaining items
|
||||
layoutSingleRow(rowItems, originX, currentYPosition);
|
||||
|
||||
// Return the total height used, including the last row's height
|
||||
return currentYPosition + rowHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Arranges a single row of items within specified x and y starting positions.
|
||||
* @param rowItems A list of items to be arranged in the row.
|
||||
* @param x The starting x-coordinate for the row.
|
||||
* @param y The starting y-coordinate for the row.
|
||||
*/
|
||||
void VerticalFlowLayout::layoutSingleRow(const QVector<QLayoutItem *> &rowItems, int x, const int y)
|
||||
{
|
||||
for (QLayoutItem *item : rowItems) {
|
||||
if (item == nullptr || item->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the maximum allowed size for the item
|
||||
QSize itemMaxSize = item->widget()->maximumSize();
|
||||
// Constrain the item's width and height to its size hint or maximum size
|
||||
const int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width());
|
||||
const int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height());
|
||||
// Set the item's geometry based on the computed size and position
|
||||
item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight)));
|
||||
// Move the x-position to the right, leaving space for horizontal spacing
|
||||
x += itemWidth + horizontalSpacing();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
#ifndef VERTICAL_FLOW_LAYOUT_H
|
||||
#define VERTICAL_FLOW_LAYOUT_H
|
||||
|
||||
#include "flow_layout.h"
|
||||
|
||||
class VerticalFlowLayout : public FlowLayout
|
||||
{
|
||||
public:
|
||||
explicit VerticalFlowLayout(QWidget *parent = nullptr, int margin = 0, int hSpacing = 0, int vSpacing = 0);
|
||||
~VerticalFlowLayout() override;
|
||||
|
||||
[[nodiscard]] int heightForWidth(int width) const override;
|
||||
|
||||
void setGeometry(const QRect &rect) override;
|
||||
int layoutAllRows(int originX, int originY, int availableWidth) override;
|
||||
void layoutSingleRow(const QVector<QLayoutItem *> &rowItems, int x, int y) override;
|
||||
};
|
||||
|
||||
#endif // VERTICAL_FLOW_LAYOUT_H
|
||||
@@ -1,835 +0,0 @@
|
||||
#include "picture_loader.h"
|
||||
|
||||
#include "../../game/cards/card_database_manager.h"
|
||||
#include "../../settings/cache_settings.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QBuffer>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDebug>
|
||||
#include <QDirIterator>
|
||||
#include <QFileInfo>
|
||||
#include <QImageReader>
|
||||
#include <QMovie>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QPainter>
|
||||
#include <QPixmapCache>
|
||||
#include <QRegularExpression>
|
||||
#include <QScreen>
|
||||
#include <QSet>
|
||||
#include <QThread>
|
||||
#include <QUrl>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
// never cache more than 300 cards at once for a single deck
|
||||
#define CACHED_CARD_PER_DECK_MAX 300
|
||||
|
||||
PictureToLoad::PictureToLoad(CardInfoPtr _card)
|
||||
: card(std::move(_card)), urlTemplates(SettingsCache::instance().downloads().getAllURLs())
|
||||
{
|
||||
if (card) {
|
||||
for (const auto &cardInfoPerSetList : card->getSets()) {
|
||||
for (const auto &set : cardInfoPerSetList) {
|
||||
sortedSets << set.getPtr();
|
||||
}
|
||||
}
|
||||
if (sortedSets.empty()) {
|
||||
sortedSets << CardSet::newInstance("", "", "", QDate());
|
||||
}
|
||||
std::sort(sortedSets.begin(), sortedSets.end(), SetDownloadPriorityComparator());
|
||||
|
||||
// If the user hasn't disabled arts other than their personal preference...
|
||||
if (!SettingsCache::instance().getOverrideAllCardArtWithPersonalPreference()) {
|
||||
// If the pixmapCacheKey corresponds to a specific set, we have to try to load it first.
|
||||
for (const auto &cardInfoPerSetList : card->getSets()) {
|
||||
for (const auto &set : cardInfoPerSetList) {
|
||||
if (QLatin1String("card_") + card->getName() + QString("_") + QString(set.getProperty("uuid")) ==
|
||||
card->getPixmapCacheKey()) {
|
||||
long long setIndex = sortedSets.indexOf(set.getPtr());
|
||||
CardSetPtr setForCardProviderID = sortedSets.takeAt(setIndex);
|
||||
sortedSets.prepend(setForCardProviderID);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// The first time called, nextSet will also populate the Urls for the first set.
|
||||
nextSet();
|
||||
}
|
||||
}
|
||||
|
||||
void PictureToLoad::populateSetUrls()
|
||||
{
|
||||
/* currentSetUrls is a list, populated each time a new set is requested for a particular card
|
||||
and Urls are removed from it as a download is attempted from each one. Custom Urls for
|
||||
a set are given higher priority, so should be placed first in the list. */
|
||||
currentSetUrls.clear();
|
||||
|
||||
if (card && currentSet) {
|
||||
QString setCustomURL = card->getCustomPicURL(currentSet->getShortName());
|
||||
|
||||
if (!setCustomURL.isEmpty()) {
|
||||
currentSetUrls.append(setCustomURL);
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString &urlTemplate : urlTemplates) {
|
||||
QString transformedUrl = transformUrl(urlTemplate);
|
||||
|
||||
if (!transformedUrl.isEmpty()) {
|
||||
currentSetUrls.append(transformedUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/* Call nextUrl to make sure currentUrl is up-to-date
|
||||
but we don't need the result here. */
|
||||
(void)nextUrl();
|
||||
}
|
||||
|
||||
bool PictureToLoad::nextSet()
|
||||
{
|
||||
if (!sortedSets.isEmpty()) {
|
||||
currentSet = sortedSets.takeFirst();
|
||||
populateSetUrls();
|
||||
return true;
|
||||
}
|
||||
currentSet = {};
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PictureToLoad::nextUrl()
|
||||
{
|
||||
if (!currentSetUrls.isEmpty()) {
|
||||
currentUrl = currentSetUrls.takeFirst();
|
||||
return true;
|
||||
}
|
||||
currentUrl = QString();
|
||||
return false;
|
||||
}
|
||||
|
||||
QString PictureToLoad::getSetName() const
|
||||
{
|
||||
if (currentSet) {
|
||||
return currentSet->getCorrectedShortName();
|
||||
} else {
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
// Card back returned by gatherer when card is not found
|
||||
QStringList PictureLoaderWorker::md5Blacklist = QStringList() << "db0c48db407a907c16ade38de048a441";
|
||||
|
||||
PictureLoaderWorker::PictureLoaderWorker()
|
||||
: QObject(nullptr), picsPath(SettingsCache::instance().getPicsPath()),
|
||||
customPicsPath(SettingsCache::instance().getCustomPicsPath()),
|
||||
picDownload(SettingsCache::instance().getPicDownload()), downloadRunning(false), loadQueueRunning(false)
|
||||
{
|
||||
connect(this, SIGNAL(startLoadQueue()), this, SLOT(processLoadQueue()), Qt::QueuedConnection);
|
||||
connect(&SettingsCache::instance(), SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged()));
|
||||
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
|
||||
|
||||
networkManager = new QNetworkAccessManager(this);
|
||||
// We need a timeout to ensure requests don't hang indefinitely in case of
|
||||
// cache corruption, see related Qt bug: https://bugreports.qt.io/browse/QTBUG-111397
|
||||
// Use Qt's default timeout (30s, as of 2023-02-22)
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
|
||||
networkManager->setTransferTimeout();
|
||||
#endif
|
||||
auto cache = new QNetworkDiskCache(this);
|
||||
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
|
||||
cache->setMaximumCacheSize(1024L * 1024L *
|
||||
static_cast<qint64>(SettingsCache::instance().getNetworkCacheSizeInMB()));
|
||||
// Note: the settings is in MB, but QNetworkDiskCache uses bytes
|
||||
connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, cache,
|
||||
[cache](int newSizeInMB) { cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB)); });
|
||||
networkManager->setCache(cache);
|
||||
// Use a ManualRedirectPolicy since we keep track of redirects in picDownloadFinished
|
||||
// We can't use NoLessSafeRedirectPolicy because it is not applied with AlwaysCache
|
||||
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
|
||||
connect(networkManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(picDownloadFinished(QNetworkReply *)));
|
||||
|
||||
cacheFilePath = SettingsCache::instance().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
|
||||
loadRedirectCache();
|
||||
cleanStaleEntries();
|
||||
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this,
|
||||
&PictureLoaderWorker::saveRedirectCache);
|
||||
|
||||
pictureLoaderThread = new QThread;
|
||||
pictureLoaderThread->start(QThread::LowPriority);
|
||||
moveToThread(pictureLoaderThread);
|
||||
}
|
||||
|
||||
PictureLoaderWorker::~PictureLoaderWorker()
|
||||
{
|
||||
pictureLoaderThread->deleteLater();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::processLoadQueue()
|
||||
{
|
||||
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();
|
||||
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName
|
||||
<< "]: Trying to load picture";
|
||||
|
||||
if (CardDatabaseManager::getInstance()->isProviderIdForPreferredPrinting(
|
||||
cardName, cardBeingLoaded.getCard()->getPixmapCacheKey())) {
|
||||
if (cardImageExistsOnDisk(setName, correctedCardName)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug().nospace() << "PictureLoader: [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)
|
||||
{
|
||||
QImage image;
|
||||
QImageReader imgReader;
|
||||
imgReader.setDecideFormatFromContent(true);
|
||||
QList<QString> picsPaths = QList<QString>();
|
||||
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)) {
|
||||
qDebug().nospace() << "PictureLoader: [card: " << correctedCardname << " set: " << setName
|
||||
<< "]: Picture found on disk.";
|
||||
imageLoaded(cardBeingLoaded.getCard(), image);
|
||||
return true;
|
||||
}
|
||||
imgReader.setFileName(_picsPath + ".full");
|
||||
if (imgReader.read(&image)) {
|
||||
qDebug().nospace() << "PictureLoader: [card: " << correctedCardname << " set: " << setName
|
||||
<< "]: Picture.full found on disk.";
|
||||
imageLoaded(cardBeingLoaded.getCard(), image);
|
||||
return true;
|
||||
}
|
||||
imgReader.setFileName(_picsPath + ".xlhq");
|
||||
if (imgReader.read(&image)) {
|
||||
qDebug().nospace() << "PictureLoader: [card: " << correctedCardname << " set: " << setName
|
||||
<< "]: Picture.xlhq found on disk.";
|
||||
imageLoaded(cardBeingLoaded.getCard(), image);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static int parse(const QString &urlTemplate,
|
||||
const QString &propType,
|
||||
const QString &cardName,
|
||||
const QString &setName,
|
||||
std::function<QString(const QString &)> getProperty,
|
||||
QMap<QString, QString> &transformMap)
|
||||
{
|
||||
static const QRegularExpression rxFillWith("^(.+)_fill_with_(.+)$");
|
||||
static const QRegularExpression rxSubStr("^(.+)_substr_(\\d+)_(\\d+)$");
|
||||
|
||||
const QRegularExpression rxCardProp("!" + propType + ":([^!]+)!");
|
||||
|
||||
auto matches = rxCardProp.globalMatch(urlTemplate);
|
||||
while (matches.hasNext()) {
|
||||
auto match = matches.next();
|
||||
QString templatePropertyName = match.captured(1);
|
||||
auto fillMatch = rxFillWith.match(templatePropertyName);
|
||||
QString cardPropertyName;
|
||||
QString fillWith;
|
||||
int subStrPos = 0;
|
||||
int subStrLen = -1;
|
||||
if (fillMatch.hasMatch()) {
|
||||
cardPropertyName = fillMatch.captured(1);
|
||||
fillWith = fillMatch.captured(2);
|
||||
} else {
|
||||
fillWith = QString();
|
||||
auto subStrMatch = rxSubStr.match(templatePropertyName);
|
||||
if (subStrMatch.hasMatch()) {
|
||||
cardPropertyName = subStrMatch.captured(1);
|
||||
subStrPos = subStrMatch.captured(2).toInt();
|
||||
subStrLen = subStrMatch.captured(3).toInt();
|
||||
} else {
|
||||
cardPropertyName = templatePropertyName;
|
||||
}
|
||||
}
|
||||
QString propertyValue = getProperty(cardPropertyName);
|
||||
if (propertyValue.isEmpty()) {
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested "
|
||||
<< propType << "property (" << cardPropertyName << ") for Url template (" << urlTemplate
|
||||
<< ") is not available";
|
||||
return 1;
|
||||
} else {
|
||||
int propLength = propertyValue.length();
|
||||
if (subStrLen > 0) {
|
||||
if (subStrPos + subStrLen > propLength) {
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested "
|
||||
<< propType << " property (" << cardPropertyName << ") for Url template ("
|
||||
<< urlTemplate << ") is smaller than substr specification (" << subStrPos
|
||||
<< " + " << subStrLen << " > " << propLength << ")";
|
||||
return 1;
|
||||
} else {
|
||||
propertyValue = propertyValue.mid(subStrPos, subStrLen);
|
||||
propLength = subStrLen;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fillWith.isEmpty()) {
|
||||
int fillLength = fillWith.length();
|
||||
if (fillLength < propLength) {
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested "
|
||||
<< propType << " property (" << cardPropertyName << ") for Url template ("
|
||||
<< urlTemplate << ") is longer than fill specification (" << fillWith << ")";
|
||||
return 1;
|
||||
} else {
|
||||
|
||||
propertyValue = fillWith.left(fillLength - propLength) + propertyValue;
|
||||
}
|
||||
}
|
||||
|
||||
transformMap["!" + propType + ":" + templatePropertyName + "!"] = propertyValue;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
QString PictureToLoad::transformUrl(const QString &urlTemplate) const
|
||||
{
|
||||
/* This function takes Url templates and substitutes actual card details
|
||||
into the url. This is used for making Urls with follow a predictable format
|
||||
for downloading images. If information is requested by the template that is
|
||||
not populated for this specific card/set combination, an empty string is returned.*/
|
||||
|
||||
CardSetPtr set = getCurrentSet();
|
||||
|
||||
QMap<QString, QString> transformMap = QMap<QString, QString>();
|
||||
QString setName = getSetName();
|
||||
|
||||
// name
|
||||
QString cardName = card->getName();
|
||||
transformMap["!name!"] = cardName;
|
||||
transformMap["!name_lower!"] = card->getName().toLower();
|
||||
transformMap["!corrected_name!"] = card->getCorrectedName();
|
||||
transformMap["!corrected_name_lower!"] = card->getCorrectedName().toLower();
|
||||
|
||||
// card properties
|
||||
if (parse(
|
||||
urlTemplate, "prop", cardName, setName, [&](const QString &name) { return card->getProperty(name); },
|
||||
transformMap)) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
if (set) {
|
||||
transformMap["!setcode!"] = set->getShortName();
|
||||
transformMap["!setcode_lower!"] = set->getShortName().toLower();
|
||||
transformMap["!setname!"] = set->getLongName();
|
||||
transformMap["!setname_lower!"] = set->getLongName().toLower();
|
||||
|
||||
if (parse(
|
||||
urlTemplate, "set", cardName, setName,
|
||||
[&](const QString &name) { return card->getSetProperty(set->getShortName(), name); }, transformMap)) {
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
// language setting
|
||||
transformMap["!sflang!"] = QString(QCoreApplication::translate(
|
||||
"PictureLoader", "en", "code for scryfall's language property, not available for all languages"));
|
||||
|
||||
QString transformedUrl = urlTemplate;
|
||||
for (const QString &prop : transformMap.keys()) {
|
||||
if (transformedUrl.contains(prop)) {
|
||||
if (!transformMap[prop].isEmpty()) {
|
||||
transformedUrl.replace(prop, QUrl::toPercentEncoding(transformMap[prop]));
|
||||
} else {
|
||||
/* This means the template is requesting information that is not
|
||||
* populated in this card, so it should return an empty string,
|
||||
* indicating an invalid Url.
|
||||
*/
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName
|
||||
<< "]: Requested information (" << prop << ") for Url template (" << urlTemplate
|
||||
<< ") is not available";
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return transformedUrl;
|
||||
}
|
||||
|
||||
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);
|
||||
qDebug().nospace() << "PictureLoader: [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 {
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Picture NOT found, "
|
||||
<< (picDownload ? "download failed" : "downloads disabled")
|
||||
<< ", no more url combinations to try: BAILING OUT";
|
||||
imageLoaded(cardBeingDownloaded.getCard(), QImage());
|
||||
cardBeingDownloaded.clear();
|
||||
}
|
||||
emit startLoadQueue();
|
||||
}
|
||||
|
||||
bool PictureLoaderWorker::imageIsBlackListed(const QByteArray &picData)
|
||||
{
|
||||
QString md5sum = QCryptographicHash::hash(picData, QCryptographicHash::Md5).toHex();
|
||||
return md5Blacklist.contains(md5sum);
|
||||
}
|
||||
|
||||
QNetworkReply *PictureLoaderWorker::makeRequest(const QUrl &url)
|
||||
{
|
||||
// Check if the redirect is cached
|
||||
QUrl cachedRedirect = getCachedRedirect(url);
|
||||
if (!cachedRedirect.isEmpty()) {
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Using cached redirect for "
|
||||
<< url.toDisplayString() << " to " << cachedRedirect.toDisplayString();
|
||||
return makeRequest(cachedRedirect); // Use the cached redirect
|
||||
}
|
||||
|
||||
QNetworkRequest req(url);
|
||||
|
||||
if (!picDownload) {
|
||||
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache);
|
||||
}
|
||||
|
||||
QNetworkReply *reply = networkManager->get(req);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, url]() {
|
||||
QVariant redirectTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
|
||||
|
||||
if (redirectTarget.isValid()) {
|
||||
QUrl redirectUrl = redirectTarget.toUrl();
|
||||
if (redirectUrl.isRelative()) {
|
||||
redirectUrl = url.resolved(redirectUrl);
|
||||
}
|
||||
|
||||
cacheRedirect(url, redirectUrl);
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Caching redirect from "
|
||||
<< url.toDisplayString() << " to " << redirectUrl.toDisplayString();
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
});
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl)
|
||||
{
|
||||
redirectCache[originalUrl] = qMakePair(redirectUrl, QDateTime::currentDateTimeUtc());
|
||||
saveRedirectCache();
|
||||
}
|
||||
|
||||
QUrl PictureLoaderWorker::getCachedRedirect(const QUrl &originalUrl) const
|
||||
{
|
||||
if (redirectCache.contains(originalUrl)) {
|
||||
return redirectCache[originalUrl].first;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::loadRedirectCache()
|
||||
{
|
||||
QSettings settings(cacheFilePath, QSettings::IniFormat);
|
||||
|
||||
redirectCache.clear();
|
||||
int size = settings.beginReadArray(REDIRECT_HEADER_NAME);
|
||||
for (int i = 0; i < size; ++i) {
|
||||
settings.setArrayIndex(i);
|
||||
QUrl originalUrl = settings.value(REDIRECT_ORIGINAL_URL).toUrl();
|
||||
QUrl redirectUrl = settings.value(REDIRECT_URL).toUrl();
|
||||
QDateTime timestamp = settings.value(REDIRECT_TIMESTAMP).toDateTime();
|
||||
|
||||
if (originalUrl.isValid() && redirectUrl.isValid()) {
|
||||
redirectCache[originalUrl] = qMakePair(redirectUrl, timestamp);
|
||||
}
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::saveRedirectCache() const
|
||||
{
|
||||
QSettings settings(cacheFilePath, QSettings::IniFormat);
|
||||
|
||||
settings.beginWriteArray(REDIRECT_HEADER_NAME, static_cast<int>(redirectCache.size()));
|
||||
int index = 0;
|
||||
for (auto it = redirectCache.cbegin(); it != redirectCache.cend(); ++it) {
|
||||
settings.setArrayIndex(index++);
|
||||
settings.setValue(REDIRECT_ORIGINAL_URL, it.key());
|
||||
settings.setValue(REDIRECT_URL, it.value().first);
|
||||
settings.setValue(REDIRECT_TIMESTAMP, it.value().second);
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::cleanStaleEntries()
|
||||
{
|
||||
QDateTime now = QDateTime::currentDateTimeUtc();
|
||||
|
||||
auto it = redirectCache.begin();
|
||||
while (it != redirectCache.end()) {
|
||||
if (it.value().second.addDays(SettingsCache::instance().getRedirectCacheTtl()) < now) {
|
||||
it = redirectCache.erase(it); // Remove stale entry
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::picDownloadFinished(QNetworkReply *reply)
|
||||
{
|
||||
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
|
||||
|
||||
if (reply->error()) {
|
||||
if (isFromCache) {
|
||||
qDebug().nospace() << "PictureLoader: [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 {
|
||||
qDebug().nospace() << "PictureLoader: [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();
|
||||
qDebug().nospace() << "PictureLoader: [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)) {
|
||||
qDebug().nospace() << "PictureLoader: [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 {
|
||||
qDebug().nospace() << "PictureLoader: [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) {
|
||||
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Image successfully "
|
||||
<< (isFromCache ? "loaded from cached" : "downloaded from") << " url "
|
||||
<< reply->url().toDisplayString();
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
startNextPicDownload();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::enqueueImageLoad(CardInfoPtr card)
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
|
||||
// avoid queueing the same card more than once
|
||||
if (!card || card == cardBeingLoaded.getCard() || card == cardBeingDownloaded.getCard()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const PictureToLoad &pic : loadQueue) {
|
||||
if (pic.getCard() == card)
|
||||
return;
|
||||
}
|
||||
|
||||
for (const PictureToLoad &pic : cardsToDownload) {
|
||||
if (pic.getCard() == card)
|
||||
return;
|
||||
}
|
||||
|
||||
loadQueue.append(PictureToLoad(card));
|
||||
emit startLoadQueue();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::picDownloadChanged()
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
picDownload = SettingsCache::instance().getPicDownload();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::picsPathChanged()
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
picsPath = SettingsCache::instance().getPicsPath();
|
||||
customPicsPath = SettingsCache::instance().getCustomPicsPath();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::clearNetworkCache()
|
||||
{
|
||||
networkManager->cache()->clear();
|
||||
}
|
||||
|
||||
PictureLoader::PictureLoader() : QObject(nullptr)
|
||||
{
|
||||
worker = new PictureLoaderWorker;
|
||||
connect(&SettingsCache::instance(), SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged()));
|
||||
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
|
||||
|
||||
connect(worker, SIGNAL(imageLoaded(CardInfoPtr, const QImage &)), this,
|
||||
SLOT(imageLoaded(CardInfoPtr, const QImage &)));
|
||||
}
|
||||
|
||||
PictureLoader::~PictureLoader()
|
||||
{
|
||||
worker->deleteLater();
|
||||
}
|
||||
|
||||
void PictureLoader::getCardBackPixmap(QPixmap &pixmap, QSize size)
|
||||
{
|
||||
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
|
||||
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
||||
qDebug() << "PictureLoader: cache fail for" << backCacheKey;
|
||||
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
QPixmapCache::insert(backCacheKey, pixmap);
|
||||
}
|
||||
}
|
||||
|
||||
void PictureLoader::getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSize size)
|
||||
{
|
||||
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
|
||||
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
||||
qDebug() << "PictureLoader: cache fail for" << backCacheKey;
|
||||
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
QPixmapCache::insert(backCacheKey, pixmap);
|
||||
}
|
||||
}
|
||||
|
||||
void PictureLoader::getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize size)
|
||||
{
|
||||
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
|
||||
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
||||
qDebug() << "PictureLoader: cache fail for" << backCacheKey;
|
||||
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
QPixmapCache::insert(backCacheKey, pixmap);
|
||||
}
|
||||
}
|
||||
|
||||
void PictureLoader::getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size)
|
||||
{
|
||||
if (card == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// search for an exact size copy of the picture in cache
|
||||
QString key = card->getPixmapCacheKey();
|
||||
QString sizeKey = key + QLatin1Char('_') + QString::number(size.width()) + QString::number(size.height());
|
||||
if (QPixmapCache::find(sizeKey, &pixmap))
|
||||
return;
|
||||
|
||||
// load the image and create a copy of the correct size
|
||||
QPixmap bigPixmap;
|
||||
if (QPixmapCache::find(key, &bigPixmap)) {
|
||||
QScreen *screen = qApp->primaryScreen();
|
||||
qreal dpr = screen->devicePixelRatio();
|
||||
pixmap = bigPixmap.scaled(size * dpr, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
pixmap.setDevicePixelRatio(dpr);
|
||||
QPixmapCache::insert(sizeKey, pixmap);
|
||||
return;
|
||||
}
|
||||
|
||||
// add the card to the load queue
|
||||
getInstance().worker->enqueueImageLoad(card);
|
||||
}
|
||||
|
||||
void PictureLoader::imageLoaded(CardInfoPtr card, const QImage &image)
|
||||
{
|
||||
if (image.isNull()) {
|
||||
QPixmapCache::insert(card->getPixmapCacheKey(), QPixmap());
|
||||
} else {
|
||||
if (card->getUpsideDownArt()) {
|
||||
QImage mirrorImage = image.mirrored(true, true);
|
||||
QPixmapCache::insert(card->getPixmapCacheKey(), QPixmap::fromImage(mirrorImage));
|
||||
} else {
|
||||
QPixmapCache::insert(card->getPixmapCacheKey(), QPixmap::fromImage(image));
|
||||
}
|
||||
}
|
||||
|
||||
card->emitPixmapUpdated();
|
||||
}
|
||||
|
||||
void PictureLoader::clearPixmapCache(CardInfoPtr card)
|
||||
{
|
||||
if (card) {
|
||||
QPixmapCache::remove(card->getPixmapCacheKey());
|
||||
}
|
||||
}
|
||||
|
||||
void PictureLoader::clearPixmapCache()
|
||||
{
|
||||
QPixmapCache::clear();
|
||||
}
|
||||
|
||||
void PictureLoader::clearNetworkCache()
|
||||
{
|
||||
getInstance().worker->clearNetworkCache();
|
||||
}
|
||||
|
||||
void PictureLoader::cacheCardPixmaps(QList<CardInfoPtr> cards)
|
||||
{
|
||||
QPixmap tmp;
|
||||
int max = qMin(cards.size(), CACHED_CARD_PER_DECK_MAX);
|
||||
for (int i = 0; i < max; ++i) {
|
||||
const CardInfoPtr &card = cards.at(i);
|
||||
if (!card) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString key = card->getPixmapCacheKey();
|
||||
if (QPixmapCache::find(key, &tmp)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
getInstance().worker->enqueueImageLoad(card);
|
||||
}
|
||||
}
|
||||
|
||||
void PictureLoader::picDownloadChanged()
|
||||
{
|
||||
QPixmapCache::clear();
|
||||
}
|
||||
|
||||
void PictureLoader::picsPathChanged()
|
||||
{
|
||||
QPixmapCache::clear();
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
#ifndef PICTURELOADER_H
|
||||
#define PICTURELOADER_H
|
||||
|
||||
#include "../../game/cards/card_database.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QMutex>
|
||||
#include <QNetworkRequest>
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class 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"
|
||||
|
||||
class PictureToLoad
|
||||
{
|
||||
private:
|
||||
class SetDownloadPriorityComparator
|
||||
{
|
||||
public:
|
||||
/*
|
||||
* Returns true if a has higher download priority than b
|
||||
* Enabled sets have priority over disabled sets
|
||||
* Both groups follows the user-defined order
|
||||
*/
|
||||
inline bool operator()(const CardSetPtr &a, const CardSetPtr &b) const
|
||||
{
|
||||
if (a->getEnabled()) {
|
||||
return !b->getEnabled() || a->getSortKey() < b->getSortKey();
|
||||
} else {
|
||||
return !b->getEnabled() && a->getSortKey() < b->getSortKey();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
CardInfoPtr card;
|
||||
QList<CardSetPtr> sortedSets;
|
||||
QList<QString> urlTemplates;
|
||||
QList<QString> currentSetUrls;
|
||||
QString currentUrl;
|
||||
CardSetPtr currentSet;
|
||||
|
||||
public:
|
||||
explicit PictureToLoad(CardInfoPtr _card = CardInfoPtr());
|
||||
|
||||
CardInfoPtr getCard() const
|
||||
{
|
||||
return card;
|
||||
}
|
||||
void clear()
|
||||
{
|
||||
card.clear();
|
||||
}
|
||||
QString getCurrentUrl() const
|
||||
{
|
||||
return currentUrl;
|
||||
}
|
||||
CardSetPtr getCurrentSet() const
|
||||
{
|
||||
return currentSet;
|
||||
}
|
||||
QString getSetName() const;
|
||||
QString transformUrl(const QString &urlTemplate) const;
|
||||
bool nextSet();
|
||||
bool nextUrl();
|
||||
void populateSetUrls();
|
||||
};
|
||||
|
||||
class PictureLoaderWorker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PictureLoaderWorker();
|
||||
~PictureLoaderWorker() override;
|
||||
|
||||
void enqueueImageLoad(CardInfoPtr card);
|
||||
void clearNetworkCache();
|
||||
|
||||
private:
|
||||
static QStringList md5Blacklist;
|
||||
|
||||
QThread *pictureLoaderThread;
|
||||
QString picsPath, customPicsPath;
|
||||
QList<PictureToLoad> loadQueue;
|
||||
QMutex mutex;
|
||||
QNetworkAccessManager *networkManager;
|
||||
QHash<QUrl, QPair<QUrl, QDateTime>> redirectCache; // Stores redirect and timestamp
|
||||
QString cacheFilePath; // Path to persistent storage
|
||||
static constexpr int CacheTTLInDays = 30; // TODO: Make user configurable
|
||||
QList<PictureToLoad> cardsToDownload;
|
||||
PictureToLoad cardBeingLoaded;
|
||||
PictureToLoad cardBeingDownloaded;
|
||||
bool picDownload, downloadRunning, loadQueueRunning;
|
||||
void startNextPicDownload();
|
||||
bool cardImageExistsOnDisk(QString &setName, QString &correctedCardName);
|
||||
bool imageIsBlackListed(const QByteArray &);
|
||||
QNetworkReply *makeRequest(const QUrl &url);
|
||||
void cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl);
|
||||
QUrl getCachedRedirect(const QUrl &originalUrl) const;
|
||||
void loadRedirectCache();
|
||||
void saveRedirectCache() const;
|
||||
void cleanStaleEntries();
|
||||
|
||||
private slots:
|
||||
void picDownloadFinished(QNetworkReply *reply);
|
||||
void picDownloadFailed();
|
||||
|
||||
void picDownloadChanged();
|
||||
void picsPathChanged();
|
||||
public slots:
|
||||
void processLoadQueue();
|
||||
|
||||
signals:
|
||||
void startLoadQueue();
|
||||
void imageLoaded(CardInfoPtr card, const QImage &image);
|
||||
};
|
||||
|
||||
class PictureLoader : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
static PictureLoader &getInstance()
|
||||
{
|
||||
static PictureLoader instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
private:
|
||||
explicit PictureLoader();
|
||||
~PictureLoader() override;
|
||||
// Singleton - Don't implement copy constructor and assign operator
|
||||
PictureLoader(PictureLoader const &);
|
||||
void operator=(PictureLoader const &);
|
||||
|
||||
PictureLoaderWorker *worker;
|
||||
|
||||
public:
|
||||
static void getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size);
|
||||
static void getCardBackPixmap(QPixmap &pixmap, QSize size);
|
||||
static void getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSize size);
|
||||
static void getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize size);
|
||||
static void clearPixmapCache(CardInfoPtr card);
|
||||
static void clearPixmapCache();
|
||||
static void cacheCardPixmaps(QList<CardInfoPtr> cards);
|
||||
|
||||
public slots:
|
||||
static void clearNetworkCache();
|
||||
|
||||
private slots:
|
||||
void picDownloadChanged();
|
||||
void picsPathChanged();
|
||||
|
||||
public slots:
|
||||
void imageLoaded(CardInfoPtr card, const QImage &image);
|
||||
};
|
||||
#endif
|
||||
156
cockatrice/src/client/ui/picture_loader/picture_loader.cpp
Normal file
@@ -0,0 +1,156 @@
|
||||
#include "picture_loader.h"
|
||||
|
||||
#include "../../../settings/cache_settings.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QBuffer>
|
||||
#include <QDebug>
|
||||
#include <QDirIterator>
|
||||
#include <QFileInfo>
|
||||
#include <QMovie>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QNetworkRequest>
|
||||
#include <QPainter>
|
||||
#include <QPixmapCache>
|
||||
#include <QScreen>
|
||||
#include <QThread>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
// never cache more than 300 cards at once for a single deck
|
||||
#define CACHED_CARD_PER_DECK_MAX 300
|
||||
|
||||
PictureLoader::PictureLoader() : QObject(nullptr)
|
||||
{
|
||||
worker = new PictureLoaderWorker;
|
||||
connect(&SettingsCache::instance(), SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged()));
|
||||
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
|
||||
|
||||
connect(worker, SIGNAL(imageLoaded(CardInfoPtr, const QImage &)), this,
|
||||
SLOT(imageLoaded(CardInfoPtr, const QImage &)));
|
||||
}
|
||||
|
||||
PictureLoader::~PictureLoader()
|
||||
{
|
||||
worker->deleteLater();
|
||||
}
|
||||
|
||||
void PictureLoader::getCardBackPixmap(QPixmap &pixmap, QSize size)
|
||||
{
|
||||
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
|
||||
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
||||
qCDebug(PictureLoaderLog) << "PictureLoader: cache fail for" << backCacheKey;
|
||||
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
QPixmapCache::insert(backCacheKey, pixmap);
|
||||
}
|
||||
}
|
||||
|
||||
void PictureLoader::getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSize size)
|
||||
{
|
||||
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
|
||||
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
||||
qCDebug(PictureLoaderCardBackCacheFailLog) << "PictureLoader: cache fail for" << backCacheKey;
|
||||
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
QPixmapCache::insert(backCacheKey, pixmap);
|
||||
}
|
||||
}
|
||||
|
||||
void PictureLoader::getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize size)
|
||||
{
|
||||
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
|
||||
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
|
||||
qCDebug(PictureLoaderCardBackCacheFailLog) << "PictureLoader: cache fail for" << backCacheKey;
|
||||
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
QPixmapCache::insert(backCacheKey, pixmap);
|
||||
}
|
||||
}
|
||||
|
||||
void PictureLoader::getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size)
|
||||
{
|
||||
if (card == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// search for an exact size copy of the picture in cache
|
||||
QString key = card->getPixmapCacheKey();
|
||||
QString sizeKey = key + QLatin1Char('_') + QString::number(size.width()) + QString::number(size.height());
|
||||
if (QPixmapCache::find(sizeKey, &pixmap))
|
||||
return;
|
||||
|
||||
// load the image and create a copy of the correct size
|
||||
QPixmap bigPixmap;
|
||||
if (QPixmapCache::find(key, &bigPixmap)) {
|
||||
QScreen *screen = qApp->primaryScreen();
|
||||
qreal dpr = screen->devicePixelRatio();
|
||||
pixmap = bigPixmap.scaled(size * dpr, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
pixmap.setDevicePixelRatio(dpr);
|
||||
QPixmapCache::insert(sizeKey, pixmap);
|
||||
return;
|
||||
}
|
||||
|
||||
// add the card to the load queue
|
||||
qCDebug(PictureLoaderLog) << "Enqueuing " << card->getName() << " for " << card->getPixmapCacheKey();
|
||||
getInstance().worker->enqueueImageLoad(card);
|
||||
}
|
||||
|
||||
void PictureLoader::imageLoaded(CardInfoPtr card, const QImage &image)
|
||||
{
|
||||
if (image.isNull()) {
|
||||
QPixmapCache::insert(card->getPixmapCacheKey(), QPixmap());
|
||||
} else {
|
||||
if (card->getUpsideDownArt()) {
|
||||
QImage mirrorImage = image.mirrored(true, true);
|
||||
QPixmapCache::insert(card->getPixmapCacheKey(), QPixmap::fromImage(mirrorImage));
|
||||
} else {
|
||||
QPixmapCache::insert(card->getPixmapCacheKey(), QPixmap::fromImage(image));
|
||||
}
|
||||
}
|
||||
|
||||
card->emitPixmapUpdated();
|
||||
}
|
||||
|
||||
void PictureLoader::clearPixmapCache(CardInfoPtr card)
|
||||
{
|
||||
if (card) {
|
||||
QPixmapCache::remove(card->getPixmapCacheKey());
|
||||
}
|
||||
}
|
||||
|
||||
void PictureLoader::clearPixmapCache()
|
||||
{
|
||||
QPixmapCache::clear();
|
||||
}
|
||||
|
||||
void PictureLoader::clearNetworkCache()
|
||||
{
|
||||
getInstance().worker->clearNetworkCache();
|
||||
}
|
||||
|
||||
void PictureLoader::cacheCardPixmaps(QList<CardInfoPtr> cards)
|
||||
{
|
||||
QPixmap tmp;
|
||||
int max = qMin(cards.size(), CACHED_CARD_PER_DECK_MAX);
|
||||
for (int i = 0; i < max; ++i) {
|
||||
const CardInfoPtr &card = cards.at(i);
|
||||
if (!card) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString key = card->getPixmapCacheKey();
|
||||
if (QPixmapCache::find(key, &tmp)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
getInstance().worker->enqueueImageLoad(card);
|
||||
}
|
||||
}
|
||||
|
||||
void PictureLoader::picDownloadChanged()
|
||||
{
|
||||
QPixmapCache::clear();
|
||||
}
|
||||
|
||||
void PictureLoader::picsPathChanged()
|
||||
{
|
||||
QPixmapCache::clear();
|
||||
}
|
||||
50
cockatrice/src/client/ui/picture_loader/picture_loader.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#ifndef PICTURELOADER_H
|
||||
#define PICTURELOADER_H
|
||||
|
||||
#include "../../../game/cards/card_database.h"
|
||||
#include "picture_loader_worker.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(PictureLoaderLog, "picture_loader");
|
||||
inline Q_LOGGING_CATEGORY(PictureLoaderCardBackCacheFailLog, "picture_loader.card_back_cache_fail");
|
||||
|
||||
class PictureLoader : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
static PictureLoader &getInstance()
|
||||
{
|
||||
static PictureLoader instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
private:
|
||||
explicit PictureLoader();
|
||||
~PictureLoader() override;
|
||||
// Singleton - Don't implement copy constructor and assign operator
|
||||
PictureLoader(PictureLoader const &);
|
||||
void operator=(PictureLoader const &);
|
||||
|
||||
PictureLoaderWorker *worker;
|
||||
|
||||
public:
|
||||
static void getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size);
|
||||
static void getCardBackPixmap(QPixmap &pixmap, QSize size);
|
||||
static void getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSize size);
|
||||
static void getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize size);
|
||||
static void clearPixmapCache(CardInfoPtr card);
|
||||
static void clearPixmapCache();
|
||||
static void cacheCardPixmaps(QList<CardInfoPtr> cards);
|
||||
|
||||
public slots:
|
||||
static void clearNetworkCache();
|
||||
|
||||
private slots:
|
||||
void picDownloadChanged();
|
||||
void picsPathChanged();
|
||||
|
||||
public slots:
|
||||
void imageLoaded(CardInfoPtr card, const QImage &image);
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,457 @@
|
||||
#include "picture_loader_worker.h"
|
||||
|
||||
#include "../../../game/cards/card_database_manager.h"
|
||||
#include "../../../settings/cache_settings.h"
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QDirIterator>
|
||||
#include <QMovie>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QNetworkReply>
|
||||
#include <QThread>
|
||||
|
||||
// Card back returned by gatherer when card is not found
|
||||
QStringList PictureLoaderWorker::md5Blacklist = QStringList() << "db0c48db407a907c16ade38de048a441";
|
||||
|
||||
PictureLoaderWorker::PictureLoaderWorker()
|
||||
: QObject(nullptr), picsPath(SettingsCache::instance().getPicsPath()),
|
||||
customPicsPath(SettingsCache::instance().getCustomPicsPath()),
|
||||
picDownload(SettingsCache::instance().getPicDownload()), downloadRunning(false), loadQueueRunning(false)
|
||||
{
|
||||
connect(this, SIGNAL(startLoadQueue()), this, SLOT(processLoadQueue()), Qt::QueuedConnection);
|
||||
connect(&SettingsCache::instance(), SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged()));
|
||||
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
|
||||
|
||||
networkManager = new QNetworkAccessManager(this);
|
||||
// We need a timeout to ensure requests don't hang indefinitely in case of
|
||||
// cache corruption, see related Qt bug: https://bugreports.qt.io/browse/QTBUG-111397
|
||||
// Use Qt's default timeout (30s, as of 2023-02-22)
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
|
||||
networkManager->setTransferTimeout();
|
||||
#endif
|
||||
auto cache = new QNetworkDiskCache(this);
|
||||
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
|
||||
cache->setMaximumCacheSize(1024L * 1024L *
|
||||
static_cast<qint64>(SettingsCache::instance().getNetworkCacheSizeInMB()));
|
||||
// Note: the settings is in MB, but QNetworkDiskCache uses bytes
|
||||
connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, cache,
|
||||
[cache](int newSizeInMB) { cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB)); });
|
||||
networkManager->setCache(cache);
|
||||
// Use a ManualRedirectPolicy since we keep track of redirects in picDownloadFinished
|
||||
// We can't use NoLessSafeRedirectPolicy because it is not applied with AlwaysCache
|
||||
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
|
||||
connect(networkManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(picDownloadFinished(QNetworkReply *)));
|
||||
|
||||
cacheFilePath = SettingsCache::instance().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
|
||||
loadRedirectCache();
|
||||
cleanStaleEntries();
|
||||
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this,
|
||||
&PictureLoaderWorker::saveRedirectCache);
|
||||
|
||||
pictureLoaderThread = new QThread;
|
||||
pictureLoaderThread->start(QThread::LowPriority);
|
||||
moveToThread(pictureLoaderThread);
|
||||
}
|
||||
|
||||
PictureLoaderWorker::~PictureLoaderWorker()
|
||||
{
|
||||
pictureLoaderThread->deleteLater();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::processLoadQueue()
|
||||
{
|
||||
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";
|
||||
|
||||
if (CardDatabaseManager::getInstance()->isProviderIdForPreferredPrinting(
|
||||
cardName, cardBeingLoaded.getCard()->getPixmapCacheKey())) {
|
||||
if (cardImageExistsOnDisk(setName, correctedCardName)) {
|
||||
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)
|
||||
{
|
||||
QImage image;
|
||||
QImageReader imgReader;
|
||||
imgReader.setDecideFormatFromContent(true);
|
||||
QList<QString> picsPaths = QList<QString>();
|
||||
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 {
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Picture NOT found, "
|
||||
<< (picDownload ? "download failed" : "downloads disabled")
|
||||
<< ", no more url combinations to try: BAILING OUT";
|
||||
imageLoaded(cardBeingDownloaded.getCard(), QImage());
|
||||
cardBeingDownloaded.clear();
|
||||
}
|
||||
emit startLoadQueue();
|
||||
}
|
||||
|
||||
bool PictureLoaderWorker::imageIsBlackListed(const QByteArray &picData)
|
||||
{
|
||||
QString md5sum = QCryptographicHash::hash(picData, QCryptographicHash::Md5).toHex();
|
||||
return md5Blacklist.contains(md5sum);
|
||||
}
|
||||
|
||||
QNetworkReply *PictureLoaderWorker::makeRequest(const QUrl &url)
|
||||
{
|
||||
// Check if the redirect is cached
|
||||
QUrl cachedRedirect = getCachedRedirect(url);
|
||||
if (!cachedRedirect.isEmpty()) {
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Using cached redirect for " << url.toDisplayString()
|
||||
<< " to " << cachedRedirect.toDisplayString();
|
||||
return makeRequest(cachedRedirect); // Use the cached redirect
|
||||
}
|
||||
|
||||
QNetworkRequest req(url);
|
||||
|
||||
if (!picDownload) {
|
||||
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache);
|
||||
}
|
||||
|
||||
QNetworkReply *reply = networkManager->get(req);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, url]() {
|
||||
QVariant redirectTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
|
||||
|
||||
if (redirectTarget.isValid()) {
|
||||
QUrl redirectUrl = redirectTarget.toUrl();
|
||||
if (redirectUrl.isRelative()) {
|
||||
redirectUrl = url.resolved(redirectUrl);
|
||||
}
|
||||
|
||||
cacheRedirect(url, redirectUrl);
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
|
||||
<< " set: " << cardBeingDownloaded.getSetName() << "]: Caching redirect from " << url.toDisplayString()
|
||||
<< " to " << redirectUrl.toDisplayString();
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
});
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl)
|
||||
{
|
||||
redirectCache[originalUrl] = qMakePair(redirectUrl, QDateTime::currentDateTimeUtc());
|
||||
saveRedirectCache();
|
||||
}
|
||||
|
||||
QUrl PictureLoaderWorker::getCachedRedirect(const QUrl &originalUrl) const
|
||||
{
|
||||
if (redirectCache.contains(originalUrl)) {
|
||||
return redirectCache[originalUrl].first;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::loadRedirectCache()
|
||||
{
|
||||
QSettings settings(cacheFilePath, QSettings::IniFormat);
|
||||
|
||||
redirectCache.clear();
|
||||
int size = settings.beginReadArray(REDIRECT_HEADER_NAME);
|
||||
for (int i = 0; i < size; ++i) {
|
||||
settings.setArrayIndex(i);
|
||||
QUrl originalUrl = settings.value(REDIRECT_ORIGINAL_URL).toUrl();
|
||||
QUrl redirectUrl = settings.value(REDIRECT_URL).toUrl();
|
||||
QDateTime timestamp = settings.value(REDIRECT_TIMESTAMP).toDateTime();
|
||||
|
||||
if (originalUrl.isValid() && redirectUrl.isValid()) {
|
||||
redirectCache[originalUrl] = qMakePair(redirectUrl, timestamp);
|
||||
}
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::saveRedirectCache() const
|
||||
{
|
||||
QSettings settings(cacheFilePath, QSettings::IniFormat);
|
||||
|
||||
settings.beginWriteArray(REDIRECT_HEADER_NAME, static_cast<int>(redirectCache.size()));
|
||||
int index = 0;
|
||||
for (auto it = redirectCache.cbegin(); it != redirectCache.cend(); ++it) {
|
||||
settings.setArrayIndex(index++);
|
||||
settings.setValue(REDIRECT_ORIGINAL_URL, it.key());
|
||||
settings.setValue(REDIRECT_URL, it.value().first);
|
||||
settings.setValue(REDIRECT_TIMESTAMP, it.value().second);
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::cleanStaleEntries()
|
||||
{
|
||||
QDateTime now = QDateTime::currentDateTimeUtc();
|
||||
|
||||
auto it = redirectCache.begin();
|
||||
while (it != redirectCache.end()) {
|
||||
if (it.value().second.addDays(SettingsCache::instance().getRedirectCacheTtl()) < now) {
|
||||
it = redirectCache.erase(it); // Remove stale entry
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
qCDebug(PictureLoaderWorkerLog).nospace()
|
||||
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
|
||||
<< "]: Image successfully " << (isFromCache ? "loaded from cached" : "downloaded from") << " url "
|
||||
<< reply->url().toDisplayString();
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
startNextPicDownload();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::enqueueImageLoad(CardInfoPtr card)
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
|
||||
// avoid queueing the same card more than once
|
||||
if (!card || card == cardBeingLoaded.getCard() || card == cardBeingDownloaded.getCard()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const PictureToLoad &pic : loadQueue) {
|
||||
if (pic.getCard() == card)
|
||||
return;
|
||||
}
|
||||
|
||||
for (const PictureToLoad &pic : cardsToDownload) {
|
||||
if (pic.getCard() == card)
|
||||
return;
|
||||
}
|
||||
|
||||
loadQueue.append(PictureToLoad(card));
|
||||
emit startLoadQueue();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::picDownloadChanged()
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
picDownload = SettingsCache::instance().getPicDownload();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::picsPathChanged()
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
picsPath = SettingsCache::instance().getPicsPath();
|
||||
customPicsPath = SettingsCache::instance().getCustomPicsPath();
|
||||
}
|
||||
|
||||
void PictureLoaderWorker::clearNetworkCache()
|
||||
{
|
||||
networkManager->cache()->clear();
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#ifndef PICTURE_LOADER_WORKER_H
|
||||
#define PICTURE_LOADER_WORKER_H
|
||||
|
||||
#include "../../../game/cards/card_database.h"
|
||||
#include "picture_to_load.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QMutex>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
|
||||
#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(PictureLoaderWorkerLog, "picture_loader.worker");
|
||||
|
||||
class PictureLoaderWorker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PictureLoaderWorker();
|
||||
~PictureLoaderWorker() override;
|
||||
|
||||
void enqueueImageLoad(CardInfoPtr card);
|
||||
void clearNetworkCache();
|
||||
|
||||
private:
|
||||
static QStringList md5Blacklist;
|
||||
|
||||
QThread *pictureLoaderThread;
|
||||
QString picsPath, customPicsPath;
|
||||
QList<PictureToLoad> loadQueue;
|
||||
QMutex mutex;
|
||||
QNetworkAccessManager *networkManager;
|
||||
QHash<QUrl, QPair<QUrl, QDateTime>> redirectCache; // Stores redirect and timestamp
|
||||
QString cacheFilePath; // Path to persistent storage
|
||||
static constexpr int CacheTTLInDays = 30; // TODO: Make user configurable
|
||||
QList<PictureToLoad> cardsToDownload;
|
||||
PictureToLoad cardBeingLoaded;
|
||||
PictureToLoad cardBeingDownloaded;
|
||||
bool picDownload, downloadRunning, loadQueueRunning;
|
||||
void startNextPicDownload();
|
||||
bool cardImageExistsOnDisk(QString &setName, QString &correctedCardName);
|
||||
bool imageIsBlackListed(const QByteArray &);
|
||||
QNetworkReply *makeRequest(const QUrl &url);
|
||||
void cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl);
|
||||
QUrl getCachedRedirect(const QUrl &originalUrl) const;
|
||||
void loadRedirectCache();
|
||||
void saveRedirectCache() const;
|
||||
void cleanStaleEntries();
|
||||
|
||||
private slots:
|
||||
void picDownloadFinished(QNetworkReply *reply);
|
||||
void picDownloadFailed();
|
||||
|
||||
void picDownloadChanged();
|
||||
void picsPathChanged();
|
||||
public slots:
|
||||
void processLoadQueue();
|
||||
|
||||
signals:
|
||||
void startLoadQueue();
|
||||
void imageLoaded(CardInfoPtr card, const QImage &image);
|
||||
};
|
||||
|
||||
#endif // PICTURE_LOADER_WORKER_H
|
||||
241
cockatrice/src/client/ui/picture_loader/picture_to_load.cpp
Normal file
@@ -0,0 +1,241 @@
|
||||
#include "picture_to_load.h"
|
||||
|
||||
#include "../../../settings/cache_settings.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDate>
|
||||
#include <QRegularExpression>
|
||||
#include <QUrl>
|
||||
#include <algorithm>
|
||||
|
||||
PictureToLoad::PictureToLoad(CardInfoPtr _card)
|
||||
: card(std::move(_card)), urlTemplates(SettingsCache::instance().downloads().getAllURLs())
|
||||
{
|
||||
if (card) {
|
||||
for (const auto &cardInfoPerSetList : card->getSets()) {
|
||||
for (const auto &set : cardInfoPerSetList) {
|
||||
sortedSets << set.getPtr();
|
||||
}
|
||||
}
|
||||
if (sortedSets.empty()) {
|
||||
sortedSets << CardSet::newInstance("", "", "", QDate());
|
||||
}
|
||||
std::sort(sortedSets.begin(), sortedSets.end(), SetDownloadPriorityComparator());
|
||||
|
||||
// If the user hasn't disabled arts other than their personal preference...
|
||||
if (!SettingsCache::instance().getOverrideAllCardArtWithPersonalPreference()) {
|
||||
// If the pixmapCacheKey corresponds to a specific set, we have to try to load it first.
|
||||
for (const auto &cardInfoPerSetList : card->getSets()) {
|
||||
for (const auto &set : cardInfoPerSetList) {
|
||||
if (QLatin1String("card_") + card->getName() + QString("_") + QString(set.getProperty("uuid")) ==
|
||||
card->getPixmapCacheKey()) {
|
||||
long long setIndex = sortedSets.indexOf(set.getPtr());
|
||||
CardSetPtr setForCardProviderID = sortedSets.takeAt(setIndex);
|
||||
sortedSets.prepend(setForCardProviderID);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// The first time called, nextSet will also populate the Urls for the first set.
|
||||
nextSet();
|
||||
}
|
||||
}
|
||||
|
||||
void PictureToLoad::populateSetUrls()
|
||||
{
|
||||
/* currentSetUrls is a list, populated each time a new set is requested for a particular card
|
||||
and Urls are removed from it as a download is attempted from each one. Custom Urls for
|
||||
a set are given higher priority, so should be placed first in the list. */
|
||||
currentSetUrls.clear();
|
||||
|
||||
if (card && currentSet) {
|
||||
QString setCustomURL = card->getCustomPicURL(currentSet->getShortName());
|
||||
|
||||
if (!setCustomURL.isEmpty()) {
|
||||
currentSetUrls.append(setCustomURL);
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString &urlTemplate : urlTemplates) {
|
||||
QString transformedUrl = transformUrl(urlTemplate);
|
||||
|
||||
if (!transformedUrl.isEmpty()) {
|
||||
currentSetUrls.append(transformedUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/* Call nextUrl to make sure currentUrl is up-to-date
|
||||
but we don't need the result here. */
|
||||
(void)nextUrl();
|
||||
}
|
||||
|
||||
bool PictureToLoad::nextSet()
|
||||
{
|
||||
if (!sortedSets.isEmpty()) {
|
||||
currentSet = sortedSets.takeFirst();
|
||||
populateSetUrls();
|
||||
return true;
|
||||
}
|
||||
currentSet = {};
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PictureToLoad::nextUrl()
|
||||
{
|
||||
if (!currentSetUrls.isEmpty()) {
|
||||
currentUrl = currentSetUrls.takeFirst();
|
||||
return true;
|
||||
}
|
||||
currentUrl = QString();
|
||||
return false;
|
||||
}
|
||||
|
||||
QString PictureToLoad::getSetName() const
|
||||
{
|
||||
if (currentSet) {
|
||||
return currentSet->getCorrectedShortName();
|
||||
} else {
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
static int parse(const QString &urlTemplate,
|
||||
const QString &propType,
|
||||
const QString &cardName,
|
||||
const QString &setName,
|
||||
std::function<QString(const QString &)> getProperty,
|
||||
QMap<QString, QString> &transformMap)
|
||||
{
|
||||
static const QRegularExpression rxFillWith("^(.+)_fill_with_(.+)$");
|
||||
static const QRegularExpression rxSubStr("^(.+)_substr_(\\d+)_(\\d+)$");
|
||||
|
||||
const QRegularExpression rxCardProp("!" + propType + ":([^!]+)!");
|
||||
|
||||
auto matches = rxCardProp.globalMatch(urlTemplate);
|
||||
while (matches.hasNext()) {
|
||||
auto match = matches.next();
|
||||
QString templatePropertyName = match.captured(1);
|
||||
auto fillMatch = rxFillWith.match(templatePropertyName);
|
||||
QString cardPropertyName;
|
||||
QString fillWith;
|
||||
int subStrPos = 0;
|
||||
int subStrLen = -1;
|
||||
if (fillMatch.hasMatch()) {
|
||||
cardPropertyName = fillMatch.captured(1);
|
||||
fillWith = fillMatch.captured(2);
|
||||
} else {
|
||||
fillWith = QString();
|
||||
auto subStrMatch = rxSubStr.match(templatePropertyName);
|
||||
if (subStrMatch.hasMatch()) {
|
||||
cardPropertyName = subStrMatch.captured(1);
|
||||
subStrPos = subStrMatch.captured(2).toInt();
|
||||
subStrLen = subStrMatch.captured(3).toInt();
|
||||
} else {
|
||||
cardPropertyName = templatePropertyName;
|
||||
}
|
||||
}
|
||||
QString propertyValue = getProperty(cardPropertyName);
|
||||
if (propertyValue.isEmpty()) {
|
||||
qCDebug(PictureToLoadLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested " << propType
|
||||
<< "property (" << cardPropertyName << ") for Url template (" << urlTemplate << ") is not available";
|
||||
return 1;
|
||||
} else {
|
||||
int propLength = propertyValue.length();
|
||||
if (subStrLen > 0) {
|
||||
if (subStrPos + subStrLen > propLength) {
|
||||
qCDebug(PictureToLoadLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested " << propType
|
||||
<< " property (" << cardPropertyName << ") for Url template (" << urlTemplate
|
||||
<< ") is smaller than substr specification (" << subStrPos << " + " << subStrLen << " > "
|
||||
<< propLength << ")";
|
||||
return 1;
|
||||
} else {
|
||||
propertyValue = propertyValue.mid(subStrPos, subStrLen);
|
||||
propLength = subStrLen;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fillWith.isEmpty()) {
|
||||
int fillLength = fillWith.length();
|
||||
if (fillLength < propLength) {
|
||||
qCDebug(PictureToLoadLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested " << propType
|
||||
<< " property (" << cardPropertyName << ") for Url template (" << urlTemplate
|
||||
<< ") is longer than fill specification (" << fillWith << ")";
|
||||
return 1;
|
||||
} else {
|
||||
|
||||
propertyValue = fillWith.left(fillLength - propLength) + propertyValue;
|
||||
}
|
||||
}
|
||||
|
||||
transformMap["!" + propType + ":" + templatePropertyName + "!"] = propertyValue;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
QString PictureToLoad::transformUrl(const QString &urlTemplate) const
|
||||
{
|
||||
/* This function takes Url templates and substitutes actual card details
|
||||
into the url. This is used for making Urls with follow a predictable format
|
||||
for downloading images. If information is requested by the template that is
|
||||
not populated for this specific card/set combination, an empty string is returned.*/
|
||||
|
||||
CardSetPtr set = getCurrentSet();
|
||||
|
||||
QMap<QString, QString> transformMap = QMap<QString, QString>();
|
||||
QString setName = getSetName();
|
||||
|
||||
// name
|
||||
QString cardName = card->getName();
|
||||
transformMap["!name!"] = cardName;
|
||||
transformMap["!name_lower!"] = card->getName().toLower();
|
||||
transformMap["!corrected_name!"] = card->getCorrectedName();
|
||||
transformMap["!corrected_name_lower!"] = card->getCorrectedName().toLower();
|
||||
|
||||
// card properties
|
||||
if (parse(
|
||||
urlTemplate, "prop", cardName, setName, [&](const QString &name) { return card->getProperty(name); },
|
||||
transformMap)) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
if (set) {
|
||||
transformMap["!setcode!"] = set->getShortName();
|
||||
transformMap["!setcode_lower!"] = set->getShortName().toLower();
|
||||
transformMap["!setname!"] = set->getLongName();
|
||||
transformMap["!setname_lower!"] = set->getLongName().toLower();
|
||||
|
||||
if (parse(
|
||||
urlTemplate, "set", cardName, setName,
|
||||
[&](const QString &name) { return card->getSetProperty(set->getShortName(), name); }, transformMap)) {
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
// language setting
|
||||
transformMap["!sflang!"] = QString(QCoreApplication::translate(
|
||||
"PictureLoader", "en", "code for scryfall's language property, not available for all languages"));
|
||||
|
||||
QString transformedUrl = urlTemplate;
|
||||
for (const QString &prop : transformMap.keys()) {
|
||||
if (transformedUrl.contains(prop)) {
|
||||
if (!transformMap[prop].isEmpty()) {
|
||||
transformedUrl.replace(prop, QUrl::toPercentEncoding(transformMap[prop]));
|
||||
} else {
|
||||
/* This means the template is requesting information that is not
|
||||
* populated in this card, so it should return an empty string,
|
||||
* indicating an invalid Url.
|
||||
*/
|
||||
qCDebug(PictureToLoadLog).nospace()
|
||||
<< "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested information ("
|
||||
<< prop << ") for Url template (" << urlTemplate << ") is not available";
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return transformedUrl;
|
||||
}
|
||||