Compare commits

..

4 commits

Author SHA1 Message Date
Lukas Brübach
310caa7dc0 [PictureLoader] Hand backed-off requests back to their worker instead of parking them 2026-09-18 04:21:19 +02:00
Lukas Brübach
5956dcab83 [PictureLoader] Seed per-host allowances on demand and skip hosts in 429 backoff
The quota reset re-filled every host's remaining allowance to a full
MAX_REQUESTS_PER_SEC as soon as the queue had a request for it. A server
that was just rate limited could therefore be hammered again at full speed
immediately after (or even during) recovery.

Only seed a host's allowance the first time it is dispatched in the
current second, seeded from its reduced sustained quota, and skip hosts
still inside their 429 backoff window entirely. This makes the pacing
commit's burst-free behavior hold per host too, instead of just smoothing
the global aggregate.
2026-09-18 04:19:23 +02:00
Lukas Brübach
91519166f9 [PictureLoader] Guard dispatch timer restarts and drop dead request quota 2026-09-18 03:55:45 +02:00
Lukas Brübach
1c6ee62393 [PictureLoader] Pace requests and run the throttle timers on the worker thread
Previously the whole backed-up queue was drained in a burst as soon as a
request was enqueued, sending up to 10 requests back-to-back and then
immediately re-filling the quota one second later. That hard-bursts a
rate-limited API like Scryfall's (10 requests/second) into a 30 second
lockout.

Introduce a pacing timer that dispatches a single queue entry every
100 ms, so the per-second allowance is used smoothly instead of in spikes,
and keep the quota timer at 1 second. Also fix both timers' thread
affinity: they are QTimer value members and so are not QObject children,
meaning moveToThread() on the worker left them on the main thread while
the slot code started them from the picture thread, which was a no-op that
also warned. They are moved to the worker thread explicitly and started
lazily from there.
2026-09-13 03:36:31 +02:00
279 changed files with 1044 additions and 10451 deletions

View file

@ -18,12 +18,12 @@ RUN apt-get update && \
libssl-dev \
ninja-build \
protobuf-compiler \
qt6-declarative-dev \
qt6-image-formats-plugins \
qt6-l10n-tools \
qt6-multimedia-dev \
qt6-shadertools-dev \
qt6-declarative-dev \
qt6-svg-dev \
qt6-shadertools-dev \
qt6-tools-dev \
qt6-tools-dev-tools \
qt6-websockets-dev \

View file

@ -19,12 +19,12 @@ RUN apt-get update && \
libssl-dev \
ninja-build \
protobuf-compiler \
qt6-declarative-dev \
qt6-image-formats-plugins \
qt6-l10n-tools \
qt6-multimedia-dev \
qt6-shadertools-dev \
qt6-declarative-dev \
qt6-svg-dev \
qt6-shadertools-dev \
qt6-tools-dev \
qt6-tools-dev-tools \
qt6-websockets-dev \

View file

@ -9,7 +9,7 @@ RUN dnf install -y \
ninja-build \
openssl-devel \
protobuf-devel \
qt6-{qtdeclarative,qtmultimedia,qtshadertools,qtsvg,qttools,qtwebsockets}-devel \
qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
qt6-qtimageformats \
rpm-build \
xz-devel \

View file

@ -9,7 +9,7 @@ RUN dnf install -y \
ninja-build \
openssl-devel \
protobuf-devel \
qt6-{qtdeclarative,qtmultimedia,qtshadertools,qtsvg,qttools,qtwebsockets}-devel \
qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
qt6-qtimageformats \
rpm-build \
xz-devel \

View file

@ -18,12 +18,12 @@ RUN apt-get update && \
libssl-dev \
ninja-build \
protobuf-compiler \
qt6-declarative-dev \
qt6-image-formats-plugins \
qt6-l10n-tools \
qt6-multimedia-dev \
qt6-shadertools-dev \
qt6-declarative-dev \
qt6-svg-dev \
qt6-shadertools-dev \
qt6-tools-dev \
qt6-tools-dev-tools \
qt6-websockets-dev \

View file

@ -19,12 +19,12 @@ RUN apt-get update && \
libssl-dev \
ninja-build \
protobuf-compiler \
qt6-declarative-dev \
qt6-image-formats-plugins \
qt6-l10n-tools \
qt6-multimedia-dev \
qt6-shadertools-dev \
qt6-declarative-dev \
qt6-svg-dev \
qt6-shadertools-dev \
qt6-tools-dev \
qt6-tools-dev-tools \
qt6-websockets-dev \

View file

@ -327,32 +327,4 @@ if [[ $MAKE_PACKAGE ]]; then
BUILD_DIR="$BUILD_DIR" .ci/name_build.sh "$PACKAGE_SUFFIX"
echo "::endgroup::"
fi
if [[ $RUNNER_OS == Windows ]]; then
echo "::group::Check installer for build-tree artifacts"
cd "$BUILD_DIR"
package="$(find . -maxdepth 1 -type f -name 'Cockatrice-*.exe' -print -quit)"
if [[ ! $package ]]; then
echo "::error file=$0::Could not find installer to inspect"
exit 1
fi
seven_zip="$(command -v 7z || true)"
if [[ ! $seven_zip ]]; then
seven_zip="/c/Program Files/7-Zip/7z.exe"
fi
if [[ ! -f $seven_zip ]]; then
echo "::warning file=$0::7-Zip not found, skipping installer content check"
else
echo "Inspecting $package"
# Fail the build if the installer contains any path left behind by the MSBuild or
# Qt AUTOMOC tooling (build-tree artifacts must live in the build dir, not the install)
if "$seven_zip" l "$package" |
grep -E "_autogen|\.dir[\\/]|\.tlog|(^|[\\/])x64[\\/]|(^|[\\/])\.qt[\\/]|(^|[\\/])\.qsb[\\/]|(^|[\\/])\.lupdate[\\/]|CMakeFiles"; then
echo "::error file=$0::Installer contains build-tree artifacts"
exit 1
fi
echo "Installer content is clean"
fi
echo "::endgroup::"
fi
fi

View file

@ -272,8 +272,8 @@ jobs:
make_package: 1
override_target: 13
package_suffix: "-macOS13_Intel"
qt_version: 6.11.*
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
qt_version: 6.11.1
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
soc: Intel
type: Release
use_ccache: 1
@ -288,8 +288,8 @@ jobs:
make_package: 1
override_target: 14
package_suffix: "-macOS14"
qt_version: 6.11.*
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
qt_version: 6.11.1
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
soc: Apple
type: Release
use_ccache: 1
@ -304,8 +304,8 @@ jobs:
make_package: 1
override_target: 15
package_suffix: "-macOS15"
qt_version: 6.11.*
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
qt_version: 6.11.1
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
soc: Apple
type: Release
use_ccache: 1
@ -317,8 +317,8 @@ jobs:
ccache_eviction_age: 7d
cmake_generator: Ninja
qt_version: 6.11.*
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
qt_version: 6.11.1
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
soc: Apple
type: Debug
use_ccache: 1
@ -332,8 +332,8 @@ jobs:
cmake_generator_platform: x64
make_package: 1
package_suffix: "-Win10"
qt_version: 6.11.*
qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets
qt_version: 6.11.1
qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools
type: Release
name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }}
@ -368,20 +368,18 @@ jobs:
key: ccache-${{ matrix.runner }}_${{ matrix.override_target }}-Xcode${{ matrix.xcode }}
path: ${{ env.CCACHE_DIR }}
- name: "[macOS] Install aqtinstall"
if: matrix.os == 'macOS'
- name: "Install aqtinstall"
run: pipx install aqtinstall
# Resolve given wildcard versions (e.g. Qt 6.6.*) to latest version via aqtinstall to avoid stale caches on new releases
- name: "[macOS] Resolve latest Qt from ${{ matrix.qt_version }} input"
if: matrix.os == 'macOS'
- name: "Resolve latest Qt patch version"
env:
QT_VERSION: ${{ matrix.qt_version }}
id: resolve_qt_version
shell: bash
run: .ci/resolve_latest_aqt_qt_version.sh "$QT_VERSION"
- name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }}"
- name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries"
if: matrix.os == 'macOS'
id: restore_qt
uses: actions/cache/restore@v6
@ -391,22 +389,21 @@ jobs:
# Using jurplel/install-qt-action to install Qt without using brew
# Qt build using vcpkg either just fails or takes too long to build
- name: "[macOS] Install fat Qt ${{ matrix.qt_version }}"
- name: "[macOS] Install fat Qt ${{ steps.resolve_qt_version.outputs.version }}"
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
uses: jurplel/install-qt-action@v4
with:
cache: false
# cache-key-prefix: Qt
dir: ${{ github.workspace }} # thinning script depends on this location
dir: ${{ github.workspace }}
modules: ${{ matrix.qt_modules }}
version: ${{ matrix.qt_version }}
version: ${{ steps.resolve_qt_version.outputs.version }}
- name: "[macOS] Create thin Qt libraries"
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
run: .ci/thin_macos_qtlib.sh
- name: "[macOS] Cache thin Qt libraries"
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' && github.ref == 'refs/heads/master'
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
with:
key: ${{ steps.restore_qt.outputs.cache-primary-key }}
@ -418,10 +415,10 @@ jobs:
with:
# Qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released
aqtsource: git+https://github.com/miurahr/aqtinstall.git
cache: ${{ github.ref == 'refs/heads/master' }}
cache: true
cache-key-prefix: Qt
modules: ${{ matrix.qt_modules }}
version: ${{ matrix.qt_version }}
version: ${{ steps.resolve_qt_version.outputs.version }}
- name: "[Windows] Install NSIS"
if: matrix.os == 'Windows'
@ -453,7 +450,7 @@ jobs:
PACKAGE_SUFFIX: '${{ matrix.package_suffix }}'
TARGET_MACOS_VERSION: ${{ matrix.override_target }}
USE_CCACHE: ${{ matrix.use_ccache }}
VCPKG_BINARY_SOURCES: "clear;files,${{ steps.vcpkg-cache.outputs.path }},${{ case(github.ref == 'refs/heads/master', 'readwrite', 'read') }}"
VCPKG_BINARY_SOURCES: 'clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite'
VCPKG_DISABLE_METRICS: 1
VCPKG_FEATURE_FLAGS: dependencygraph
run: .ci/compile.sh --server --test --vcpkg

View file

@ -76,7 +76,7 @@ jobs:
uses: docker/build-push-action@v7
with:
cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
cache-to: ${{ case(github.ref == 'refs/heads/master', format('type=gha,mode=max,scope={0}', env.CACHE_SCOPE), '') }}
cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }}
context: .
platforms: ${{ matrix.platform }}
push: false

View file

@ -293,7 +293,7 @@ if(UNIX)
if(CPACK_GENERATOR STREQUAL "RPM")
set(CPACK_RPM_PACKAGE_LICENSE "GPLv2")
set(CPACK_RPM_MAIN_COMPONENT "cockatrice")
set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qtimageformats, qt6-qtmultimedia, qt6-qtsvg, qt6-qttools")
set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats")
set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games")
set(CPACK_RPM_PACKAGE_URL "http://github.com/Cockatrice/Cockatrice")
# stop directories from making package conflicts
@ -311,7 +311,7 @@ if(UNIX)
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
set(CPACK_DEBIAN_PACKAGE_SECTION "games")
set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "http://github.com/Cockatrice/Cockatrice")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-image-formats-plugins, qt6-qpa-plugins")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-qpa-plugins, qt6-image-formats-plugins")
set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db
endif()
endif()

View file

@ -387,14 +387,19 @@ SectionEnd
Section "un.Application" UnSecApplication
SetShellVarContext all
RMDir /r "$INSTDIR\plugins"
RMDir /r "$INSTDIR\sounds"
RMDir /r "$INSTDIR\themes"
RMDir /r "$INSTDIR\translations"
Delete "$INSTDIR\*.exe"
Delete "$INSTDIR\*.dll"
Delete "$INSTDIR\qt.conf"
Delete "$INSTDIR\qdebug.txt"
Delete "$INSTDIR\servatrice.sql"
Delete "$INSTDIR\servatrice.ini.example"
RMDir "$INSTDIR"
; Remove the entire application directory so any file that is not part of
; the installed payload (e.g. build-tree artifacts such as *.dir folders,
; *_autogen and *.tlog files from a build) cannot survive between an
; uninstall and a fresh reinstall.
RMDir /r "$INSTDIR"
RMDir /r "$SMPROGRAMS\Cockatrice"
RMDir "$SMPROGRAMS\Cockatrice"
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice"
SectionEnd

View file

@ -45,14 +45,11 @@ set(cockatrice_SOURCES
src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp
src/interface/widgets/dialogs/dlg_load_remote_deck.cpp
src/interface/widgets/dialogs/dlg_local_game_options.cpp
src/interface/widgets/dialogs/dlg_login_prompt.cpp
src/interface/widgets/dialogs/dlg_manage_sets.cpp
src/interface/widgets/dialogs/dlg_my_reports.cpp
src/interface/widgets/dialogs/dlg_register.cpp
src/interface/widgets/dialogs/dlg_report_user.cpp
src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp
src/interface/widgets/dialogs/dlg_share_deck.cpp
src/interface/widgets/dialogs/dlg_shared_decks_preview.cpp
src/interface/widgets/dialogs/dlg_settings.cpp
src/interface/widgets/dialogs/dlg_startup_card_check.cpp
src/interface/widgets/dialogs/dlg_tip_of_the_day.cpp
@ -60,9 +57,6 @@ set(cockatrice_SOURCES
src/interface/widgets/dialogs/dlg_view_log.cpp
src/interface/widgets/dialogs/override_printing_warning.cpp
src/interface/widgets/dialogs/tip_of_the_day.cpp
src/interface/widgets/deck_share/deck_share_utils.cpp
src/interface/widgets/deck_share/shared_deck_preview_widget.cpp
src/interface/widgets/deck_share/share_bar_widget.cpp
src/filters/deck_filter_string.cpp
src/filters/filter_builder.cpp
src/filters/filter_tree_model.cpp
@ -169,7 +163,6 @@ set(cockatrice_SOURCES
src/interface/palette_editor/palette_grid_widget.cpp
src/interface/palette_editor/palette_editor_dialog.cpp
src/interface/widgets/cards/additional_info/color_identity_widget.cpp
src/interface/widgets/cards/additional_info/deck_color_identity.cpp
src/interface/widgets/cards/additional_info/mana_cost_widget.cpp
src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp
src/interface/widgets/cards/art_crop_attribution.cpp
@ -324,8 +317,6 @@ set(cockatrice_SOURCES
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp
src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.cpp
src/interface/widgets/visual_deck_storage/remote_public_decks_model.cpp
src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp
src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp
src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp
@ -397,7 +388,6 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/tab_logs.cpp
src/interface/widgets/tabs/tab_message.cpp
src/interface/widgets/tabs/tab_moderation.cpp
src/interface/widgets/tabs/tab_public_decks.cpp
src/interface/widgets/tabs/tab_report.cpp
src/interface/widgets/tabs/tab_replays.cpp
src/interface/widgets/tabs/tab_room.cpp
@ -413,7 +403,6 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp
src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp
src/interface/widgets/onboarding/banner_shader_config.h
src/interface/widgets/onboarding/brand_colors.h
src/interface/widgets/onboarding/first_run_wizard.cpp
src/interface/widgets/onboarding/first_run_wizard.h
src/interface/widgets/onboarding/first_run_wizard_page.cpp
@ -454,8 +443,6 @@ set(cockatrice_SOURCES
src/interface/intents/intent_login.h
src/interface/intents/intent_open_server_room_by_name.cpp
src/interface/intents/intent_open_server_room_by_name.h
src/interface/intents/intent_open_shared_deck.cpp
src/interface/intents/intent_open_shared_deck.h
src/interface/intents/url_parser.cpp
src/interface/intents/url_parser.h
src/interface/widgets/server/user/user_info_popup.cpp
@ -543,7 +530,6 @@ qt6_add_shaders(
"src/interface/widgets/onboarding/shaders"
FILES
src/interface/widgets/onboarding/shaders/brand_banner.frag
src/interface/widgets/onboarding/shaders/brand_plate.frag
)
qt6_add_resources(
@ -668,35 +654,18 @@ if(WIN32)
set(qtconf_dest_dir .)
install(
DIRECTORY "$<TARGET_FILE_DIR:cockatrice>/"
DIRECTORY "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/${CMAKE_BUILD_TYPE}/"
DESTINATION ./
FILES_MATCHING
PATTERN "*.dll"
PATTERN "*.pdb" EXCLUDE
PATTERN "*.dir*" EXCLUDE
PATTERN "*_autogen*" EXCLUDE
PATTERN "*.tlog*" EXCLUDE
PATTERN "CMakeFiles*" EXCLUDE
PATTERN "x64*" EXCLUDE
PATTERN ".qt*" EXCLUDE
PATTERN ".qsb*" EXCLUDE
PATTERN ".lupdate*" EXCLUDE
)
install(
DIRECTORY "${CMAKE_BINARY_DIR}/cockatrice/"
DESTINATION ./
FILES_MATCHING
PATTERN "CMakeFiles" EXCLUDE
PATTERN "*.ini"
PATTERN "CMakeFiles*" EXCLUDE
PATTERN "*.dir*" EXCLUDE
PATTERN "*_autogen*" EXCLUDE
PATTERN "*.tlog*" EXCLUDE
PATTERN "*.pdb" EXCLUDE
PATTERN "x64*" EXCLUDE
PATTERN ".qt*" EXCLUDE
PATTERN ".qsb*" EXCLUDE
PATTERN ".lupdate*" EXCLUDE
)
# Qt plugins: audio, iconengines, imageformats, multimedia, platforms, printsupport, styles, tls
@ -749,6 +718,10 @@ Data = Resources\")
"
COMPONENT Runtime
)
if(OPENSSL_FOUND)
install(FILES ${OPENSSL_INCLUDE_DIRS} DESTINATION ./)
endif()
endif()
if(Qt6LinguistTools_FOUND)

View file

@ -63,8 +63,6 @@
<file>resources/icons/mana/W.svg</file>
<file>resources/backgrounds/home.png</file>
<file>resources/backgrounds/home-dark.png</file>
<file>resources/backgrounds/home-light.png</file>
<file>resources/backgrounds/card_triplet.svg</file>
<file>resources/backgrounds/placeholder_printing_selector.svg</file>
@ -365,8 +363,6 @@
<file>resources/usericons/pawn_single.svg</file>
<file>resources/usericons/pawn_double.svg</file>
<file>resources/usericons/pawn_dev_single.svg</file>
<file>resources/usericons/pawn_dev_double.svg</file>
<file>resources/usericons/pawn_donator_single.svg</file>
<file>resources/usericons/pawn_donator_double.svg</file>
<file>resources/usericons/pawn_judge_single.svg</file>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 MiB

After

Width:  |  Height:  |  Size: 12 MiB

Before After
Before After

View file

@ -1,343 +0,0 @@
<?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.2 (ebf0e940, 2025-05-08)"
sodipodi:docname="pawn_dev_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: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
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"/>
</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="6.0735294"
inkscape:cx="53.757869"
inkscape:cy="53.840194"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1696"
inkscape:window-height="1051"
inkscape:window-x="98"
inkscape:window-y="1118"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showguides="true">
<sodipodi:guide
position="49.829627,61.114263"
orientation="1,0"
id="guide1"
inkscape:locked="false"/>
</sodipodi:namedview>
<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"
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
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="right"
inkscape:connector-curvature="0"/>
<path
d="m 478.409,116.617 c -0.368,-4.271 -3.181,-7.94 -7.2,-9.403 -4.029,-1.472 -8.539,-0.47 -11.57,2.556 l -62.015,62.011 -68.749,-21.768 -21.768,-68.748 62.016,-62.016 c 3.035,-3.032 4.025,-7.543 2.563,-11.565 -1.477,-4.03 -5.137,-6.837 -9.417,-7.207 -37.663,-3.245 -74.566,10.202 -101.247,36.887 -36.542,36.545 -46.219,89.911 -29.083,135.399 -1.873,1.578 -3.721,3.25 -5.544,5.053 L 19.386,373.152 c -0.073,0.071 -0.145,0.149 -0.224,0.219 -24.345,24.346 -24.345,63.959 0,88.309 24.349,24.344 63.672,24.048 88.013,-0.298 0.105,-0.098 0.201,-0.196 0.297,-0.305 L 301.104,252.456 c 1.765,-1.773 3.404,-3.628 4.949,-5.532 45.5,17.167 98.9,7.513 135.474,-29.056 26.675,-26.687 40.131,-63.593 36.882,-101.251 z M 75.98,435.38 c -8.971,8.969 -23.5,8.963 -32.47,0 -8.967,-8.961 -8.967,-23.502 0,-32.466 8.97,-8.963 23.499,-8.963 32.47,0 8.967,8.964 8.967,23.505 0,32.466 z"
id="path1"
style="display:inline;fill:#A1A1A1;stroke-width:9.87059588;stroke-dasharray:none;fill-opacity:1;stroke:#000000;stroke-opacity:1"
transform="matrix(0.19145387,0,0,0.19145387,4.0816072,956.99677)"
inkscape:label="wrench" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,211 +0,0 @@
<?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.2 (ebf0e940, 2025-05-08)"
sodipodi:docname="pawn_dev_single.svg"
xml:space="preserve"
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: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="3.1582091"
inkscape:cx="104.64791"
inkscape:cy="56.04442"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1027"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showguides="true"><sodipodi:guide
position="50.002551,111.99556"
orientation="1,0"
id="guide3"
inkscape:locked="false" /></sodipodi:namedview><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" /></cc:Work></rdf:RDF></metadata><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-952.36218)"
style="display:inline">
<path
style="display:inline;opacity:1;fill-opacity:1;stroke:#000000;stroke-width:2.7822;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 49.84375,1.71875 C 36.719738,1.71875 26.0625,12.375988 26.0625,25.5 c 0,7.477454 3.475825,14.112734 8.875,18.46875 -10.497549,5.974948 -17.018351,18.227376 -20.625,31.6875 -5.2744126,19.6844 15.911513,22.5625 35.53125,22.5625 19.619736,0 40.705577,-3.2516 35.53125,-22.5625 C 81.693381,61.916246 75.224585,49.827177 64.8125,43.9375 70.181573,39.580662 73.59375,32.953205 73.59375,25.5 c 0,-13.124012 -10.625988,-23.78125 -23.75,-23.78125 z"
id="left"
sodipodi:insensitive="true"
transform="translate(0,952.36218)" />
<path
d="m 478.409,116.617 c -0.368,-4.271 -3.181,-7.94 -7.2,-9.403 -4.029,-1.472 -8.539,-0.47 -11.57,2.556 l -62.015,62.011 -68.749,-21.768 -21.768,-68.748 62.016,-62.016 c 3.035,-3.032 4.025,-7.543 2.563,-11.565 -1.477,-4.03 -5.137,-6.837 -9.417,-7.207 -37.663,-3.245 -74.566,10.202 -101.247,36.887 -36.542,36.545 -46.219,89.911 -29.083,135.399 -1.873,1.578 -3.721,3.25 -5.544,5.053 L 19.386,373.152 c -0.073,0.071 -0.145,0.149 -0.224,0.219 -24.345,24.346 -24.345,63.959 0,88.309 24.349,24.344 63.672,24.048 88.013,-0.298 0.105,-0.098 0.201,-0.196 0.297,-0.305 L 301.104,252.456 c 1.765,-1.773 3.404,-3.628 4.949,-5.532 45.5,17.167 98.9,7.513 135.474,-29.056 26.675,-26.687 40.131,-63.593 36.882,-101.251 z M 75.98,435.38 c -8.971,8.969 -23.5,8.963 -32.47,0 -8.967,-8.961 -8.967,-23.502 0,-32.466 8.97,-8.963 23.499,-8.963 32.47,0 8.967,8.964 8.967,23.505 0,32.466 z"
id="path1"
style="display:inline;fill:#A1A1A1;stroke-width:9.87059588;stroke-dasharray:none;fill-opacity:1;stroke:#000000;stroke-opacity:1"
transform="matrix(0.19145387,0,0,0.19145387,4.0816072,956.99677)"
inkscape:label="wrench" /></g></svg>

Before

Width:  |  Height:  |  Size: 9.1 KiB

View file

@ -1,7 +1,6 @@
#include "abstract_card_item.h"
#include "../../client/settings/cache_settings.h"
#include "../../interface/card_localization.h"
#include "../../interface/card_picture_loader/card_picture_loader.h"
#include "../game_scene.h"
#include "../z_values.h"
@ -27,8 +26,6 @@ AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this,
[this] { update(); });
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
[this] { update(); });
refreshCardInfo();
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this,
@ -174,7 +171,7 @@ void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedS
if (SettingsCache::instance().debug().getShowCardId()) {
prefix = "#" + QString::number(id) + " ";
}
nameStr = prefix + CardLocalization::displayName(getCardInfo());
nameStr = prefix + cardRef.name;
}
painter->drawText(QRectF(3 * scaleFactor, 3 * scaleFactor, translatedSize.width() - 6 * scaleFactor,
translatedSize.height() - 6 * scaleFactor),

View file

@ -1,59 +0,0 @@
#ifndef COCKATRICE_CARD_LOCALIZATION_H
#define COCKATRICE_CARD_LOCALIZATION_H
#include "../client/settings/cache_settings.h"
#include <QString>
#include <libcockatrice/card/card_info.h>
#include <libcockatrice/settings/cards_display_settings.h>
namespace CardLocalization
{
/**
* @brief The language code selected for localized card text and images.
*/
inline QString displayLang()
{
return SettingsCache::instance().cardsDisplay().getCardLang();
}
/**
* @brief Card name in the configured display language, falling back to English.
* @param card The card to display.
* @return The localized name, or an empty string for a null card.
*/
inline QString displayName(const CardInfoPtr &card)
{
return card.isNull() ? QString() : card->getLocalizedName(displayLang());
}
/**
* @brief Card rules text in the configured display language, falling back to English.
* @param card The card to display.
* @return The localized text, or an empty string for a null card.
*/
inline QString displayText(const CardInfoPtr &card)
{
return card.isNull() ? QString() : card->getLocalizedText(displayLang());
}
/**
* @brief Card name in the configured display language, falling back to English.
* @param card The card to display.
*/
inline QString displayName(const CardInfo &card)
{
return card.getLocalizedName(displayLang());
}
/**
* @brief Card rules text in the configured display language, falling back to English.
* @param card The card to display.
*/
inline QString displayText(const CardInfo &card)
{
return card.getLocalizedText(displayLang());
}
} // namespace CardLocalization
#endif // COCKATRICE_CARD_LOCALIZATION_H

View file

@ -8,7 +8,7 @@
#include <QApplication>
#include <QBuffer>
#include <QDebug>
#include <QDir>
#include <QDirIterator>
#include <QFileInfo>
#include <QMainWindow>
#include <QMovie>
@ -20,7 +20,6 @@
#include <QThread>
#include <algorithm>
#include <libcockatrice/settings/cache_storage_settings.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/download_settings.h>
#include <libcockatrice/settings/paths_settings.h>
#include <utility>
@ -38,10 +37,8 @@ CardPictureLoader::CardPictureLoader() : QObject(nullptr)
&CardPictureLoader::picsPathChanged);
connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this,
&CardPictureLoader::picDownloadChanged);
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
&CardPictureLoader::cardLangChanged);
qRegisterMetaType<ExactCard>("ExactCard");
qRegisterMetaType<ExactCard>();
connect(worker, &CardPictureLoaderWorker::imageLoaded, this, &CardPictureLoader::imageLoaded);
statusBar = new CardPictureLoaderStatusBar(nullptr);
@ -209,49 +206,7 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
card.emitPixmapUpdated();
}
void CardPictureLoader::deleteAllLocalOverrides(const ExactCard &card)
{
const QString picsRoot = SettingsCache::instance().paths().getPicsPath();
if (picsRoot.isEmpty() || !card) {
return;
}
QDir baseDir(picsRoot);
if (!baseDir.cd("downloadedPics")) {
return;
}
const QString name = card.getInfo().getCorrectedName();
QString set, collector, uuid;
auto printing = card.getPrinting();
if (printing.getSet()) {
set = printing.getSet()->getCorrectedShortName();
collector = printing.getProperty("num");
uuid = printing.getUuid();
}
for (const auto &scheme : CardPictureLoaderLocalSchemes::exportSchemes()) {
QString rel = CardPictureLoaderLocalSchemes::expandPattern(scheme.pattern, name, set, collector, uuid);
if (rel.isEmpty()) {
continue;
}
rel += ".png";
rel = QDir::cleanPath(rel);
QString fullPath = baseDir.filePath(rel);
if (QFile::exists(fullPath)) {
QFile::remove(fullPath);
}
}
}
void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card,
const QPixmap &pixmap,
const bool allowOverwrite)
void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap)
{
if (pixmap.isNull() || !card) {
return;
@ -311,9 +266,8 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card,
QFileInfo outInfo(baseDir.filePath(relativePath));
// Automatic cache writes (FILESYSTEM_CACHE) must never clobber an explicit user override.
// Only the explicit override paths pass allowOverwrite == true.
if (!allowOverwrite && outInfo.exists()) {
// Do not overwrite existing files
if (outInfo.exists()) {
return;
}
@ -334,122 +288,6 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card,
}
}
void CardPictureLoader::installPrintingOverrideOnLoad(const ExactCard &originalCard, const ExactCard &overrideCard)
{
// Overriding a card with itself is the reset case, not a real override: every code path below
// would re-enter itself through emitPixmapUpdated(). Reject it outright.
if (originalCard == overrideCard) {
return;
}
CardInfoPtr cardPtr = overrideCard.getCardPtr();
if (!cardPtr) {
return;
}
// Heap-allocate so the lambda can capture it before the connection is made
auto *connectionHandle = new QMetaObject::Connection;
*connectionHandle =
connect(cardPtr.data(), &CardInfo::pixmapUpdated, cardPtr.data(),
[originalCard, overrideCard, connectionHandle, this](const PrintingInfo &printing) {
// All printings share the same CardInfo, so ignore updates triggered by any
// other printing (e.g., the original card re-loading from disk).
if (printing != overrideCard.getPrinting()) {
return;
}
QPixmap pixmap;
if (QPixmapCache::find(overrideCard.getPixmapCacheKey(), &pixmap) && !pixmap.isNull()) {
// The override art has resolved — persist it and reflect it immediately.
// Retire the connection before emitting so the refresh can't re-enter.
saveCardImageToLocalStorage(originalCard, pixmap, /*allowOverwrite=*/true);
QObject::disconnect(*connectionHandle);
delete connectionHandle;
QPixmapCache::clear();
originalCard.emitPixmapUpdated();
return;
}
// The art could not be resolved. Keep the connection armed so a late resolution
// still lands, and surface a visible refusal instead of a silent no-op. An
// override already on disk is left untouched and simply re-displayed.
QPixmapCache::clear();
if (!hasLocalOverrides(originalCard)) {
QPixmap refusedPixmap;
getCardBackLoadingFailedPixmap(refusedPixmap, QSize(480, 672));
QPixmapCache::insert(originalCard.getPixmapCacheKey(), refusedPixmap);
}
originalCard.emitPixmapUpdated();
});
// Now enqueue; if the image is already loading (deduplicated in the worker),
// the signal will still fire when it completes
CardPictureLoader::getInstance().worker->enqueueImageLoad(overrideCard);
}
void CardPictureLoader::installPrintingOverride(const ExactCard &originalCard, const ExactCard &overrideCard)
{
// Same guard as installPrintingOverrideOnLoad: self-override is the reset case.
if (originalCard == overrideCard) {
return;
}
QPixmap pixmap;
const QString key = overrideCard.getPixmapCacheKey();
if (QPixmapCache::find(key, &pixmap) && !pixmap.isNull()) {
// Already cached — save immediately; the caller refreshes the card.
saveCardImageToLocalStorage(originalCard, pixmap, /*allowOverwrite=*/true);
return;
}
// Cache miss or previously failed load — enqueue load and wait for the signal.
installPrintingOverrideOnLoad(originalCard, overrideCard);
}
bool CardPictureLoader::hasLocalOverrides(const ExactCard &card)
{
const QString picsRoot = SettingsCache::instance().paths().getPicsPath();
if (picsRoot.isEmpty() || !card) {
return false;
}
QDir baseDir(picsRoot);
if (!baseDir.cd("downloadedPics")) {
return false;
}
const QString name = card.getInfo().getCorrectedName();
QString set, collector, uuid;
const PrintingInfo printing = card.getPrinting();
if (printing.getSet()) {
set = printing.getSet()->getCorrectedShortName();
collector = printing.getProperty("num");
uuid = printing.getUuid();
}
for (const auto &scheme : CardPictureLoaderLocalSchemes::exportSchemes()) {
QString rel = CardPictureLoaderLocalSchemes::expandPattern(scheme.pattern, name, set, collector, uuid);
if (rel.isEmpty()) {
continue;
}
rel += ".png";
rel = QDir::cleanPath(rel);
if (QFile::exists(baseDir.filePath(rel))) {
return true;
}
}
return false;
}
void CardPictureLoader::clearPixmapCache()
{
QPixmapCache::clear();
@ -489,11 +327,31 @@ void CardPictureLoader::picsPathChanged()
QPixmapCache::clear();
}
void CardPictureLoader::cardLangChanged()
bool CardPictureLoader::hasCustomArt()
{
// Localized images are fetched via a different URL, but the in-memory
// pixmap cache is keyed by card name/uuid, so drop everything cached
// (including failure timestamps) to force a reload in the new language.
QPixmapCache::clear();
failedAt.clear();
auto picsPath = SettingsCache::instance().paths().getPicsPath();
QDirIterator it(picsPath, QDir::Dirs | QDir::NoDotAndDotDot);
// Check if there is at least one non-directory file in the pics path, other
// than in the "downloadedPics" subdirectory.
while (it.hasNext()) {
#if (QT_VERSION >= QT_VERSION_CHECK(6, 3, 0))
QFileInfo dir(it.nextFileInfo());
#else
// nextFileInfo() is only available in Qt 6.3+, for previous versions, we build
// the QFileInfo from a QString which requires more system calls.
QFileInfo dir(it.next());
#endif
if (it.fileName() == "downloadedPics") {
continue;
}
QDirIterator subIt(it.filePath(), QDir::Files, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
if (subIt.hasNext()) {
return true;
}
}
return false;
}

View file

@ -97,17 +97,10 @@ public:
static void cacheCardPixmaps(const QList<ExactCard> &cards);
/**
* @brief Check if a local override image already exists for the card.
* @param card The card to check.
* @return True if the card has at least one locally stored override image.
* @brief Check if the user has custom card art in the picsPath directory.
* @return True if any custom art exists.
*/
static bool hasLocalOverrides(const ExactCard &card);
/**
* @brief Removes all locally stored override images for the card.
* @param card The card to remove the override images of.
*/
static void deleteAllLocalOverrides(const ExactCard &card);
static bool hasCustomArt();
/**
* @brief Clears the in-memory QPixmap cache for all cards.
@ -127,9 +120,7 @@ public slots:
* @param image Loaded QImage.
*/
void imageLoaded(const ExactCard &card, const QImage &image);
void saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap, bool allowOverwrite = false);
void installPrintingOverride(const ExactCard &originalCard, const ExactCard &overrideCard);
void installPrintingOverrideOnLoad(const ExactCard &originalCard, const ExactCard &overrideCard);
void saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap);
private slots:
/**
@ -143,12 +134,6 @@ private slots:
* Clears the QPixmap cache to reload images.
*/
void picsPathChanged();
/**
* @brief Triggered when the card language setting changes.
* Clears the in-memory picture caches so images reload in the new language.
*/
void cardLangChanged();
};
#endif

View file

@ -94,10 +94,6 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
candidatePaths << picsPath + "/downloadedPics/" + setName + "/" + nameVariant;
}
// Non-set-folder export schemes (e.g., Name_Set_Collector) write straight into
// downloadedPics/; check there as a fallback so local overrides round-trip.
candidatePaths << picsPath + "/downloadedPics/" + nameVariant;
for (const QString &path : candidatePaths) {
QFileInfo fileInfo(path);
QDir dir = fileInfo.dir();
@ -109,8 +105,7 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
QStringList files = dir.entryList(QDir::Files);
for (const QString &file : files) {
QFileInfo fi(file);
if (fi.completeBaseName() != baseName) {
if (!file.startsWith(baseName)) {
continue;
}

View file

@ -107,15 +107,15 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
// Check for cached redirects
QUrl cachedRedirect = getCachedRedirect(url);
if (!cachedRedirect.isEmpty()) {
emit imageRequestSucceeded(url);
// The redirect target is a different host, which may itself be in 429 backoff; hand the
// entry back to its worker so it waits the backoff out instead of dispatching straight
// onto the backed-off host.
if (CardPictureLoaderWorkerWork::rateLimiter().isRateLimited(cachedRedirect.host(),
QDateTime::currentDateTime())) {
worker->scheduleDeferredRetry(cachedRedirect.host());
worker->scheduleDeferredRetry();
return nullptr;
}
emit imageRequestSucceeded(url);
return makeRequest(cachedRedirect, worker);
}
@ -144,6 +144,11 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
void CardPictureLoaderWorker::resetRequestQuota()
{
// Allowances are seeded lazily per host in processSingleRequest() when a request is first
// looked at in a new second, so a host that enters the queue mid-second now gets its reduced
// per-host allowance instead of falling through to the full per-second default.
hostQuotaRemaining.clear();
QDateTime now = QDateTime::currentDateTime();
for (auto it = hostRequestQuota.begin(); it != hostRequestQuota.end(); ++it) {
if (!hostLast429.contains(it.key()) || now.msecsTo(hostLast429.value(it.key())) < -QUOTA_RECOVER_MS) {
@ -151,30 +156,35 @@ void CardPictureLoaderWorker::resetRequestQuota()
}
}
// Forget the per-second allowances; each host's allowance is re-seeded lazily from its
// reduced sustained quota the first time it is dispatched in the new second, so a host that
// enters the queue mid-second no longer falls through to a fresh full quota.
hostQuotaRemaining.clear();
updateTimerState();
processQueuedRequests();
}
void CardPictureLoaderWorker::processQueuedRequests()
{
// QTimer must be started from the thread it lives in; if this public slot is ever reached from
// another thread, replay it on the worker's event loop instead of letting start() fail silently.
if (thread() != QThread::currentThread()) {
QMetaObject::invokeMethod(this, &CardPictureLoaderWorker::processQueuedRequests, Qt::QueuedConnection);
Q_ASSERT(thread() == QThread::currentThread());
if (requestLoadQueue.isEmpty()) {
dispatchTimer.stop();
requestTimer.stop();
return;
}
updateTimerState();
// Start lazily from the worker's own thread: QTimer must be started in the thread it lives in.
if (!requestTimer.isActive()) {
requestTimer.start();
}
// Restarting an active timer would reset the pacing countdown, so a burst of enqueues could
// keep starving the dispatcher; only start it when it has actually stopped.
if (!dispatchTimer.isActive()) {
dispatchTimer.start();
}
}
void CardPictureLoaderWorker::dispatchQueuedRequest()
{
if (requestLoadQueue.isEmpty()) {
// All queued requests have been dispatched; stop the pacing timers.
updateTimerState();
// All queued requests have been dispatched; stop the pacing and quota-reset timers.
dispatchTimer.stop();
requestTimer.stop();
return;
}
@ -184,40 +194,6 @@ void CardPictureLoaderWorker::dispatchQueuedRequest()
}
}
void CardPictureLoaderWorker::updateTimerState()
{
// Never restart an active timer: that would reset the pacing countdown and a burst of enqueues
// could keep starving the dispatcher, so only (re)start a timer that has actually stopped.
if (requestLoadQueue.isEmpty()) {
dispatchTimer.stop();
// Forget per-second allowances once nothing is pending: a stale zero would otherwise delay
// the next single request by a full quota-reset interval.
hostQuotaRemaining.clear();
} else if (!dispatchTimer.isActive()) {
dispatchTimer.start();
}
// The quota timer resets allowances every second and is also the only thing that heals a host
// after a 429 (see resetRequestQuota). It must keep ticking while work is queued or a host is
// still recovering below the ceiling, and only winds down once no host needs recovery anymore.
// Keeping it alive during such idle periods lets reduced quotas recover as intended.
bool hostRecovering = false;
for (auto it = hostRequestQuota.cbegin(); it != hostRequestQuota.cend(); ++it) {
if (it.value() < MAX_REQUESTS_PER_SEC) {
hostRecovering = true;
break;
}
}
if (!requestLoadQueue.isEmpty() || hostRecovering) {
if (!requestTimer.isActive()) {
requestTimer.start();
}
} else if (requestTimer.isActive()) {
requestTimer.stop();
}
}
bool CardPictureLoaderWorker::processSingleRequest()
{
QDateTime now = QDateTime::currentDateTime();
@ -228,8 +204,8 @@ bool CardPictureLoaderWorker::processSingleRequest()
// back to its worker so it can wait the backoff out or fall through to another source,
// instead of leaving it parked in the queue with no reply pending.
if (CardPictureLoaderWorkerWork::rateLimiter().isRateLimited(host, now)) {
auto entry = requestLoadQueue.takeAt(i);
entry.second->startNextPicDownload();
requestLoadQueue.removeAt(i);
request.second->startNextPicDownload();
return true;
}
// Seed the allowance now so a host that was rate limited gets its reduced
@ -240,8 +216,8 @@ bool CardPictureLoaderWorker::processSingleRequest()
int allowance = hostQuotaRemaining.value(host);
if (allowance > 0) {
hostQuotaRemaining.insert(host, allowance - 1);
auto entry = requestLoadQueue.takeAt(i);
makeRequest(entry.first, entry.second);
makeRequest(request.first, request.second);
requestLoadQueue.removeAt(i);
return true;
}
}

View file

@ -86,7 +86,7 @@ public slots:
*/
QNetworkReply *makeRequest(const QUrl &url, CardPictureLoaderWorkerWork *workThread);
/** @brief Ensures the pacing and quota-reset timers reflect the current queue and recovery state. */
/** @brief Starts the pacing timers if there is queued work, stops them when the queue is empty. */
void processQueuedRequests();
/** @brief Chooses a request from the queue and starts it, respecting the quota and pacing. */
@ -142,9 +142,6 @@ private:
/** @brief Removes stale redirect entries older than TTL. */
void cleanStaleEntries();
/** @brief Starts or stops the pacing and quota-reset timers to match the queue and recovery state. */
void updateTimerState();
private slots:
/** @brief Resets the request quota for rate-limiting. */
void resetRequestQuota();

View file

@ -173,7 +173,7 @@ void CardPictureLoaderWorkerWork::handleFailedReply(const QNetworkReply *reply)
<< "PictureLoader: [card: " << cardToDownload.getCard().getName()
<< " set: " << cardToDownload.getSetName() << "]: Too many requests from " << host
<< ", backing off until " << backoffUntil.toString(Qt::ISODate) << ", retrying the same url";
scheduleDeferredRetry(host);
scheduleDeferredRetry();
} else {
qCWarning(CardPictureLoaderWorkerWorkLog).nospace()
<< "PictureLoader: [card: " << cardToDownload.getCard().getName()
@ -278,16 +278,14 @@ QImage CardPictureLoaderWorkerWork::tryLoadImageFromReply(QNetworkReply *reply)
return imgReader.read();
}
void CardPictureLoaderWorkerWork::scheduleDeferredRetry(const QString &preferredHost)
void CardPictureLoaderWorkerWork::scheduleDeferredRetry()
{
QDateTime now = QDateTime::currentDateTime();
// Prefer waiting on the server that is actually blocking the request: callers hand in the
// rate-limited host when it differs from the current URL (e.g. a cached redirect target still
// in backoff), otherwise fall back to the current URL's server so we retry the same source.
QString waitHost = preferredHost.isEmpty() ? QUrl(cardToDownload.getCurrentUrl()).host() : preferredHost;
QDateTime backoffUntil = s_rateLimiter.deadline(waitHost);
if (!s_rateLimiter.isRateLimited(waitHost, now)) {
// Prefer waiting on the current URL's server so we retry the same source.
QString currentHost = QUrl(cardToDownload.getCurrentUrl()).host();
QDateTime backoffUntil = s_rateLimiter.deadline(currentHost);
if (!s_rateLimiter.isRateLimited(currentHost, now)) {
backoffUntil = s_rateLimiter.earliestDeadline(now);
}

View file

@ -10,7 +10,6 @@
#include <QNetworkAccessManager>
#include <QObject>
#include <QRandomGenerator>
#include <QString>
#include <QThread>
#include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/utility/server_rate_limiter.h>
@ -59,14 +58,13 @@ public:
/**
* @brief Schedules a deferred retry after the relevant server backoff expires.
* @param preferredHost The server that is actually blocking the request, or an empty
* string to use the current URL's server
*
* Waits on the blocking server's backoff deadline, otherwise on the earliest active
* backoff. If no servers are in backoff, concludes with failure. Otherwise resets the
* CardPictureToLoad indices and retries after the backoff period.
* Waits on the current URL's server when it is the reason we are blocked,
* otherwise on the earliest active backoff. If no servers are in backoff,
* concludes with failure. Otherwise resets the CardPictureToLoad indices and
* retries after the backoff period.
*/
void scheduleDeferredRetry(const QString &preferredHost = {});
void scheduleDeferredRetry();
public slots:
/**

View file

@ -94,8 +94,7 @@ void CardPictureToLoad::populateSetUrls()
}
}
const QStringList orderedTemplates = urlTemplates;
for (const QString &urlTemplate : orderedTemplates) {
for (const QString &urlTemplate : urlTemplates) {
QString transformedUrl = transformUrl(urlTemplate);
if (!transformedUrl.isEmpty()) {
@ -283,15 +282,8 @@ QString CardPictureToLoad::transformUrl(const QString &urlTemplate) const
}
// language setting
const QString cardLang = SettingsCache::instance().cardsDisplay().getCardLang();
transformMap["!sflang!"] = cardLang;
// The localized printing's own id is unknown, so Scryfall must resolve it by
// its translated name (see populateSetUrls); expose that name for the
// `/cards/named` template.
if (cardLang != "en") {
transformMap["!localizedName!"] = card.getInfo().getLocalizedName(cardLang);
}
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()) {

View file

@ -1,14 +0,0 @@
#ifndef COCKATRICE_CONTEXT_OPEN_DECK_H
#define COCKATRICE_CONTEXT_OPEN_DECK_H
#include "context_connect_to_server.h"
#include <QString>
struct ContextOpenDeck
{
ContextConnectToServer serverContext;
QString shareToken;
};
#endif // COCKATRICE_CONTEXT_OPEN_DECK_H

View file

@ -2,11 +2,10 @@
Intent::Intent(QObject *parent) : QObject(parent)
{
// An intent is done as soon as it reports success, failure, or cancellation.
// Deleting it also tears down its dependency chain and disconnects any signal wiring.
// An intent is done as soon as it reports success or failure. Deleting it
// also tears down its dependency chain and disconnects any signal wiring.
connect(this, &Intent::finished, this, &QObject::deleteLater);
connect(this, &Intent::failed, this, &QObject::deleteLater);
connect(this, &Intent::cancelled, this, &QObject::deleteLater);
}
Intent::~Intent() = default;
@ -28,7 +27,6 @@ void Intent::runDependency(Intent *dependency)
this->execute();
});
connect(dependency, &Intent::failed, this, &Intent::failed);
connect(dependency, &Intent::cancelled, this, &Intent::cancelled);
dependency->execute();
}
@ -48,11 +46,3 @@ void Intent::emitFailed(const QString &reason)
emit failed(reason);
}
}
void Intent::emitCancelled()
{
if (!completed) {
completed = true;
emit cancelled();
}
}

View file

@ -16,7 +16,6 @@ public:
signals:
void finished();
void failed(QString reason);
void cancelled();
protected:
// --- Subclasses must implement these ---
@ -30,7 +29,6 @@ protected:
// Emit the outcome exactly once; ignore late signals after the intent is done.
void emitFinished();
void emitFailed(const QString &reason);
void emitCancelled();
private:
bool completed = false;

View file

@ -19,15 +19,13 @@ bool IntentJoinServerGame::checkPrecondition() const
if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) {
return false;
}
// serverName()/serverPort() reflect the server the client was configured
// to connect to, which may differ from the actual TCP peer (e.g. when
// connecting through a proxy), so compare those configured values. A link
// naming the same host on another port is a different server and must not
// reuse the session there.
if (remoteClient->serverName().compare(context->roomContext.serverContext.hostname, Qt::CaseInsensitive) != 0) {
// peerPort() reflects the actual TCP peer, which may differ from the
// configured server port (e.g. when connecting through a proxy), so only
// the hostname is compared here.
if (remoteClient->peerName() != context->roomContext.serverContext.hostname) {
return false;
}
if (QString::number(remoteClient->serverPort()) != context->roomContext.serverContext.port) {
if (QString::number(remoteClient->peerPort()) != context->roomContext.serverContext.port) {
return false;
}

View file

@ -1,14 +1,9 @@
#include "intent_login.h"
#include "../../client/settings/cache_settings.h"
#include "../widgets/dialogs/dlg_login_prompt.h"
#include "libcockatrice/settings/servers_settings.h"
#include <QDialog>
IntentGetLoginCredentials::IntentGetLoginCredentials(ContextConnectToServer *_context,
bool _promptForMissingCredentials)
: Intent(), context(_context), promptForMissingCredentials(_promptForMissingCredentials)
IntentGetLoginCredentials::IntentGetLoginCredentials(ContextConnectToServer *_context) : Intent(), context(_context)
{
}
@ -34,46 +29,5 @@ void IntentGetLoginCredentials::onPreconditionSatisfied()
void IntentGetLoginCredentials::onPreconditionNotSatisfied()
{
// MainWindow::applyStartupDestination runs this intent on every launch for
// users whose startup tab is Server / Server Room; keep that path quiet, as
// it was before the link-driven sign-in dialog existed.
if (!promptForMissingCredentials) {
emitFailed(tr("No saved credentials for this server"));
return;
}
// No credentials saved for the target server: ask the user for them. They
// opt into saving them so later links to the same server connect directly.
const QString serverText = context->hostname + ":" + context->port;
DlgLoginPrompt dialog(serverText);
// ApplicationModal: the dialog has no parent (the intent is not a widget),
// so WindowModal would not actually block any other window.
dialog.setWindowModality(Qt::ApplicationModal);
if (dialog.exec() != QDialog::Accepted) {
emitCancelled();
return;
}
context->username = dialog.username();
context->password = dialog.password();
if (dialog.savePassword() && !context->username.isEmpty()) {
ServersSettings &servers = SettingsCache::instance().servers();
// The host may already be saved under a friendly name (e.g. a public-server
// list entry) with no credentials; reuse that name instead of overwriting
// it with the raw hostname when addNewServer updates the entry in place.
QString saveName = context->hostname;
const int existingIndex = servers.findServerIndex(context->hostname, context->port);
if (existingIndex >= 0) {
saveName =
servers.getValue(QString("saveName%1").arg(existingIndex), "server", "server_details").toString();
if (saveName.isEmpty()) {
saveName = context->hostname;
}
}
servers.addNewServer(saveName, context->hostname, context->port, context->username, context->password, true);
}
emitFinished();
emitFailed(tr("No saved credentials for this server"));
}

View file

@ -9,10 +9,7 @@ class IntentGetLoginCredentials : public Intent
Q_OBJECT
public:
// When promptForMissingCredentials is false (the default) a server without
// saved credentials fails silently; only intent chains from cockatrice://
// links opt into the interactive sign-in dialog.
explicit IntentGetLoginCredentials(ContextConnectToServer *_context, bool _promptForMissingCredentials = false);
IntentGetLoginCredentials(ContextConnectToServer *_context);
protected:
bool checkPrecondition() const override;
@ -21,7 +18,6 @@ protected:
private:
ContextConnectToServer *context;
bool promptForMissingCredentials;
};
#endif // COCKATRICE_INTENT_LOGIN_H

View file

@ -1,208 +0,0 @@
#include "intent_open_shared_deck.h"
#include "../deck_loader/deck_loader.h"
#include "../widgets/dialogs/dlg_shared_decks_preview.h"
#include "../widgets/tabs/tab_supervisor.h"
#include "intent_connect_to_server.h"
#include <QMessageBox>
#include <QTimer>
#include <libcockatrice/card/database/card_database_querier.h>
#include <libcockatrice/protocol/pb/command_deck_share_download.pb.h>
#include <libcockatrice/protocol/pb/command_deck_share_list.pb.h>
#include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pb/response_deck_share_download.pb.h>
#include <libcockatrice/protocol/pb/response_deck_share_list.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_deck_share_item.pb.h>
#include <libcockatrice/protocol/pending_command.h>
IntentOpenSharedDeck::IntentOpenSharedDeck(TabSupervisor *_tabSupervisor,
RemoteClient *_remoteClient,
const CardDatabaseQuerier *_querier,
std::unique_ptr<ContextOpenDeck> _context)
: Intent(), tabSupervisor(_tabSupervisor), remoteClient(_remoteClient), querier(_querier),
context(_context.release())
{
downloadTimer = new QTimer(this);
downloadTimer->setSingleShot(true);
downloadTimer->setInterval(15000);
connect(downloadTimer, &QTimer::timeout, this, &IntentOpenSharedDeck::onDownloadTimeout);
}
bool IntentOpenSharedDeck::checkPrecondition() const
{
if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) {
return false;
}
// serverName()/serverPort() reflect the server the client was configured
// to connect to, which may differ from the actual TCP peer (e.g. when
// connecting through a proxy), so compare those configured values. The
// share token must be resolved against the host the link named — a link to
// the same host on another port is a different server.
if (remoteClient->serverName().compare(context->serverContext.hostname, Qt::CaseInsensitive) != 0) {
return false;
}
return QString::number(remoteClient->serverPort()) == context->serverContext.port;
}
void IntentOpenSharedDeck::onPreconditionSatisfied()
{
// Resolve the share token to its items first; a share can contain more than
// one deck, and each item is downloaded by id. Time the round trip like the
// downloads, so a silent server cannot hang the chain forever.
listPhase = true;
downloadTimer->start();
Command_DeckShareList cmd;
cmd.set_token(context->shareToken.toStdString());
PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, &IntentOpenSharedDeck::listShareFinished);
remoteClient->sendCommand(pend);
}
void IntentOpenSharedDeck::onPreconditionNotSatisfied()
{
runDependency(new IntentConnectToServer(remoteClient, &context->serverContext));
}
void IntentOpenSharedDeck::listShareFinished(const Response &response, const CommandContainer & /* commandContainer */)
{
downloadTimer->stop();
listPhase = false;
if (response.response_code() != Response::RespOk) {
emitFailed(tr("The shared deck could not be found or has expired"));
return;
}
const Response_DeckShareList &resp = response.GetExtension(Response_DeckShareList::ext);
if (resp.items_size() == 0) {
emitFailed(tr("The shared deck is empty"));
return;
}
QList<ServerInfo_DeckShareItem> items;
items.reserve(resp.items_size());
for (const ServerInfo_DeckShareItem &item : resp.items()) {
items.append(item);
itemNames.insert(item.id(), QString::fromStdString(item.name()));
}
const QString serverText = context->serverContext.hostname + ":" + context->serverContext.port;
// Ask the user which decks to open before downloading anything.
previewDialog = new DlgSharedDecksPreview(tabSupervisor, querier, QString::fromStdString(resp.name()),
resp.expires_at(), serverText, items);
connect(previewDialog, &DlgSharedDecksPreview::openRequested, this, &IntentOpenSharedDeck::startDownloads);
connect(previewDialog, &DlgSharedDecksPreview::cancelled, this, &IntentOpenSharedDeck::emitCancelled);
connect(previewDialog, &DlgSharedDecksPreview::cancelled, previewDialog, &QWidget::deleteLater);
previewDialog->show();
previewDialog->raise();
previewDialog->activateWindow();
}
void IntentOpenSharedDeck::startDownloads(const QList<int> &itemIds)
{
pendingItemIds = itemIds;
totalItems = itemIds.size();
completedItems = 0;
loadedDecks.clear();
downloadNextItem();
}
void IntentOpenSharedDeck::downloadNextItem()
{
if (pendingItemIds.isEmpty()) {
finishAll();
return;
}
currentItemId = pendingItemIds.takeFirst();
downloadTimer->start();
Command_DeckShareDownload cmd;
cmd.set_token(context->shareToken.toStdString());
cmd.set_item_id(currentItemId);
PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, &IntentOpenSharedDeck::downloadShareFinished);
remoteClient->sendCommand(pend);
}
void IntentOpenSharedDeck::downloadShareFinished(const Response &response,
const CommandContainer & /* commandContainer */)
{
downloadTimer->stop();
QString failureReason;
if (response.response_code() != Response::RespOk) {
failureReason = tr("Failed to download the shared deck");
} else {
const Response_DeckShareDownload &resp = response.GetExtension(Response_DeckShareDownload::ext);
const QString deckString = QString::fromStdString(resp.deck());
if (deckString.isEmpty()) {
failureReason = tr("The shared deck is empty");
} else {
std::optional<LoadedDeck> deckOpt =
DeckLoader::loadFromRemote(deckString, LoadedDeck::LoadInfo::NON_REMOTE_ID);
if (!deckOpt) {
failureReason = tr("The shared deck could not be loaded");
} else {
loadedDecks.append(deckOpt.value());
++completedItems;
previewDialog->setDownloadProgress(completedItems, totalItems,
itemNames.value(currentItemId, tr("Unknown deck")));
downloadNextItem();
return;
}
}
}
onItemFailure(failureReason);
}
void IntentOpenSharedDeck::onItemFailure(const QString &reason)
{
downloadTimer->stop();
if (loadedDecks.isEmpty()) {
previewDialog->deleteLater();
emitFailed(reason);
return;
}
const int downloadedCount = loadedDecks.size();
const QMessageBox::StandardButton answer = QMessageBox::question(
previewDialog, tr("Open shared decks"),
tr("Could not download the deck \"%1\".\n\n%n deck(s) were already downloaded. Open them?", "", downloadedCount)
.arg(itemNames.value(currentItemId, tr("Unknown deck"))),
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
if (answer == QMessageBox::Yes) {
finishAll();
} else {
previewDialog->deleteLater();
emitCancelled();
}
}
void IntentOpenSharedDeck::onDownloadTimeout()
{
// The list phase has no preview dialog yet to report progress into; fail the
// whole intent instead of letting the shared deck hang in limbo.
if (listPhase) {
emitFailed(tr("Timed out while loading the shared deck"));
return;
}
onItemFailure(tr("Timed out while downloading the shared deck"));
}
void IntentOpenSharedDeck::finishAll()
{
previewDialog->deleteLater();
for (const LoadedDeck &deck : loadedDecks) {
tabSupervisor->openDeckInNewTab(deck);
}
emitFinished();
}

View file

@ -1,60 +0,0 @@
#ifndef COCKATRICE_INTENT_OPEN_SHARED_DECK_H
#define COCKATRICE_INTENT_OPEN_SHARED_DECK_H
#include "contexts/context_open_deck.h"
#include "intent.h"
#include "remote_client.h"
#include <QList>
#include <QMap>
#include <QScopedPointer>
#include <memory>
class TabSupervisor;
struct LoadedDeck;
class CardDatabaseQuerier;
class DlgSharedDecksPreview;
class QTimer;
class IntentOpenSharedDeck : public Intent
{
Q_OBJECT
public:
IntentOpenSharedDeck(TabSupervisor *_tabSupervisor,
RemoteClient *_remoteClient,
const CardDatabaseQuerier *_querier,
std::unique_ptr<ContextOpenDeck> _context);
protected:
bool checkPrecondition() const override;
void onPreconditionSatisfied() override;
void onPreconditionNotSatisfied() override;
private slots:
void listShareFinished(const Response &response, const CommandContainer &commandContainer);
void downloadShareFinished(const Response &response, const CommandContainer &commandContainer);
void onDownloadTimeout();
private:
void startDownloads(const QList<int> &itemIds);
void downloadNextItem();
void onItemFailure(const QString &reason);
void finishAll();
TabSupervisor *tabSupervisor;
RemoteClient *remoteClient;
const CardDatabaseQuerier *querier;
QScopedPointer<ContextOpenDeck> context;
DlgSharedDecksPreview *previewDialog = nullptr;
QTimer *downloadTimer;
QMap<int, QString> itemNames;
QList<int> pendingItemIds;
QList<LoadedDeck> loadedDecks;
bool listPhase = true;
int currentItemId = 0;
int totalItems = 0;
int completedItems = 0;
};
#endif // COCKATRICE_INTENT_OPEN_SHARED_DECK_H

View file

@ -1,28 +1,19 @@
#include "url_parser.h"
#include "../../client/settings/cache_settings.h"
#include "../widgets/tabs/tab_room.h"
#include "../widgets/tabs/tab_supervisor.h"
#include "../window_main.h"
#include "contexts/context_join_game.h"
#include "contexts/context_open_deck.h"
#include "intent.h"
#include "intent_join_server_game.h"
#include "intent_login.h"
#include "intent_open_shared_deck.h"
#include <QDebug>
#include <QLoggingCategory>
#include <QMessageBox>
#include <QUrl>
#include <QUrlQuery>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/settings/servers_settings.h>
#include <memory>
inline Q_LOGGING_CATEGORY(UrlParserLog, "url_parser");
IntentUrlParser::IntentUrlParser(QObject *parent, MainWindow *_mainWindow) : QObject(parent), mainWindow(_mainWindow)
{
}
@ -38,33 +29,16 @@ void IntentUrlParser::handle(const QString &urlStr)
const QString action = url.host();
QUrlQuery query(url);
qCDebug(UrlParserLog) << "Parsing intent URL, action:" << action;
PendingIntentChain chain;
Intent *firstIntent = nullptr;
if (action == "joingame") {
firstIntent = createJoinGameIntent(query, chain);
handleJoinGame(query);
} else if (action == "opendeck") {
firstIntent = createOpenDeckIntent(query, chain);
// handleOpenDeck(query);
} else {
qWarning() << "Unknown intent:" << action;
}
if (firstIntent == nullptr) {
// The link was invalid or the user declined the confirm: nothing runs.
// Report the idle state when no other chain is queued so that a startup
// launch (which skipped its own connection for this URL) falls back to it.
if (!chainRunning && pendingChains.isEmpty()) {
emit urlChainFinished(mainWindow->getRemoteClient()->getStatus() == StatusLoggedIn);
}
return;
}
pendingChains.append(chain);
startNextChain();
}
Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, PendingIntentChain &chain)
void IntentUrlParser::handleJoinGame(const QUrlQuery &query)
{
auto showError = [this](const QString &message) { QMessageBox::warning(mainWindow, tr("Open game"), message); };
@ -75,21 +49,21 @@ Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, PendingInt
if (ctx->roomContext.serverContext.hostname.isEmpty()) {
showError(tr("Missing or empty hostname in the game link"));
return nullptr;
return;
}
bool ok = false;
ctx->roomContext.serverContext.port.toUShort(&ok);
if (!ok) {
showError(tr("Invalid or missing port in the game link"));
return nullptr;
return;
}
ctx->roomContext.roomId = query.queryItemValue("roomid").toInt(&ok);
if (!ok) {
showError(tr("Invalid or missing room id in the game link"));
return nullptr;
return;
}
ok = false;
@ -97,7 +71,7 @@ Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, PendingInt
if (!ok) {
showError(tr("Invalid or missing game id in the game link"));
return nullptr;
return;
}
const QString gameDescription = query.queryItemValue("game", QUrl::FullyDecoded);
@ -106,33 +80,24 @@ Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, PendingInt
const QMessageBox::StandardButton answer = QMessageBox::question(
mainWindow, tr("Join game"), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
if (answer != QMessageBox::Yes) {
return nullptr;
return;
}
RemoteClient *client = mainWindow->getRemoteClient();
ContextConnectToServer *serverContext = &ctx->roomContext.serverContext;
// The join game intent owns the context and the credential lookup; once the
// chain finishes (or fails) it deletes the whole tree.
auto joinGameIntent = new IntentJoinServerGame(mainWindow->getTabSupervisor(), client, std::move(ctx));
ContextConnectToServer *serverContext = &ctx->roomContext.serverContext;
auto joinGameIntent =
new IntentJoinServerGame(mainWindow->getTabSupervisor(), mainWindow->getRemoteClient(), std::move(ctx));
joinGameIntent->setParent(this);
chain.intents.append(joinGameIntent);
auto getLoginCredentialsIntent = new IntentGetLoginCredentials(serverContext);
getLoginCredentialsIntent->setParent(joinGameIntent);
connect(getLoginCredentialsIntent, &Intent::finished, joinGameIntent, &Intent::execute);
connect(getLoginCredentialsIntent, &Intent::failed, joinGameIntent, &Intent::failed);
connect(joinGameIntent, &Intent::failed, this, [showError](const QString &reason) { showError(reason); });
Intent *firstIntent = joinGameIntent;
if (!isConnectedTo(serverContext->hostname, serverContext->port)) {
auto getLoginCredentialsIntent =
new IntentGetLoginCredentials(serverContext, /*promptForMissingCredentials=*/true);
getLoginCredentialsIntent->setParent(joinGameIntent);
chain.intents.insert(0, getLoginCredentialsIntent);
connect(getLoginCredentialsIntent, &Intent::finished, joinGameIntent, &Intent::execute);
connect(getLoginCredentialsIntent, &Intent::failed, joinGameIntent, &Intent::failed);
connect(getLoginCredentialsIntent, &Intent::cancelled, joinGameIntent, &Intent::cancelled);
firstIntent = getLoginCredentialsIntent;
}
return firstIntent;
getLoginCredentialsIntent->execute();
}
QString IntentUrlParser::generateJoinGameMessage(const ContextJoinGame &context, const QString &gameDescription)
@ -169,270 +134,3 @@ QString IntentUrlParser::generateJoinGameMessage(const ContextJoinGame &context,
.arg(gameDescription, gameIdStr, roomTab->getRoomName(), server)
: tr("Join game \"%1\" (#%2) on %3?").arg(gameDescription, gameIdStr, server);
}
Intent *IntentUrlParser::createOpenDeckIntent(const QUrlQuery &query, PendingIntentChain &chain)
{
auto showError = [this](const QString &message) {
QMessageBox::warning(mainWindow, tr("Open shared deck"), message);
};
auto ctx = std::make_unique<ContextOpenDeck>();
ctx->serverContext.hostname = query.queryItemValue("hostname");
ctx->serverContext.port = query.queryItemValue("port");
ctx->shareToken = query.queryItemValue("share");
qCDebug(UrlParserLog) << "Open-deck intent: host" << ctx->serverContext.hostname << "port"
<< ctx->serverContext.port << "token length" << ctx->shareToken.length();
if (ctx->serverContext.hostname.isEmpty()) {
showError(tr("Missing or empty hostname in the share link"));
return nullptr;
}
bool ok = false;
const quint16 port = ctx->serverContext.port.toUShort(&ok);
if (!ok || port == 0) {
showError(tr("Invalid or missing port in the share link"));
return nullptr;
}
if (ctx->shareToken.isEmpty()) {
showError(tr("Missing or empty share value in the share link"));
return nullptr;
}
RemoteClient *client = mainWindow->getRemoteClient();
// The open deck download needs a connection to the link's server. Ask before
// taking the session anywhere it isn't already, naming the host we would
// connect to. Remember the link's target when it moves us away from a live
// session so a failed or cancelled chain can restore the session it left.
// The hostname is link-supplied and percent-decoded, so escape it: QMessageBox
// renders AutoText, and markup in a hostname would otherwise flip the whole
// prompt to rich text and let a link pad the message the user is shown.
const bool alreadyConnected = isConnectedTo(ctx->serverContext.hostname, ctx->serverContext.port);
if (!alreadyConnected) {
const QString target =
QStringLiteral("%1:%2").arg(ctx->serverContext.hostname.toHtmlEscaped(), ctx->serverContext.port);
if (client->getStatus() == StatusLoggedIn) {
const QString current =
QStringLiteral("%1:%2").arg(client->serverName(), QString::number(client->serverPort()));
const QMessageBox::StandardButton answer = QMessageBox::question(
mainWindow, tr("Open shared deck"),
tr("Opening this share link connects you to %1 instead of %2.\n\nContinue?").arg(target, current),
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
if (answer != QMessageBox::Yes) {
return nullptr;
}
chain.migrationTargetHost = ctx->serverContext.hostname;
chain.migrationTargetPort = ctx->serverContext.port;
chain.pendingRestore = true;
} else {
// Fresh connection is harmless to wander away from, but a server the
// client has never been configured for deserves a harder warning (no
// by default) so a stray link cannot silently steer the client there.
const bool knownHost = SettingsCache::instance().servers().findHostIndex(ctx->serverContext.hostname) >= 0;
const QMessageBox::StandardButton answer =
knownHost
? QMessageBox::question(mainWindow, tr("Open shared deck"),
tr("Opening this share link connects you to %1.\n\nContinue?").arg(target))
: QMessageBox::warning(mainWindow, tr("Open shared deck"),
tr("Opening this share link connects you to %1, a server you have "
"never connected to before.\n\nContinue?")
.arg(target),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (answer != QMessageBox::Yes) {
return nullptr;
}
}
}
ContextConnectToServer *serverContext = &ctx->serverContext;
// The open deck intent owns the context and the credential lookup; once
// the chain finishes (or fails) it deletes the whole tree.
auto openDeckIntent =
new IntentOpenSharedDeck(mainWindow->getTabSupervisor(), client, CardDatabaseManager::query(), std::move(ctx));
openDeckIntent->setParent(this);
chain.intents.append(openDeckIntent);
connect(openDeckIntent, &Intent::failed, this, [showError](const QString &reason) { showError(reason); });
Intent *firstIntent = openDeckIntent;
if (!isConnectedTo(serverContext->hostname, serverContext->port)) {
auto getLoginCredentialsIntent =
new IntentGetLoginCredentials(serverContext, /*promptForMissingCredentials=*/true);
getLoginCredentialsIntent->setParent(openDeckIntent);
chain.intents.insert(0, getLoginCredentialsIntent);
connect(getLoginCredentialsIntent, &Intent::finished, openDeckIntent, &Intent::execute);
connect(getLoginCredentialsIntent, &Intent::failed, openDeckIntent, &Intent::failed);
connect(getLoginCredentialsIntent, &Intent::cancelled, openDeckIntent, &Intent::cancelled);
firstIntent = getLoginCredentialsIntent;
}
return firstIntent;
}
bool IntentUrlParser::isConnectedTo(const QString &hostname, const QString &port) const
{
// serverName() reflects the server the client was configured to connect to,
// which may differ from the actual TCP peer (e.g. when connecting through a
// proxy), so compare the configured host and port — exactly what a link
// names. A link to the same host on another port is a different server and
// must not silently reuse an existing session there.
RemoteClient *client = mainWindow->getRemoteClient();
return client->getStatus() == StatusLoggedIn && client->serverName().compare(hostname, Qt::CaseInsensitive) == 0 &&
QString::number(client->serverPort()) == port;
}
void IntentUrlParser::startNextChain()
{
if (chainRunning || pendingChains.isEmpty()) {
return;
}
chainRunning = true;
PendingIntentChain &chain = pendingChains.first();
if (chain.intents.isEmpty()) {
pendingChains.removeFirst();
chainRunning = false;
startNextChain();
return;
}
// Snapshot the session this chain moves away from now that it actually
// runs. Chains are parsed while earlier ones are still queued, so a capture
// at parse time would follow whichever server the chain before it settled
// on, not the one the user is really on when this link is handled.
if (chain.pendingRestore) {
RemoteClient *client = mainWindow->getRemoteClient();
chain.previousServerHost = client->serverName();
chain.previousServerPort = QString::number(client->serverPort());
}
// Only the last intent completes the chain; its terminal signal ends the
// whole run. Cancellation of an intermediate intent (e.g. declined login
// prompt) is forwarded onto the last intent in the chain builders above.
Intent *finalIntent = chain.intents.last();
connect(finalIntent, &Intent::finished, this, [this]() { chainEnded(true); });
connect(finalIntent, &Intent::failed, this, [this]() { chainEnded(false); });
connect(finalIntent, &Intent::cancelled, this, [this]() { chainEnded(false); });
// Backstop: if the final intent is destroyed without emitting a terminal
// signal (e.g. a network error dropped it while running), end the chain so
// later links are not queued and dropped for the rest of the session.
chainBackstopConnection = connect(finalIntent, &QObject::destroyed, this, &IntentUrlParser::onChainIntentDestroyed);
chain.intents.first()->execute();
}
void IntentUrlParser::chainEnded(bool chainSucceeded)
{
chainRunning = false;
QObject::disconnect(chainBackstopConnection);
const PendingIntentChain chain = pendingChains.takeFirst();
// Only a failed or cancelled chain restores the session the link migrated
// away from; a successful one leaves the user where they are.
if (chain.pendingRestore && !chainSucceeded) {
restorePreviousServer(chain);
}
startNextChain();
// Only report the terminal state once the queue has fully drained, so a
// queued follow-up link keeps the startup fallback out of the picture.
if (!chainRunning && pendingChains.isEmpty()) {
emit urlChainFinished(mainWindow->getRemoteClient()->getStatus() == StatusLoggedIn);
}
}
void IntentUrlParser::onChainIntentDestroyed()
{
if (!chainRunning) {
return;
}
qCWarning(UrlParserLog) << "Share-link intent destroyed without a terminal signal; ending its chain";
chainEnded(false);
}
void IntentUrlParser::restorePreviousServer(const PendingIntentChain &chain)
{
if (chain.previousServerHost.isEmpty()) {
return;
}
RemoteClient *client = mainWindow->getRemoteClient();
const ClientStatus status = client->getStatus();
// A failed/cancelled chain can fire while the client is still settling the
// in-flight connection attempt (wrong password, connect timeout). Only
// decide once the client has settled into logged-in or disconnected;
// deciding mid-connect would strand the user offline from their previous
// server.
if (status == StatusDisconnected || status == StatusLoggedIn) {
restoreToPreviousServer(chain);
return;
}
auto waitConnection = std::make_shared<QMetaObject::Connection>();
*waitConnection = connect(client, &RemoteClient::statusChanged, this, [this, chain, client, waitConnection]() {
const ClientStatus settled = client->getStatus();
if (settled == StatusDisconnected || settled == StatusLoggedIn) {
QObject::disconnect(*waitConnection);
restoreToPreviousServer(chain);
}
});
}
void IntentUrlParser::restoreToPreviousServer(const PendingIntentChain &chain)
{
RemoteClient *client = mainWindow->getRemoteClient();
// Back on the previous server already → nothing to undo.
if (client->serverName().compare(chain.previousServerHost, Qt::CaseInsensitive) == 0 &&
QString::number(client->serverPort()) == chain.previousServerPort) {
return;
}
// When logged in somewhere, only intervene if that somewhere is the server
// the link moved us to; if the user went elsewhere on their own, leave them.
if (client->getStatus() == StatusLoggedIn) {
const bool onMigrationTarget =
client->serverName().compare(chain.migrationTargetHost, Qt::CaseInsensitive) == 0 &&
QString::number(client->serverPort()) == chain.migrationTargetPort;
if (!onMigrationTarget) {
return;
}
ServersSettings &servers = SettingsCache::instance().servers();
const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort);
if (index >= 0 && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) {
const QString username =
servers.getValue(QString("username%1").arg(index), "server", "server_details").toString();
const QString password =
servers.getValue(QString("password%1").arg(index), "server", "server_details").toString();
client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), username, password);
return;
}
client->disconnectFromServer();
return;
}
if (client->getStatus() != StatusDisconnected) {
return;
}
// The link's connection attempt failed: reconnect to the previous server
// when credentials are saved, otherwise stay offline.
ServersSettings &servers = SettingsCache::instance().servers();
const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort);
if (index >= 0 && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) {
const QString username =
servers.getValue(QString("username%1").arg(index), "server", "server_details").toString();
const QString password =
servers.getValue(QString("password%1").arg(index), "server", "server_details").toString();
client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), username, password);
}
}

View file

@ -1,46 +1,10 @@
#ifndef COCKATRICE_URL_PARSER_H
#define COCKATRICE_URL_PARSER_H
#include <QList>
#include <QObject>
#include <QUrlQuery>
class Intent;
class MainWindow;
struct ContextJoinGame;
/**
* @brief One queued intent chain with the session-migration bookkeeping for it.
*
* The restore fields are per-chain on purpose: chains are parsed while earlier
* ones are still queued, so parser-wide state would let one chain's failure
* consume the restore data another chain recorded.
*/
struct PendingIntentChain
{
QList<Intent *> intents;
// Snapshot of the session in place when this chain started running, so a
// queued chain follows whichever server the chain before it settled on.
QString previousServerHost;
QString previousServerPort;
// Recorded at parse time when the user confirmed migrating away from a live
// session to the host/port named by the link.
QString migrationTargetHost;
QString migrationTargetPort;
bool pendingRestore = false;
};
/**
* @brief Parses cockatrice:// links and runs them as serialized intent chains.
*
* Links are parsed by action (joingame/opendeck) and translated into an intent
* chain. Chains are queued and run one at a time: a document can hand multiple
* links to the window while an earlier chain still connects, and running two
* connect chains concurrently tears the connection down. urlChainFinished is
* emitted once the queue has fully drained.
*/
class IntentUrlParser : public QObject
{
Q_OBJECT
@ -48,28 +12,12 @@ class IntentUrlParser : public QObject
public:
IntentUrlParser(QObject *parent, MainWindow *mainWindow);
void handle(const QString &urlStr);
signals:
/** @brief Emitted when the last queued chain ended; carries whether the client is logged in. */
void urlChainFinished(bool connected);
void handleJoinGame(const QUrlQuery &query);
private:
Intent *createJoinGameIntent(const QUrlQuery &query, PendingIntentChain &chain);
Intent *createOpenDeckIntent(const QUrlQuery &query, PendingIntentChain &chain);
QString generateJoinGameMessage(const ContextJoinGame &context, const QString &gameDescription);
[[nodiscard]] bool isConnectedTo(const QString &hostname, const QString &port) const;
void startNextChain();
void chainEnded(bool chainSucceeded);
void onChainIntentDestroyed();
void restorePreviousServer(const PendingIntentChain &chain);
void restoreToPreviousServer(const PendingIntentChain &chain);
MainWindow *mainWindow;
QList<PendingIntentChain> pendingChains;
bool chainRunning = false;
// Disconnects the destroyed-signal backstop once a chain ends, so an old
// intent's deferred deletion cannot end the chain that runs after it.
QMetaObject::Connection chainBackstopConnection;
};
#endif // COCKATRICE_URL_PARSER_H

View file

@ -1,5 +1,6 @@
#include "palette_editor_dialog.h"
#include "../../client/settings/cache_settings.h"
#include "../theme_manager.h"
#include "palette_generator.h"
#include "palette_grid_widget.h"
@ -10,11 +11,31 @@
#include <QDialogButtonBox>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QFrame>
#include <QGuiApplication>
#include <QLabel>
#include <QLoggingCategory>
#include <QMessageBox>
#include <QPushButton>
#include <QStyleHints>
#include <QTimer>
#include <libcockatrice/settings/paths_settings.h>
// Probe whether a directory is truly writable by trying to create and remove a
// temporary file. QFileInfo::isWritable() on a directory is unreliable (notably
// on Windows where UAC VirtualStore can make a system dir appear writable).
static bool isDirReallyWritable(const QString &dirPath)
{
const QString probe = QDir(dirPath).absoluteFilePath(".cockatrice_write_test");
QFile f(probe);
if (!f.open(QIODevice::WriteOnly)) {
return false;
}
f.close();
f.remove();
return true;
}
PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QString &_themeName, QWidget *parent)
: QDialog(parent), themeDirPath(_themeDirPath), themeName(_themeName)
@ -25,7 +46,14 @@ PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QSt
// Resolve a writable directory for saving. Built-in (Default / Fusion) and
// other read-only theme directories must be customised in the user-writable
// themes directory; otherwise the write would fail or be lost on upgrade.
saveDir = ThemeManager::writableThemeDir(themeName);
if (!themeDirPath.isEmpty() && isDirReallyWritable(themeDirPath)) {
saveDir = themeDirPath;
} else {
saveDir = QDir(SettingsCache::instance().paths().getThemesPath()).absoluteFilePath(themeName);
if (!QDir().mkpath(saveDir)) {
qWarning() << "Failed to create palette save directory:" << saveDir;
}
}
// Load both scheme configs upfront so switching is instant
loadSchemes();
@ -186,7 +214,7 @@ void PaletteEditorDialog::retranslateUi()
resetBtn->setToolTip(tr("Discard unsaved edits and restore the last saved palette"));
saveBtn->setToolTip(tr("Write palette-%1.toml and reload the theme").arg(loadedScheme.toLower()));
if (saveDir.isEmpty() || !ThemeManager::isDirReallyWritable(saveDir)) {
if (saveDir.isEmpty() || !isDirReallyWritable(saveDir)) {
saveBtn->setEnabled(false);
saveBtn->setToolTip(tr("Cannot save: this theme has no writable directory"));
}
@ -269,7 +297,7 @@ void PaletteEditorDialog::onSave()
if (it.key() == loadedScheme) {
continue;
}
if (it.value() == savedConfig.value(it.key())) {
if (it.value().colors == savedConfig.value(it.key()).colors) {
continue;
}
if (!ThemeManager::commitPalette(saveDir, it.key(), it.value())) {
@ -280,7 +308,7 @@ void PaletteEditorDialog::onSave()
}
// Commit the active scheme last so the global colour scheme matches.
if (workingConfig[loadedScheme] != savedConfig.value(loadedScheme)) {
if (workingConfig[loadedScheme].colors != savedConfig.value(loadedScheme).colors) {
if (!ThemeManager::commitPalette(saveDir, loadedScheme, workingConfig[loadedScheme])) {
QMessageBox::warning(this, tr("Save failed"),
tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(loadedScheme), saveDir));

View file

@ -150,17 +150,6 @@ PaletteConfig fromAccent(const QColor &accent, int intensity, const QString &sch
cfg.colors[CG::Disabled][CR::HighlightedText] = disText;
cfg.colors[CG::Inactive][CR::HighlightedText] = dark ? Qt::white : Qt::black;
// Accent: same primary hue as Highlight, so palettes derived from a
// QuickSetup accent always carry a matching Accent role.
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
set3(CR::Accent, hl, disText, hl);
#endif
// Application role colors: Strong tracks the primary accent, while Soft is
// the lightened, desaturated companion used for button-gradient highlights.
cfg.appColors[AppColor::AccentStrong] = hl;
cfg.appColors[AppColor::AccentSoft] = hsl(accent.lightness() + 60, qRound(accent.hslSaturation() * 70 / 100.0));
// BrightText
QColor bright;
if (achromatic) {

View file

@ -1,7 +1,5 @@
#include "palette_grid_widget.h"
#include "../theme_manager.h"
#include <QApplication>
#include <QGridLayout>
#include <QLabel>
@ -47,11 +45,6 @@ static const QMap<QPalette::ColorRole, const char *> ROLE_DESCRIPTIONS = {
{QPalette::Shadow, QT_TR_NOOP("Very dark shadow colour")},
};
static const QMap<AppColor::Role, const char *> APP_ROLE_DESCRIPTIONS = {
{AppColor::AccentStrong, QT_TR_NOOP("Vivid primary accent (e.g. home-tab button gradient start)")},
{AppColor::AccentSoft, QT_TR_NOOP("Lightened, desaturated accent (e.g. home-tab button gradient end)")},
};
PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent)
{
scroll = new QScrollArea(this);
@ -129,46 +122,6 @@ void PaletteGridWidget::buildGrid(QWidget *host)
grid->addWidget(btn, row + 1, col + 1, Qt::AlignHCenter | Qt::AlignVCenter);
}
}
// Application color section: one ColorButton per role below the role grid.
// These are not tied to a color group, so a single button spans the row.
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
const int appHeaderRow = roles.size() + 1;
auto *appHeader = new QLabel(tr("App colors"), host);
appHeader->setToolTip(tr("Application-specific colors layered on top of the Qt palette"));
QFont appHeaderFont = appHeader->font();
appHeaderFont.setBold(true);
appHeader->setFont(appHeaderFont);
appHeader->setAutoFillBackground(true);
appHeader->setContentsMargins(4, 4, 4, 4);
grid->addWidget(appHeader, appHeaderRow, 0, 1, 4);
headerLabels.append(appHeader);
for (int i = 0; i < appEnum.keyCount(); ++i) {
auto role = static_cast<AppColor::Role>(appEnum.value(i));
const int row = appHeaderRow + 1 + i;
if (i % 2 == 0) {
for (int col = 0; col < 4; ++col) {
auto *shade = new QWidget(host);
shade->setAutoFillBackground(true);
grid->addWidget(shade, row, col);
rowShadeWidgets.push_back(shade);
}
}
auto *label = new QLabel(QString(appEnum.valueToKey(role)), host);
label->setToolTip(APP_ROLE_DESCRIPTIONS.value(role, {}));
label->setContentsMargins(4, 2, 8, 2);
grid->addWidget(label, row, 0);
auto *btn = new ColorButton(host);
connect(btn, &ColorButton::colorChanged, this, [this] { emit paletteChanged(); });
appColorButtons[role] = btn;
grid->addWidget(btn, row, 1, Qt::AlignHCenter | Qt::AlignVCenter);
}
}
void PaletteGridWidget::changeEvent(QEvent *e)
@ -213,16 +166,6 @@ void PaletteGridWidget::loadPalette(const PaletteConfig &cfg)
colorButtons[group][role]->setColor(color);
}
}
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
for (int i = 0; i < appEnum.keyCount(); ++i) {
auto role = static_cast<AppColor::Role>(appEnum.value(i));
QColor color = cfg.appColors.value(role);
if (!color.isValid()) {
color = themeManager->appColor(role);
}
appColorButtons[role]->setColor(color);
}
}
PaletteConfig PaletteGridWidget::currentPaletteConfig() const
@ -233,12 +176,5 @@ PaletteConfig PaletteGridWidget::currentPaletteConfig() const
cfg.colors[group][role] = colorButtons[group][role]->getColor();
}
}
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
for (int i = 0; i < appEnum.keyCount(); ++i) {
auto role = static_cast<AppColor::Role>(appEnum.value(i));
cfg.appColors[role] = appColorButtons[role]->getColor();
}
return cfg;
}

View file

@ -31,7 +31,6 @@ private:
void refreshChromePalettes();
QMap<QPalette::ColorGroup, QMap<QPalette::ColorRole, ColorButton *>> colorButtons;
QMap<AppColor::Role, ColorButton *> appColorButtons;
QScrollArea *scroll;
QWidget *gridHost;
QVBoxLayout *layout;

View file

@ -16,6 +16,7 @@
#define DEFAULT_COLOR_MODERATOR_LEFT "#ffffff";
#define DEFAULT_COLOR_MODERATOR_RIGHT "#000000";
#define DEFAULT_COLOR_ADMIN "#ff2701";
#define DEFAULT_COLOR_DEVELOPER "#B8B8B8"
/**
* Clamps an svg render size so that rendering does not exceed a multiple of the requested size.
@ -361,10 +362,6 @@ static QString getIconType(const bool isBuddy, const UserLevelFlags &userLevelFl
return "pawn_judge";
}
if (userLevelFlags.testFlag(ServerInfo_User::IsDeveloper)) {
return "pawn_dev";
}
if (!privLevel.isEmpty() && privLevel.toLower() != "none") {
return QString("pawn_%1").arg(privLevel.toLower());
}
@ -385,6 +382,8 @@ QIcon UserLevelPixmapGenerator::generateIconDefault(int height,
if (userLevel.testFlag(ServerInfo_User::IsAdmin)) {
colorLeft = DEFAULT_COLOR_ADMIN;
} else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) {
colorLeft = DEFAULT_COLOR_DEVELOPER;
} else if (userLevel.testFlag(ServerInfo_User::IsModerator)) {
colorLeft = DEFAULT_COLOR_MODERATOR_LEFT;
colorRight = DEFAULT_COLOR_MODERATOR_RIGHT;

View file

@ -16,7 +16,7 @@ QString ThemeConfig::toIni() const
out += "[Appearance]\n";
out += QString("ColorScheme = %1\n").arg(colorScheme.isEmpty() ? "System" : colorScheme);
out += "\n[Style]\n";
out += QString("Name = %1\n").arg(styleName.isEmpty() ? "System" : styleName);
out += QString("Name = %1\n").arg(styleName.isEmpty() ? "Default" : styleName);
return out;
}
@ -96,7 +96,7 @@ bool ThemeConfig::save(const QString &themeDirPath) const
bool PaletteConfig::hasPalette() const
{
return !colors.isEmpty() || !appColors.isEmpty();
return !colors.isEmpty();
}
QString PaletteConfig::toToml() const
@ -133,24 +133,6 @@ QString PaletteConfig::toToml() const
out += "\n";
}
if (!appColors.isEmpty()) {
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
out += "[AppColors]\n";
for (auto it = appColors.cbegin(); it != appColors.cend(); ++it) {
const char *roleName = appEnum.valueToKey(it.key());
if (!roleName) {
continue;
}
out += QString("%1 = %2\n").arg(QString(roleName), -20).arg(it.value().name(QColor::HexArgb));
}
out += "\n";
}
return out;
}
@ -170,7 +152,6 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath)
}
QMetaEnum roleEnum = QMetaEnum::fromType<QPalette::ColorRole>();
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
QString currentSection;
QPalette::ColorGroup currentGroup = QPalette::Active;
@ -221,26 +202,6 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath)
}
}
QColor color(value);
if (!color.isValid()) {
continue;
}
if (currentSection.compare("AppColors", Qt::CaseInsensitive) == 0) {
if (key.startsWith("AppColor::")) {
key = key.mid(10);
}
int appRoleInt = appEnum.keyToValue(key.toUtf8().constData());
if (appRoleInt >= 0) {
cfg.appColors[static_cast<AppColor::Role>(appRoleInt)] = color;
}
continue;
}
if (!currentSection.startsWith("Palette", Qt::CaseInsensitive)) {
continue;
}
@ -255,7 +216,11 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath)
continue;
}
cfg.colors[currentGroup][static_cast<QPalette::ColorRole>(roleInt)] = color;
QColor color(value);
if (color.isValid()) {
cfg.colors[currentGroup][static_cast<QPalette::ColorRole>(roleInt)] = color;
}
}
return cfg;

View file

@ -3,25 +3,9 @@
#include <QColor>
#include <QMap>
#include <QObject>
#include <QPalette>
#include <QString>
// Application-specific color roles, layered on top of the fixed QPalette role
// set. Stored in the same palette-<scheme>.toml under an [AppColors] section
// and editable from the palette editor, so theme authors can control colors
// beyond what Qt's palette can express.
namespace AppColor
{
Q_NAMESPACE
enum Role
{
AccentStrong,
AccentSoft,
};
Q_ENUM_NS(Role)
} // namespace AppColor
struct ThemeConfig
{
QString colorScheme;
@ -37,16 +21,7 @@ struct ThemeConfig
struct PaletteConfig
{
QMap<QPalette::ColorGroup, QMap<QPalette::ColorRole, QColor>> colors;
QMap<AppColor::Role, QColor> appColors;
bool operator==(const PaletteConfig &rhs) const
{
return colors == rhs.colors && appColors == rhs.appColors;
}
bool operator!=(const PaletteConfig &rhs) const
{
return !(*this == rhs);
}
bool hasPalette() const;
QString toToml() const;

View file

@ -6,7 +6,6 @@
#include <QApplication>
#include <QColor>
#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QLibraryInfo>
#include <QMap>
@ -22,7 +21,7 @@
#include <Qt>
#include <libcockatrice/settings/paths_settings.h>
#define SYSTEM_THEME_NAME "System"
#define NONE_THEME_NAME "Default"
#define FUSION_THEME_NAME "Fusion"
#define STYLE_CSS_NAME "style.css"
#define HANDZONE_BG_NAME "handzone"
@ -96,7 +95,7 @@ struct PaletteColorInfo
static QString usableDefaultStyle(const QString &style)
{
// The Windows 11 native style is broken: when the OS default
// ("System" theme selection) would use it, fall back to the Vista style.
// ("Default" theme selection) would use it, fall back to the Vista style.
// Explicitly choosing "windows11" in a theme is still honored.
return style.compare("windows11", Qt::CaseInsensitive) == 0 ? QStringLiteral("windowsvista") : style;
}
@ -119,16 +118,10 @@ ThemeManager::ThemeManager(QObject *parent) : QObject(parent)
void ThemeManager::ensureThemeDirectoryExists()
{
auto &settings = SettingsCache::instance();
// Migrate the old "Default" theme name to "System"
if (settings.getThemeName() == "Default") {
settings.setThemeName(SYSTEM_THEME_NAME);
}
if (settings.getThemeName().isEmpty() || !getAvailableThemes().contains(settings.getThemeName())) {
if (SettingsCache::instance().getThemeName().isEmpty() ||
!getAvailableThemes().contains(SettingsCache::instance().getThemeName())) {
qCInfo(ThemeManagerLog) << "Theme name not set, setting default value";
settings.setThemeName(FUSION_THEME_NAME);
SettingsCache::instance().setThemeName(NONE_THEME_NAME);
}
}
@ -191,32 +184,11 @@ QString ThemeManager::assetPath(QStringView prefix) const
return resolvedPlain.isEmpty() ? prefix.toString() : resolvedPlain;
}
// Probe whether a directory is truly writable by trying to create and remove a
// temporary file. QFileInfo::isWritable() on a directory is unreliable (notably
// on Windows where UAC VirtualStore can make a system dir appear writable).
bool ThemeManager::isDirReallyWritable(const QString &dirPath)
bool ThemeManager::isBuiltInTheme()
{
const QString probe = QDir(dirPath).absoluteFilePath(".cockatrice_write_test");
QFile f(probe);
if (!f.open(QIODevice::WriteOnly)) {
return false;
}
f.close();
f.remove();
return true;
}
const auto themeName = SettingsCache::instance().getThemeName();
QString ThemeManager::writableThemeDir(const QString &themeName)
{
// All theme writes go to the user themes directory regardless of whether
// the resolved (system) theme directory happens to be writable. Even when a
// write would succeed in-place, routing it to the user directory keeps the
// install intact and guarantees changes survive upgrades.
const QString dirPath = QDir(SettingsCache::instance().paths().getThemesPath()).absoluteFilePath(themeName);
if (!QDir().mkpath(dirPath)) {
qWarning() << "Failed to create theme save directory:" << dirPath;
}
return dirPath;
return themeName == NONE_THEME_NAME || themeName == FUSION_THEME_NAME;
}
// System (read-only) themes location, relative to the application binary.
@ -241,7 +213,9 @@ QStringMap &ThemeManager::getAvailableThemes()
// load themes from user profile dir
dir.setPath(SettingsCache::instance().paths().getThemesPath());
availableThemes.insert(SYSTEM_THEME_NAME, dir.absoluteFilePath("System"));
// add default value
availableThemes.insert(NONE_THEME_NAME, dir.absoluteFilePath("Default"));
availableThemes.insert(FUSION_THEME_NAME, dir.absoluteFilePath("Fusion"));
for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) {
@ -357,7 +331,7 @@ bool ThemeManager::commitPalette(const QString &themeDirPath, const QString &col
void ThemeManager::setColorScheme(const QString &scheme)
{
const QString dirPath = writableThemeDir(SettingsCache::instance().getThemeName());
const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName());
ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath);
cfg.colorScheme = scheme;
@ -368,7 +342,7 @@ void ThemeManager::setColorScheme(const QString &scheme)
void ThemeManager::setStyleName(const QString &styleName)
{
const QString dirPath = writableThemeDir(SettingsCache::instance().getThemeName());
const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName());
ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath);
cfg.styleName = styleName;
@ -399,7 +373,7 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
Q_UNUSED(activeScheme)
#endif
QString styleName = themeCfg.styleName;
if (styleName.isEmpty() || styleName.compare("System", Qt::CaseInsensitive) == 0) {
if (styleName.isEmpty() || styleName.compare("Default", Qt::CaseInsensitive) == 0) {
if (themeName == FUSION_THEME_NAME) {
styleName = "Fusion";
} else {
@ -442,8 +416,6 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
qApp->setPalette(base);
qApp->setStyle(style);
currentAppColors = palCfg.appColors;
// Force every widget to re-polish and repaint immediately rather than
// waiting for natural expose events, which produces a patchwork of old
// and new colours during a live preview.
@ -456,35 +428,6 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
style->polish(widget);
widget->update();
}
emit paletteChanged();
}
QColor ThemeManager::appColor(AppColor::Role role) const
{
const auto it = currentAppColors.constFind(role);
if (it != currentAppColors.constEnd()) {
return it.value();
}
// QPalette::Accent was introduced in Qt 6.6 and several shipped palettes
// set it to a value barely distinguishable from Window, so it is not a
// reliable accent source. The selection highlight is the stable accent
// (Accent defaults to Highlight when unset), and deriving from it
// unconditionally keeps every Qt version rendering identically.
const QColor accent = qApp->palette().color(QPalette::Active, QPalette::Highlight);
if (role == AppColor::AccentSoft) {
constexpr int SOFT_SATURATION_PERCENT = 70;
constexpr int SOFT_LIGHTNESS_OFFSET = 60;
// Light end of the gradient: same hue, softened and lightened
return QColor::fromHsl(qMax(0, accent.hslHue()),
qBound(0, qRound(accent.hslSaturation() * SOFT_SATURATION_PERCENT / 100.0), 255),
qBound(0, accent.lightness() + SOFT_LIGHTNESS_OFFSET, 255));
}
return accent;
}
void ThemeManager::themeChangedSlot()
@ -521,19 +464,8 @@ void ThemeManager::themeChangedSlot()
// ── Load palette: custom first, then theme default ────────────────────
PaletteConfig palette = PaletteConfig::fromScheme(dirPath, activeScheme);
const PaletteConfig themeDefault = ThemeManager::loadDefaultPaletteConfig(dirPath, themeName, activeScheme);
if (palette.hasPalette()) {
// A custom palette written before [AppColors] existed carries no app
// colors; merge the theme's shipped defaults so the identity colors
// survive (hasPalette() counts an app-colors-only file as a palette,
// so those are kept wholesale and never reach here empty).
for (auto it = themeDefault.appColors.cbegin(); it != themeDefault.appColors.cend(); ++it) {
if (!palette.appColors.contains(it.key())) {
palette.appColors.insert(it.key(), it.value());
}
}
} else {
palette = themeDefault;
if (!palette.hasPalette()) {
palette = ThemeManager::loadDefaultPaletteConfig(dirPath, themeName, activeScheme);
}
applyStyleAndPalette(themeName, themeCfg, palette, activeScheme);

View file

@ -50,7 +50,6 @@ private:
QString currentThemePath;
std::array<QBrush, Role::MaxRole + 1> brushes;
QStringMap availableThemes;
QMap<AppColor::Role, QColor> currentAppColors;
/*
Internal cache for multiple backgrounds
*/
@ -66,16 +65,7 @@ protected:
const QString &activeScheme);
public:
// Resolves the directory to write theme changes to for the given theme
// name. The resolved theme dir (user or system) is used when writable;
// read-only system themes fall back to the user themes directory, creating
// it if needed, so customisations never get lost on upgrade.
static QString writableThemeDir(const QString &themeName);
// Probe whether a directory is truly writable by trying to create and remove
// a temporary file. QFileInfo::isWritable() on a directory is unreliable
// (notably on Windows where UAC VirtualStore can make a system dir appear
// writable).
static bool isDirReallyWritable(const QString &dirPath);
bool isBuiltInTheme();
// Explicit color scheme of the theme: theme.cfg's ColorScheme setting
// (Dark/Light), falling back to the OS color scheme when it is "System".
bool isDarkMode(const QString &themeDirPath) const;
@ -125,17 +115,12 @@ public:
void reloadCurrentTheme();
void previewPalette(const PaletteConfig &cfg, const QString &scheme);
// Resolves an application color role: the theme's stored [AppColors] value
// when present, otherwise a palette-accent-derived fallback.
QColor appColor(AppColor::Role role) const;
QBrush &getBgBrush(Role zone);
QBrush getExtraBgBrush(Role zone, int zoneId = 0);
protected slots:
void themeChangedSlot();
signals:
void themeChanged();
void paletteChanged();
};
extern ThemeManager *themeManager;

View file

@ -85,7 +85,7 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
}
lastWidth = totalWidth;
const int totalHeight = qMax(0, totalWidth / 6); // Set height to 1/4 of the width
const int totalHeight = totalWidth / 6; // Set height to 1/4 of the width
setFixedHeight(totalHeight);
const int count = layout->count();
@ -97,10 +97,6 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
const int availableWidth = totalWidth - (spacing * (count - 1));
const int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height
if (iconSize <= 0) {
lastIconSize = iconSize;
return;
}
if (iconSize == lastIconSize) {
return;
}

View file

@ -1,37 +0,0 @@
#include "deck_color_identity.h"
#include <QSet>
#include <libcockatrice/card/database/card_database_querier.h>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
QString getDeckColorIdentity(const DeckList &deck, const CardDatabaseQuerier *db)
{
const QStringList cardList = deck.getCardList({DECK_ZONE_MAIN, DECK_ZONE_SIDE});
if (cardList.isEmpty()) {
return {};
}
QSet<QChar> colorSet; // A set to collect unique color symbols (e.g., W, U, B, R, G)
for (const QString &cardName : cardList) {
CardInfoPtr currentCard = db->getCardInfo(cardName);
if (currentCard) {
const QString colors = currentCard->getColors(); // returns something like "WUB"
for (const QChar &color : colors) {
colorSet.insert(color);
}
}
}
// Ensure the color identity is in WUBRG order
QString colorIdentity;
const QString wubrgOrder = "WUBRG";
for (const QChar &color : wubrgOrder) {
if (colorSet.contains(color)) {
colorIdentity.append(color);
}
}
return colorIdentity;
}

View file

@ -1,20 +0,0 @@
#ifndef COCKATRICE_DECK_COLOR_IDENTITY_H
#define COCKATRICE_DECK_COLOR_IDENTITY_H
#include <QString>
class CardDatabaseQuerier;
class DeckList;
/**
* @brief Computes the color identity of a deck (e.g. "WUBRG") from the color
* symbols of all cards in the main deck and sideboard, ordered WUBRG.
*
* Shared as a free function so the deck storage previews and the deck share
* dialog compute identities identically.
*
* @param db Card database used to look up card color symbols.
*/
QString getDeckColorIdentity(const DeckList &deck, const CardDatabaseQuerier *db);
#endif // COCKATRICE_DECK_COLOR_IDENTITY_H

View file

@ -74,8 +74,6 @@ CardInfoPictureWidget::CardInfoPictureWidget(QWidget *parent, const bool _hoverT
update();
});
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
&CardInfoPictureWidget::updatePixmap);
}
/**

View file

@ -133,14 +133,12 @@ void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event)
path.addRoundedRect(glowRect, radius, radius);
// Soft outer glow
QColor glowColor = palette().color(QPalette::Highlight);
glowColor.setAlpha(80);
QColor glowColor(0, 150, 255, 80); // subtle blu
painter.setPen(QPen(glowColor, 6));
painter.drawPath(path);
// Thin inner border for crispness
QColor borderColor = palette().color(QPalette::Highlight);
borderColor.setAlpha(200);
QColor borderColor(0, 150, 255, 200);
painter.setPen(QPen(borderColor, 2));
painter.drawRoundedRect(pixmapRect, radius, radius);

View file

@ -1,7 +1,6 @@
#include "card_info_text_widget.h"
#include "../../../game_graphics/board/card_item.h"
#include "../../card_localization.h"
#include <QGridLayout>
#include <QLabel>
@ -11,7 +10,7 @@
#include <libcockatrice/card/game_specific_terms.h>
#include <libcockatrice/card/relation/card_relation.h>
CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent)
CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent), info(nullptr)
{
propsLabel = new QLabel;
propsLabel->setOpenExternalLinks(false);
@ -40,12 +39,6 @@ CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent)
grid->setRowStretch(1, 1);
retranslateUi();
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this, [this] {
if (currentCard) {
setCard(currentCard);
}
});
}
void CardInfoTextWidget::setTexts(const QString &propsText, const QString &textText)
@ -67,7 +60,7 @@ void CardInfoTextWidget::setCard(const ExactCard &exactCard)
QString text = "<table width=\"100%\" border=0 cellspacing=0 cellpadding=0>";
text += QString("<tr><td>%1</td><td width=\"5\"></td><td>%2</td></tr>")
.arg(tr("Name:"), CardLocalization::displayName(card).toHtmlEscaped());
.arg(tr("Name:"), card->getName().toHtmlEscaped());
if (!exactCard.getPrinting().isEmpty()) {
QString setShort = exactCard.getPrinting().getSet()->getShortName().toHtmlEscaped();
@ -101,8 +94,7 @@ void CardInfoTextWidget::setCard(const ExactCard &exactCard)
}
text += "</table>";
setTexts(text, CardLocalization::displayText(card));
currentCard = exactCard;
setTexts(text, card->getText());
}
void CardInfoTextWidget::setInvalidCardName(const QString &cardName)

View file

@ -23,7 +23,7 @@ private:
QLabel *propsLabel;
QScrollArea *propsScroll;
QTextEdit *textLabel;
ExactCard currentCard; ///< Last card set, re-rendered when the card language changes.
CardInfoPtr info;
void setTexts(const QString &propsText, const QString &textText);
public:

View file

@ -27,23 +27,18 @@ DeckPreviewCardPictureWidget::DeckPreviewCardPictureWidget(QWidget *parent,
const QColor &textColor,
const QColor &outlineColor,
const int fontSize,
const Qt::Alignment alignment,
const bool _emitClickImmediately)
const Qt::Alignment alignment)
: CardInfoPictureWithTextOverlayWidget(parent,
hoverToZoomEnabled,
raiseOnEnter,
textColor,
outlineColor,
fontSize,
alignment),
emitClickImmediately(_emitClickImmediately)
alignment)
{
singleClickTimer = new QTimer(this);
singleClickTimer->setSingleShot(true);
connect(singleClickTimer, &QTimer::timeout, this, [this]() {
emit imageClicked(lastMouseEvent, this);
emit imageSingleClicked();
});
connect(singleClickTimer, &QTimer::timeout, this, [this]() { emit imageClicked(lastMouseEvent, this); });
connect(&SettingsCache::instance().visualDeckStorage(),
&VisualDeckStorageSettings::visualDeckStorageSelectionAnimationChanged, this,
&CardInfoPictureWidget::setRaiseOnEnterEnabled);
@ -52,13 +47,8 @@ DeckPreviewCardPictureWidget::DeckPreviewCardPictureWidget(QWidget *parent,
void DeckPreviewCardPictureWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
if (emitClickImmediately) {
emit imageClicked(event, this);
emit imageSingleClicked();
} else {
lastMouseEvent = event;
singleClickTimer->start(QApplication::doubleClickInterval());
}
lastMouseEvent = event;
singleClickTimer->start(QApplication::doubleClickInterval());
} else {
emit imageClicked(event, this);
event->accept();
@ -68,14 +58,7 @@ void DeckPreviewCardPictureWidget::mousePressEvent(QMouseEvent *event)
void DeckPreviewCardPictureWidget::mouseDoubleClickEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
if (emitClickImmediately) {
// Do not report a second single click for the second press of the
// double-click; the consumer maps the double-click to select+open.
lastMouseEvent = event;
emit imageDoubleClicked(event, this);
} else {
singleClickTimer->stop(); // Prevent single-click logic
emit imageDoubleClicked(lastMouseEvent, this);
}
singleClickTimer->stop(); // Prevent single-click logic
emit imageDoubleClicked(lastMouseEvent, this);
}
}

View file

@ -20,38 +20,21 @@ class DeckPreviewCardPictureWidget final : public CardInfoPictureWithTextOverlay
Q_OBJECT
public:
/**
* @brief Constructs a DeckPreviewCardPictureWidget.
* @param parent The parent widget.
* @param hoverToZoomEnabled If this widget will spawn a larger widget when hovered over.
* @param raiseOnEnter If the widget raises its border when the mouse enters.
* @param textColor The color of the overlay text.
* @param outlineColor The color of the outline around the text.
* @param fontSize The font size of the overlay text.
* @param alignment The alignment of the text within the overlay.
* @param emitClickImmediately If true, a left click is reported immediately on click
* instead of after the double-click interval. Use this for selection surfaces
* where reacting to a double-click (select-and-open) would needlessly delay the
* single-click feedback. The double-click signal is still emitted.
*/
explicit DeckPreviewCardPictureWidget(QWidget *parent,
bool hoverToZoomEnabled = false,
bool raiseOnEnter = false,
const QColor &textColor = Qt::white,
const QColor &outlineColor = Qt::black,
int fontSize = 12,
Qt::Alignment alignment = Qt::AlignCenter,
bool _emitClickImmediately = false);
Qt::Alignment alignment = Qt::AlignCenter);
signals:
void imageClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
void imageSingleClicked();
void imageDoubleClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
private:
QTimer *singleClickTimer;
QMouseEvent *lastMouseEvent = nullptr; // Store the last mouse event
bool emitClickImmediately;
protected:
void mousePressEvent(QMouseEvent *event) override;

View file

@ -345,9 +345,7 @@ ExactCard DeckEditorDeckDockWidget::getCurrentCard()
if (!current.isValid()) {
return {};
}
// The display role holds the localized card name; the edit role always carries the
// canonical English name needed to look the card up in the database.
const QString cardName = current.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
const QString cardName = current.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
const QString cardProviderID = current.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString();
const QModelIndex gparent = current.parent().parent();
@ -520,11 +518,8 @@ void DeckEditorDeckDockWidget::syncBannerCardComboBoxSelectionWithDeck()
void DeckEditorDeckDockWidget::setSelectedIndex(const QModelIndex &newCardIndex, bool preserveWidgetFocus)
{
const QModelIndex proxyIndex = proxy->mapFromSource(newCardIndex);
deckView->clearSelection();
deckView->setCurrentIndex(proxyIndex);
deckView->scrollTo(proxyIndex);
deckView->setCurrentIndex(newCardIndex);
recursiveExpand(newCardIndex);
if (!preserveWidgetFocus) {

View file

@ -1,20 +1,13 @@
#include "deck_state_manager.h"
#include "../../../client/settings/cache_settings.h"
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list_history_manager.h>
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
#include <libcockatrice/settings/cards_display_settings.h>
DeckStateManager::DeckStateManager(QObject *parent)
: QObject(parent), deckList(QSharedPointer<DeckList>(new DeckList)),
deckListModel(new DeckListModel(this, deckList)), historyManager(new DeckListHistoryManager(this))
{
deckListModel->setDisplayLanguage(SettingsCache::instance().cardsDisplay().getCardLang());
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, deckListModel,
[this](const QString &lang) { deckListModel->setDisplayLanguage(lang); });
connect(historyManager, &DeckListHistoryManager::undoRedoStateChanged, this, [this] {
setModified(true);
emit historyChanged();
@ -267,10 +260,7 @@ bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx)
return false;
}
// The display role holds the localized card name; the edit role always carries the
// canonical English name needed to look the card up in the database.
QString displayCardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
QString providerId = idx.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString();
QModelIndex gparent = idx.parent().parent();
@ -287,7 +277,7 @@ bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx)
QString reason = tr("Moved to %1 1 × \"%2\" (%3)") //
.arg(otherZoneName)
.arg(displayCardName)
.arg(cardName)
.arg(providerId);
return modifyDeck(reason, [&idx, &cardName, &providerId, &otherZoneName](auto model) {
@ -301,8 +291,9 @@ bool DeckStateManager::removeCardAtIndex(const QModelIndex &idx)
return false;
}
QString reason =
tr("Removed \"%1\" (all copies)").arg(idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString());
QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString();
QString reason = tr("Removed \"%1\" (all copies)").arg(cardName);
return modifyDeck(reason, [&idx](auto model) { return model->removeRow(idx.row(), idx.parent()); });
}

View file

@ -1,59 +0,0 @@
#include "deck_share_utils.h"
#include <QClipboard>
#include <QGuiApplication>
#include <QLocale>
#include <QTimeZone>
#include <QUrl>
#include <QUrlQuery>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pb/response_deck_share_create.pb.h>
namespace DeckShareUtils
{
QString buildShareLink(const AbstractClient *client, const QString &token)
{
QUrl url;
url.setScheme(QStringLiteral("cockatrice"));
url.setHost(QStringLiteral("opendeck"));
QUrlQuery query;
query.addQueryItem(QStringLiteral("share"), token);
query.addQueryItem(QStringLiteral("hostname"), client->serverName());
query.addQueryItem(QStringLiteral("port"), QString::number(client->serverPort()));
url.setQuery(query);
return url.toString(QUrl::FullyEncoded);
}
QString copyShareLinkToClipboard(const QString &link)
{
QGuiApplication::clipboard()->setText(link);
return link;
}
QString formatShareExpiry(const QDateTime &expiry)
{
return QLocale().toString(expiry.toLocalTime(), QLocale::ShortFormat);
}
ShareResponse handleShareResponse(const AbstractClient *client, const Response &response)
{
const Response_DeckShareCreate &resp = response.GetExtension(Response_DeckShareCreate::ext);
const QString token = QString::fromStdString(resp.token());
const QString link = buildShareLink(client, token);
copyShareLinkToClipboard(link);
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
const QDateTime expiry = QDateTime::fromSecsSinceEpoch(resp.expires_at(), QTimeZone::UTC);
#else
const QDateTime expiry = QDateTime::fromSecsSinceEpoch(resp.expires_at(), Qt::UTC);
#endif
return {link, expiry};
}
} // namespace DeckShareUtils

View file

@ -1,59 +0,0 @@
/**
* @file deck_share_utils.h
* @ingroup DeckShareWidgets
*/
//! \todo Document this file.
#ifndef DECK_SHARE_UTILS_H
#define DECK_SHARE_UTILS_H
#include <QDateTime>
#include <QString>
class AbstractClient;
class Response;
/**
* @brief Shared helpers for creating temporary deck shares.
*/
namespace DeckShareUtils
{
/**
* @brief The outcome of a successful share-create response.
*/
struct ShareResponse
{
QString link; ///< The share link that was copied to the clipboard.
QDateTime expiry; ///< When the share expires (UTC).
};
/**
* @brief Builds the cockatrice:// link for a freshly created deck share.
* @param client Used to embed the target server's hostname and port.
* @param token The share token from Response_DeckShareCreate.
*/
QString buildShareLink(const AbstractClient *client, const QString &token);
/**
* @brief Copies the share link to the clipboard.
* @return The link that was copied.
*/
QString copyShareLinkToClipboard(const QString &link);
/**
* @brief Formats the expiration timestamp for a share.
*/
QString formatShareExpiry(const QDateTime &expiry);
/**
* @brief Handles a successful Response_DeckShareCreate: builds the share link,
* copies it to the clipboard, and derives the share expiry.
* @param client Used to embed the target server's hostname and port.
* @param response The successful response carrying the share token and expiry.
*/
ShareResponse handleShareResponse(const AbstractClient *client, const Response &response);
} // namespace DeckShareUtils
#endif // DECK_SHARE_UTILS_H

View file

@ -1,77 +0,0 @@
#include "share_bar_widget.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
ShareBarWidget::ShareBarWidget(QWidget *parent) : QWidget(parent)
{
auto *layout = new QHBoxLayout(this);
layout->setContentsMargins(12, 10, 12, 10);
layout->setSpacing(8);
hintLabel = new QLabel(this);
hintLabel->setWordWrap(true);
nameEdit = new QLineEdit(this);
nameEdit->setMaximumWidth(260);
countLabel = new QLabel(this);
cancelButton = new QPushButton(this);
connect(cancelButton, &QPushButton::clicked, this, &ShareBarWidget::cancelRequested);
createButton = new QPushButton(this);
createButton->setDefault(true);
connect(createButton, &QPushButton::clicked, this, &ShareBarWidget::createRequested);
layout->addWidget(hintLabel, 1);
layout->addWidget(nameEdit);
layout->addWidget(countLabel);
layout->addStretch();
layout->addWidget(cancelButton);
layout->addWidget(createButton);
setLayout(layout);
retranslateUi();
}
void ShareBarWidget::retranslateUi()
{
nameEdit->setPlaceholderText(tr("Share name"));
cancelButton->setText(tr("Cancel"));
createButton->setText(tr("Create share link"));
}
QString ShareBarWidget::name() const
{
return nameEdit->text().trimmed();
}
void ShareBarWidget::setName(const QString &value)
{
nameEdit->setText(value);
}
void ShareBarWidget::setCountText(const QString &text)
{
countLabel->setText(text);
}
void ShareBarWidget::setHintText(const QString &text, bool visible)
{
hintLabel->setText(text);
hintLabel->setVisible(visible);
}
void ShareBarWidget::setCreateEnabled(bool enabled)
{
createButton->setEnabled(enabled);
}
void ShareBarWidget::focusName()
{
nameEdit->setFocus();
}

View file

@ -1,63 +0,0 @@
/**
* @file share_bar_widget.h
* @ingroup DeckShareWidgets
*/
//! \todo Document this file.
#ifndef SHARE_BAR_WIDGET_H
#define SHARE_BAR_WIDGET_H
#include <QWidget>
class QLabel;
class QLineEdit;
class QPushButton;
/**
* @brief The activated toolbar used to create a temporary deck share.
*
* A single reusable component shared by the local visual deck storage and the
* remote server deck storage tabs, so the share workflow renders identically in
* both places. It owns its own widgets, strings, and layout; the owning tab only
* sets the count/hint text and reacts to the create/cancel signals.
*/
class ShareBarWidget final : public QWidget
{
Q_OBJECT
public:
explicit ShareBarWidget(QWidget *parent = nullptr);
void retranslateUi();
/** @return The trimmed name entered by the user. */
[[nodiscard]] QString name() const;
/** @brief Resets the name field to the given default. */
void setName(const QString &name);
/** @brief Sets the selected-count summary label text. */
void setCountText(const QString &text);
/** @brief Sets the explainer hint text, showing it when @p visible is true. */
void setHintText(const QString &text, bool visible);
/** @brief Enables or disables the create-share-link button (guards double submission). */
void setCreateEnabled(bool enabled);
/** @brief Moves keyboard focus to the name field. */
void focusName();
signals:
void createRequested();
void cancelRequested();
private:
QLabel *hintLabel;
QLineEdit *nameEdit;
QLabel *countLabel;
QPushButton *cancelButton;
QPushButton *createButton;
};
#endif // SHARE_BAR_WIDGET_H

View file

@ -1,140 +0,0 @@
#include "shared_deck_preview_widget.h"
#include "../cards/additional_info/color_identity_widget.h"
#include "../cards/deck_preview_card_picture_widget.h"
#include <QCheckBox>
#include <QFrame>
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QLabel>
#include <QVBoxLayout>
#include <libcockatrice/card/database/card_database_querier.h>
SharedDeckPreviewWidget::SharedDeckPreviewWidget(QWidget *parent,
const CardDatabaseQuerier *querier,
const QString &deckName,
const QString &bannerCardName,
const QString &colorIdentity,
const QString &gameFormat,
const QString &deckToolTip)
: QWidget(parent)
{
bannerCardDisplayWidget =
new DeckPreviewCardPictureWidget(this, false, false, Qt::white, Qt::black, 12, Qt::AlignCenter, true);
bannerCardDisplayWidget->setScaleFactor(100);
const ExactCard bannerCard = bannerCardName.isEmpty() ? ExactCard() : querier->getCard(CardRef{bannerCardName, {}});
bannerCardDisplayWidget->setCard(bannerCard);
bannerCardDisplayWidget->setOverlayText(deckName);
setToolTip(deckToolTip.isEmpty() ? deckName : deckToolTip);
setFocusPolicy(Qt::StrongFocus);
setBaseAccessibleName(deckName);
colorIdentityWidget = new ColorIdentityWidget(this, colorIdentity);
colorIdentityWidget->setVisible(!colorIdentity.isEmpty());
// gameFormat is server-supplied and the QLabel renders AutoText, so escape it.
gameFormatLabel = new QLabel(gameFormat.toHtmlEscaped(), this);
gameFormatLabel->setAlignment(Qt::AlignCenter);
gameFormatLabel->setVisible(!gameFormat.isEmpty());
selectionCheckBox = new QCheckBox(this);
selectionCheckBox->setToolTip(tr("Select this deck"));
// The tile itself is focusable (Space/Enter toggles); keep the checkbox
// from creating a second tab stop per tile.
selectionCheckBox->setFocusPolicy(Qt::NoFocus);
// Selection frame reused from the deck-preview selection covenant: a
// palette(highlight) border around the banner card, shown while selected.
selectionFrame = new QFrame(bannerCardDisplayWidget);
selectionFrame->setAttribute(Qt::WA_TransparentForMouseEvents);
selectionFrame->setStyleSheet(QStringLiteral(
"QFrame { border: 2px solid palette(highlight); border-radius: 4px; background: transparent; }"));
selectionFrame->setVisible(false);
auto *selectionRow = new QHBoxLayout;
selectionRow->addWidget(selectionCheckBox);
selectionRow->addStretch(1);
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->addLayout(selectionRow);
layout->addWidget(bannerCardDisplayWidget, 0, Qt::AlignHCenter);
layout->addWidget(colorIdentityWidget, 0, Qt::AlignHCenter);
layout->addWidget(gameFormatLabel, 0, Qt::AlignHCenter);
setLayout(layout);
connect(selectionCheckBox, &QCheckBox::toggled, this, [this](bool checked) {
updateSelectionVisual(checked);
emit selectionToggled(checked);
});
connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageClicked, this,
&SharedDeckPreviewWidget::toggleSelection);
connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this,
&SharedDeckPreviewWidget::activate);
}
bool SharedDeckPreviewWidget::isSelected() const
{
return selectionCheckBox->isChecked();
}
void SharedDeckPreviewWidget::setSelected(bool selected)
{
if (isSelected() == selected) {
return;
}
selectionCheckBox->setChecked(selected);
}
void SharedDeckPreviewWidget::updateSelectionVisual(bool selected)
{
selectionFrame->setVisible(selected);
selectionFrame->raise();
if (selected) {
setAccessibleName(baseAccessibleName + tr(" (selected)"));
} else {
setAccessibleName(baseAccessibleName);
}
}
void SharedDeckPreviewWidget::setBaseAccessibleName(const QString &name)
{
baseAccessibleName = name;
setAccessibleName(name);
}
void SharedDeckPreviewWidget::toggleSelection()
{
setSelected(!isSelected());
}
void SharedDeckPreviewWidget::activate()
{
setSelected(true);
emit activated();
}
void SharedDeckPreviewWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
updateSelectionFrameGeometry();
}
void SharedDeckPreviewWidget::updateSelectionFrameGeometry()
{
if (selectionFrame == nullptr || bannerCardDisplayWidget == nullptr) {
return;
}
selectionFrame->setGeometry(bannerCardDisplayWidget->rect().adjusted(1, 1, -1, -1));
}
void SharedDeckPreviewWidget::keyPressEvent(QKeyEvent *event)
{
if (event->key() == Qt::Key_Space || event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
toggleSelection();
event->accept();
return;
}
QWidget::keyPressEvent(event);
}

View file

@ -1,77 +0,0 @@
/**
* @file shared_deck_preview_widget.h
* @ingroup DeckShareWidgets
*/
//! \todo Document this file.
#ifndef SHARED_DECK_PREVIEW_WIDGET_H
#define SHARED_DECK_PREVIEW_WIDGET_H
#include <QWidget>
class ColorIdentityWidget;
class DeckPreviewCardPictureWidget;
class QCheckBox;
class QFrame;
class QKeyEvent;
class QLabel;
class QResizeEvent;
class CardDatabaseQuerier;
/**
* @brief A selectable preview tile for a deck that has no local file.
*
* Renders a banner card picture (looked up by name in the card database), the
* deck name, color identity and game format. Used to preview decks shared via a
* cockatrice:// link (metadata from Command_DeckShareList) and the deck
* currently open in the deck editor.
*
* Selection follows the deck-preview covenant: the tile reports its click
* immediately (no double-click interval delay), a palette(highlight) frame
* marks the selected tile, and Space/Enter toggles selection from the keyboard.
* A double click selects the tile and emits activated() so the caller can open
* just that deck.
*/
class SharedDeckPreviewWidget : public QWidget
{
Q_OBJECT
public:
explicit SharedDeckPreviewWidget(QWidget *parent,
const CardDatabaseQuerier *querier,
const QString &deckName,
const QString &bannerCardName,
const QString &colorIdentity,
const QString &gameFormat = QString(),
const QString &deckToolTip = QString());
[[nodiscard]] bool isSelected() const;
void setSelected(bool selected);
void setBaseAccessibleName(const QString &name);
signals:
void selectionToggled(bool selected);
void activated();
protected:
void resizeEvent(QResizeEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
private slots:
void toggleSelection();
void activate();
private:
void updateSelectionVisual(bool selected);
void updateSelectionFrameGeometry();
DeckPreviewCardPictureWidget *bannerCardDisplayWidget;
ColorIdentityWidget *colorIdentityWidget;
QLabel *gameFormatLabel;
QCheckBox *selectionCheckBox;
QFrame *selectionFrame;
QString baseAccessibleName;
};
#endif // SHARED_DECK_PREVIEW_WIDGET_H

View file

@ -1,50 +0,0 @@
#include "dlg_login_prompt.h"
#include <QCheckBox>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QLabel>
#include <QLineEdit>
#include <QVBoxLayout>
DlgLoginPrompt::DlgLoginPrompt(const QString &serverText, QWidget *parent) : QDialog(parent)
{
setWindowTitle(tr("Sign in"));
auto *mainLayout = new QVBoxLayout(this);
mainLayout->addWidget(
new QLabel(tr("This link requires you to be signed in.\nSign in to %1:").arg(serverText), this));
auto *formLayout = new QFormLayout;
usernameEdit = new QLineEdit(this);
passwordEdit = new QLineEdit(this);
passwordEdit->setEchoMode(QLineEdit::Password);
formLayout->addRow(tr("Username:"), usernameEdit);
formLayout->addRow(tr("Password:"), passwordEdit);
mainLayout->addLayout(formLayout);
savePasswordCheckBox = new QCheckBox(tr("Save password for this server"), this);
mainLayout->addWidget(savePasswordCheckBox);
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
mainLayout->addWidget(buttonBox);
usernameEdit->setFocus();
}
QString DlgLoginPrompt::username() const
{
return usernameEdit->text().trimmed();
}
QString DlgLoginPrompt::password() const
{
return passwordEdit->text();
}
bool DlgLoginPrompt::savePassword() const
{
return savePasswordCheckBox->isChecked();
}

View file

@ -1,40 +0,0 @@
/**
* @file dlg_login_prompt.h
* @ingroup ConnectionDialogs
*/
//! \todo Document this file.
#ifndef DLG_LOGIN_PROMPT_H
#define DLG_LOGIN_PROMPT_H
#include <QDialog>
class QCheckBox;
class QLineEdit;
/**
* @brief Small sign-in dialog used when a cockatrice:// link needs credentials
* that are not saved for the target server.
*
* The entered name and password are handed to the intent chain; when the user
* opts to save them, they are stored in the server settings so that later links
* to the same server connect seamlessly.
*/
class DlgLoginPrompt : public QDialog
{
Q_OBJECT
public:
explicit DlgLoginPrompt(const QString &serverText, QWidget *parent = nullptr);
[[nodiscard]] QString username() const;
[[nodiscard]] QString password() const;
[[nodiscard]] bool savePassword() const;
private:
QLineEdit *usernameEdit;
QLineEdit *passwordEdit;
QCheckBox *savePasswordCheckBox;
};
#endif // DLG_LOGIN_PROMPT_H

View file

@ -416,11 +416,6 @@ void DlgSettings::setTab(int index)
}
}
AbstractSettingsPage *DlgSettings::page(SettingsPage which) const
{
return pages.value(static_cast<int>(which));
}
void DlgSettings::updateLanguage()
{
qApp->removeTranslator(translator); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)

View file

@ -54,7 +54,6 @@ public:
explicit DlgSettings(QWidget *parent = nullptr);
void setTab(int index);
AbstractSettingsPage *page(SettingsPage which) const;
private slots:
void onTabClicked(int index);

View file

@ -1,96 +0,0 @@
#include "dlg_share_deck.h"
#include "../../../client/settings/cache_settings.h"
#include "../cards/additional_info/deck_color_identity.h"
#include "../deck_share/deck_share_utils.h"
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QTimer>
#include <QVBoxLayout>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/command_deck_share_create.pb.h>
#include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pb/response_deck_share_create.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/settings/network_settings.h>
DlgShareDeck::DlgShareDeck(AbstractClient *_client, const QSharedPointer<DeckList> &_deck, QWidget *_parent)
: QDialog(_parent), client(_client), deck(_deck), shareTimeoutTimer(new QTimer(this))
{
setWindowTitle(tr("Share deck"));
auto *layout = new QVBoxLayout(this);
nameEdit = new QLineEdit(this);
nameEdit->setText(tr("Shared deck"));
auto *form = new QFormLayout;
form->addRow(tr("Share name:"), nameEdit);
layout->addLayout(form);
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Create share link"));
buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgShareDeck::actShare);
connect(buttonBox, &QDialogButtonBox::rejected, this, &DlgShareDeck::reject);
this->buttonBox = buttonBox;
layout->addWidget(buttonBox);
shareTimeoutTimer->setSingleShot(true);
shareTimeoutTimer->setInterval(
static_cast<int>((static_cast<qint64>(SettingsCache::instance().network().getTimeOut()) + 1) *
SettingsCache::instance().network().getKeepAlive() * 1000));
connect(shareTimeoutTimer, &QTimer::timeout, this, &DlgShareDeck::onShareTimeout);
}
void DlgShareDeck::actShare()
{
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
Command_DeckShareCreate cmd;
cmd.set_name(nameEdit->text().trimmed().toStdString());
if (cmd.name().empty()) {
cmd.set_name(tr("Shared deck").toStdString());
}
DeckShareItem *item = cmd.add_items();
item->set_deck_list(deck->writeToString_Native().toStdString());
item->set_color_identity(getDeckColorIdentity(*deck, CardDatabaseManager::query()).toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, &DlgShareDeck::shareFinished);
client->sendCommand(pend);
shareTimeoutTimer->start();
}
void DlgShareDeck::shareFinished(const Response &response, const CommandContainer & /*commandContainer*/)
{
shareTimeoutTimer->stop();
if (response.response_code() != Response::RespOk) {
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
QMessageBox::critical(this, tr("Share deck"),
tr("Failed to create the share link (server response code %1).")
.arg(QString::number(static_cast<int>(response.response_code()))));
return;
}
const DeckShareUtils::ShareResponse share = DeckShareUtils::handleShareResponse(client, response);
QMessageBox::information(this, tr("Share deck"),
tr("Share link created and copied to the clipboard:\n\n%1\n\n"
"The share expires on %2.")
.arg(share.link, DeckShareUtils::formatShareExpiry(share.expiry)));
accept();
}
void DlgShareDeck::onShareTimeout()
{
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
QMessageBox::warning(this, tr("Share deck"), tr("The server did not respond in time. Try again."));
}

View file

@ -1,46 +0,0 @@
/**
* @file dlg_share_deck.h
* @ingroup Dialogs
*/
//! \todo Document this file.
#ifndef DLG_SHARE_DECK_H
#define DLG_SHARE_DECK_H
#include <QDialog>
#include <QSharedPointer>
class AbstractClient;
class CommandContainer;
class DeckList;
class QDialogButtonBox;
class QLineEdit;
class QTimer;
class Response;
/**
* @brief Slim dialog to create a temporary share for the deck open in the editor.
*
* Asks for a share name, sends Command_DeckShareCreate for the single inline
* deck, and copies the resulting link to the clipboard.
*/
class DlgShareDeck : public QDialog
{
Q_OBJECT
public:
DlgShareDeck(AbstractClient *_client, const QSharedPointer<DeckList> &_deck, QWidget *parent = nullptr);
private slots:
void actShare();
void shareFinished(const Response &response, const CommandContainer &commandContainer);
void onShareTimeout();
private:
AbstractClient *client;
QSharedPointer<DeckList> deck;
QLineEdit *nameEdit;
QDialogButtonBox *buttonBox;
QTimer *shareTimeoutTimer;
};
#endif // DLG_SHARE_DECK_H

View file

@ -1,181 +0,0 @@
#include "dlg_shared_decks_preview.h"
#include "../deck_share/shared_deck_preview_widget.h"
#include "../general/layout_containers/flow_widget.h"
#include <QCloseEvent>
#include <QDateTime>
#include <QDialogButtonBox>
#include <QLabel>
#include <QPushButton>
#include <QVBoxLayout>
#include <libcockatrice/card/database/card_database_querier.h>
#include <libcockatrice/protocol/pb/serverinfo_deck_share_item.pb.h>
DlgSharedDecksPreview::DlgSharedDecksPreview(QWidget *parent,
const CardDatabaseQuerier *querier,
const QString &shareName,
qint64 expiresAt,
const QString &serverText,
const QList<ServerInfo_DeckShareItem> &items)
: QDialog(parent)
{
setWindowTitle(tr("Open shared decks"));
resize(700, 500);
auto *mainLayout = new QVBoxLayout(this);
// shareName and serverText come from the share server, so escape them: the
// QLabels render AutoText and markup would otherwise be shown as rich text.
auto *titleLabel =
new QLabel(tr("Share: %1").arg((shareName.isEmpty() ? tr("Untitled") : shareName).toHtmlEscaped()), this);
QFont titleFont = titleLabel->font();
titleFont.setBold(true);
titleFont.setPointSize(titleFont.pointSize() + 2);
titleLabel->setFont(titleFont);
mainLayout->addWidget(titleLabel);
if (!serverText.isEmpty()) {
mainLayout->addWidget(new QLabel(tr("From %1").arg(serverText.toHtmlEscaped()), this));
}
if (expiresAt > 0) {
const QString expiryText = QDateTime::fromSecsSinceEpoch(expiresAt).toLocalTime().toString(Qt::TextDate);
mainLayout->addWidget(new QLabel(tr("This share link expires on %1").arg(expiryText), this));
}
downloadStatusLabel = new QLabel(this);
downloadStatusLabel->setVisible(false);
mainLayout->addWidget(downloadStatusLabel);
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
mainLayout->addWidget(flowWidget, 1);
for (const ServerInfo_DeckShareItem &item : items) {
QStringList tags;
for (const auto &tag : item.tags()) {
tags.append(QString::fromStdString(tag));
}
auto *tile = new SharedDeckPreviewWidget(
this, querier, QString::fromStdString(item.name()), QString::fromStdString(item.banner_card()),
QString::fromStdString(item.color_identity()), QString::fromStdString(item.game_format()), tags.join(", "));
flowWidget->addNavigableWidget(tile);
tiles.append(tile);
itemIds.append(item.id());
}
if (tiles.size() == 1) {
tiles.first()->setSelected(true);
}
auto *buttonBox = new QDialogButtonBox(this);
openSelectedButton = buttonBox->addButton(tr("Open selected"), QDialogButtonBox::AcceptRole);
openAllButton = buttonBox->addButton(tr("Open all"), QDialogButtonBox::ActionRole);
buttonBox->addButton(tr("Cancel"), QDialogButtonBox::RejectRole);
mainLayout->addWidget(buttonBox);
connect(buttonBox, &QDialogButtonBox::rejected, this, [this]() {
onCancel();
close();
});
// Esc calls QDialog::reject() directly (which hides the dialog without a
// close event), so route it through the same guarded cancel as the button.
connect(this, &QDialog::rejected, this, [this]() {
onCancel();
close();
});
connect(openSelectedButton, &QPushButton::clicked, this, &DlgSharedDecksPreview::openSelected);
connect(buttonBox, &QDialogButtonBox::clicked, this, [this, buttonBox](QAbstractButton *button) {
if (buttonBox->buttonRole(button) == QDialogButtonBox::ActionRole) {
openAll();
}
});
for (SharedDeckPreviewWidget *tile : tiles) {
connect(tile, &SharedDeckPreviewWidget::selectionToggled, this,
&DlgSharedDecksPreview::updateOpenSelectedEnabled);
}
for (int i = 0; i < tiles.size(); ++i) {
const int itemId = itemIds.at(i);
// Double-clicking a tile selects it and opens just that deck.
connect(tiles.at(i), &SharedDeckPreviewWidget::activated, this, [this, itemId]() {
resultEmitted = true;
setDownloading(true);
emit openRequested(QList<int>{itemId});
});
}
updateOpenSelectedEnabled();
}
QList<int> DlgSharedDecksPreview::selectedItemIds() const
{
QList<int> selectedIds;
for (int i = 0; i < tiles.size(); ++i) {
if (tiles.at(i)->isSelected()) {
selectedIds.append(itemIds.at(i));
}
}
return selectedIds;
}
void DlgSharedDecksPreview::openSelected()
{
const QList<int> selectedIds = selectedItemIds();
if (selectedIds.isEmpty()) {
return;
}
resultEmitted = true;
setDownloading(true);
emit openRequested(selectedIds);
}
void DlgSharedDecksPreview::openAll()
{
resultEmitted = true;
setDownloading(true);
emit openRequested(itemIds);
}
void DlgSharedDecksPreview::setDownloading(bool downloading)
{
if (downloadInProgress == downloading) {
return;
}
downloadInProgress = downloading;
downloadStatusLabel->setVisible(downloading);
for (SharedDeckPreviewWidget *tile : tiles) {
tile->setEnabled(!downloading);
}
openSelectedButton->setEnabled(!downloading);
openAllButton->setEnabled(!downloading);
}
void DlgSharedDecksPreview::setDownloadProgress(int done, int total, const QString &currentDeckName)
{
if (!downloadInProgress) {
return;
}
downloadStatusLabel->setText(tr("Downloading deck %1 of %2: %3").arg(done).arg(total).arg(currentDeckName));
}
void DlgSharedDecksPreview::updateOpenSelectedEnabled()
{
openSelectedButton->setEnabled(!selectedItemIds().isEmpty());
}
void DlgSharedDecksPreview::onCancel()
{
if (!resultEmitted || downloadInProgress) {
resultEmitted = true;
emit cancelled();
}
}
void DlgSharedDecksPreview::closeEvent(QCloseEvent *event)
{
onCancel();
QDialog::closeEvent(event);
}

View file

@ -1,68 +0,0 @@
#ifndef COCKATRICE_DLG_SHARED_DECKS_PREVIEW_H
#define COCKATRICE_DLG_SHARED_DECKS_PREVIEW_H
#include <QDialog>
#include <QList>
class FlowWidget;
class QCloseEvent;
class QLabel;
class QPushButton;
class ServerInfo_DeckShareItem;
class SharedDeckPreviewWidget;
class CardDatabaseQuerier;
/**
* @brief Non-modal preview of the decks contained in a shared-deck link.
*
* Lets the user pick which of the shared decks to open before anything is
* downloaded. Emits openRequested with the ids of the chosen decks, or
* cancelled when the user closes the dialog without choosing. Once the user
* picks, the dialog switches into a "downloading" state: the tiles and open
* buttons are disabled, a progress label shows the current download and Cancel
* stays functional so the download can be aborted.
*/
class DlgSharedDecksPreview : public QDialog
{
Q_OBJECT
public:
explicit DlgSharedDecksPreview(QWidget *parent,
const CardDatabaseQuerier *querier,
const QString &shareName,
qint64 expiresAt,
const QString &serverText,
const QList<ServerInfo_DeckShareItem> &items);
void setDownloadProgress(int done, int total, const QString &currentDeckName);
public slots:
void setDownloading(bool downloading);
signals:
void openRequested(const QList<int> &itemIds);
void cancelled();
protected:
void closeEvent(QCloseEvent *event) override;
private slots:
void openSelected();
void openAll();
void updateOpenSelectedEnabled();
void onCancel();
private:
QList<int> selectedItemIds() const;
FlowWidget *flowWidget;
QList<SharedDeckPreviewWidget *> tiles;
QList<int> itemIds;
QPushButton *openSelectedButton;
QPushButton *openAllButton;
QLabel *downloadStatusLabel;
bool resultEmitted = false;
bool downloadInProgress = false;
};
#endif // COCKATRICE_DLG_SHARED_DECKS_PREVIEW_H

View file

@ -11,8 +11,8 @@ namespace HomeTabButtonColor
*/
enum Source
{
FromThemeColors, ///< Use the theme's identity accent colors
FromBackground, ///< Extract colour from the background image
Automatic, ///< Extract color from background, or use theme color if no background
FromBackground, ///< Always extract color from background
};
struct Entry
@ -23,7 +23,7 @@ struct Entry
inline QList<Entry> all()
{
static QList<Entry> entries = {{FromThemeColors, QT_TR_NOOP("From theme colors")},
static QList<Entry> entries = {{Automatic, QT_TR_NOOP("Automatic")},
{FromBackground, QT_TR_NOOP("Extract from background")}};
return entries;
@ -33,12 +33,12 @@ inline QList<Entry> all()
* Safely converts an int into the corresponding Source.
*
* @param value The int value
* @return The Source. Returns Source::FromThemeColors if the value is not within range
* @return The Source. Returns Source::Automatic if the value is not within range
*/
inline Source intToSource(int value)
{
if (value > FromBackground) {
return FromThemeColors; // default
return Automatic; // default
}
return static_cast<Source>(value);

View file

@ -11,8 +11,6 @@
#include "home_tab_button_color.h"
#include <QGroupBox>
#include <QLabel>
#include <QLinearGradient>
#include <QPainter>
#include <QPainterPath>
#include <QPushButton>
@ -23,7 +21,8 @@
#include <libcockatrice/settings/paths_settings.h>
HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
: QWidget(parent), tabSupervisor(_tabSupervisor), background(themePixmap(QStringLiteral("backgrounds/home")))
: QWidget(parent), tabSupervisor(_tabSupervisor), background(themePixmap(QStringLiteral("backgrounds/home"))),
overlay(themePixmap(QStringLiteral("cockatrice")))
{
layout = new QGridLayout(this);
@ -55,8 +54,6 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
// Lambda is cleaner to read than overloading this
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabDisplayCardNameChanged, this,
[this] { repaint(); });
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundDimChanged, this,
[this] { repaint(); });
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
&HomeWidget::initializeBackgroundFromSource);
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
@ -64,18 +61,12 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
// Scheme flips (light/dark/system with an OS switch) fire on themeManager,
// not on SettingsCache::themeChanged, so re-resolve the variant background.
connect(themeManager, &ThemeManager::themeChanged, this, &HomeWidget::initializeBackgroundFromSource);
connect(themeManager, &ThemeManager::paletteChanged, this, &HomeWidget::updateButtonsToBackgroundColor);
connect(themeManager, &ThemeManager::paletteChanged, this, &HomeWidget::updateLogoOverlay);
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabButtonColorChanged, this,
&HomeWidget::updateButtonsToBackgroundColor);
}
void HomeWidget::initializeBackgroundFromSource()
{
// The featured logo is theme/scheme-derived too; reload it alongside the
// background so a theme or appearance switch doesn't leave it stale.
updateLogoOverlay();
if (CardDatabaseManager::getInstance()->getLoadStatus() != LoadStatus::Ok) {
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this,
&HomeWidget::initializeBackgroundFromSource);
@ -114,24 +105,32 @@ void HomeWidget::loadBackgroundSourceDeck()
backgroundSourceDeck = deckOpt.has_value() ? deckOpt.value().deckList : DeckList();
}
static QPair<QColor, QColor> paletteDerivedButtonColors()
static bool isDefaultBackgroundAndTheme()
{
return {themeManager->appColor(AppColor::AccentStrong), themeManager->appColor(AppColor::AccentSoft)};
QString sourceId = SettingsCache::instance().appearance().getHomeTabBackgroundSource();
return themeManager->isBuiltInTheme() && BackgroundSources::fromId(sourceId) == BackgroundSources::Theme;
}
QPair<QColor, QColor> HomeWidget::determineButtonColor() const
{
static QPair defaultColor = {QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80)};
auto colorSource =
HomeTabButtonColor::intToSource(SettingsCache::instance().appearance().getHomeTabButtonColorSourceIndex());
switch (colorSource) {
case HomeTabButtonColor::FromThemeColors:
return paletteDerivedButtonColors();
case HomeTabButtonColor::Automatic: {
if (isDefaultBackgroundAndTheme()) {
return defaultColor;
} else {
return extractDominantColors(background);
}
}
case HomeTabButtonColor::FromBackground:
return extractDominantColors(background);
}
return paletteDerivedButtonColors();
return defaultColor;
}
void HomeWidget::setRandomCard(ExactCard &newCard)
@ -235,10 +234,10 @@ QGroupBox *HomeWidget::createButtons()
QVBoxLayout *boxLayout = new QVBoxLayout;
boxLayout->setAlignment(Qt::AlignHCenter);
logoLabel = new QLabel;
QLabel *logoLabel = new QLabel;
logoLabel->setPixmap(overlay.scaledToWidth(200, Qt::SmoothTransformation));
logoLabel->setAlignment(Qt::AlignCenter);
boxLayout->addWidget(logoLabel);
updateLogoOverlay();
boxLayout->addSpacing(25);
connectButton = new HomeStyledButton("Connect/Play", gradientColors);
@ -366,15 +365,13 @@ void HomeWidget::paintEvent(QPaintEvent *event)
painter.drawPixmap(topLeft, toDraw);
}
if (SettingsCache::instance().appearance().getHomeTabBackgroundDim()) {
// Draw translucent black overlay with rounded corners
QRectF overlayRect(5, 5, width() - 10, height() - 10);
QPainterPath roundedRectPath;
roundedRectPath.addRoundedRect(overlayRect, 20, 20);
// Draw translucent black overlay with rounded corners
QRectF overlayRect(5, 5, width() - 10, height() - 10);
QPainterPath roundedRectPath;
roundedRectPath.addRoundedRect(overlayRect, 20, 20);
QColor semiTransparentBlack(0, 0, 0, static_cast<int>(255 * 0.33));
painter.fillPath(roundedRectPath, semiTransparentBlack);
}
QColor semiTransparentBlack(0, 0, 0, static_cast<int>(255 * 0.33));
painter.fillPath(roundedRectPath, semiTransparentBlack);
// Card name overlay (above the attribution, bottom-right)
QString cardName;
@ -439,56 +436,3 @@ void HomeWidget::paintEvent(QPaintEvent *event)
QWidget::paintEvent(event);
}
void HomeWidget::updateLogoOverlay()
{
// Emulate cockatrice.svg in Qt rather than rendering the baked-in SVG.
// The SVG has no separate plate: the gradient fills the bird's silhouette
// paths (light #c9fd62/AccentSoft at the top-left, dark #139740/AccentStrong
// toward the bottom-right — the SVG's linearGradient4265-7-8 stops along
// its userSpaceOnUse axis), and the white highlight path
// (cockatrice-logo-white) sits on top. So we paint that gradient clipped to
// the full logo silhouette (the full-color logo's alpha), then overlay the
// white mark. Colours stay fully theme-driven and independent of the static
// greens baked into the SVG.
const QColor strong = themeManager->appColor(AppColor::AccentStrong);
const QColor soft = themeManager->appColor(AppColor::AccentSoft);
const QPixmap silhouette = themePixmap(QStringLiteral("cockatrice")).scaledToWidth(200, Qt::SmoothTransformation);
const QPixmap whiteMark =
themePixmap(QStringLiteral("cockatrice-logo-white")).scaledToWidth(200, Qt::SmoothTransformation);
if (silhouette.isNull() || whiteMark.isNull()) {
return;
}
QPixmap composite(silhouette.size());
composite.fill(Qt::transparent);
{
QPainter painter(&composite);
painter.setRenderHint(QPainter::Antialiasing);
painter.setRenderHint(QPainter::SmoothPixmapTransform);
// Recreate cockatrice.svg's own gradient geometry (linearGradient
// 4265-7-8, userSpaceOnUse): light AccentSoft at S=(-8.097,-97.746),
// dark AccentStrong at E=(162.455,295.208), on the SVG's 300x300
// canvas. Scale those coordinates to this composite's size.
const qreal scale = composite.width() / 300.0;
QLinearGradient gradient(QPointF(-8.097, -97.746) * scale, QPointF(162.455, 295.208) * scale);
gradient.setColorAt(0.0, soft);
gradient.setColorAt(1.0, strong);
painter.fillRect(composite.rect(), gradient);
// Clip the gradient to the full logo silhouette exactly as the SVG's
// gradient paths are confined to the bird.
painter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
painter.drawPixmap(0, 0, silhouette);
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
painter.drawPixmap(0, 0, whiteMark);
}
if (logoLabel) {
logoLabel->setPixmap(composite);
}
}

View file

@ -15,9 +15,6 @@
#include <QWidget>
#include <libcockatrice/network/client/abstract/abstract_client.h>
class QGridLayout;
class QLabel;
class HomeWidget : public QWidget
{
@ -44,14 +41,13 @@ private:
QPixmap background;
CardInfoPictureArtCropWidget *backgroundSourceCard = nullptr;
DeckList backgroundSourceDeck;
QLabel *logoLabel = nullptr;
QPixmap overlay;
QPair<QColor, QColor> gradientColors;
HomeStyledButton *connectButton;
void setRandomCard(ExactCard &newCard);
void loadBackgroundSourceDeck();
QPair<QColor, QColor> determineButtonColor() const;
void updateLogoOverlay();
};
#endif // HOME_WIDGET_H

View file

@ -7,7 +7,6 @@
#include "flow_widget.h"
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QResizeEvent>
#include <QScrollArea>
#include <QSizePolicy>
@ -81,35 +80,13 @@ FlowWidget::FlowWidget(QWidget *parent,
/**
* @brief Adds a widget to the flow layout within the FlowWidget.
*
* Plain widgets are not filtered for arrow keys: intercepting them would steal
* Up/Down/Left/Right from controls that use them (combo boxes, spin boxes
* etc.). Widgets that want keyboard navigation between flow items must be
* added via addNavigableWidget instead.
*
* @param widget_to_add The widget to add to the flow layout.
*/
void FlowWidget::addWidget(QWidget *widget_to_add)
void FlowWidget::addWidget(QWidget *widget_to_add) const
{
flowLayout->addWidget(widget_to_add);
}
/**
* @brief Adds a widget and routes its arrow keys to FlowWidget focus navigation.
*
* The widget is filtered for arrow-key events so keyboard navigation between
* the flow items keeps working even when the flow sits inside a QScrollArea,
* which swallows arrow keys before they can reach FlowWidget::keyPressEvent.
* Only widgets added through this method are affected; anything that needs its
* own arrow keys should use plain addWidget.
*
* @param widget_to_add The widget to add to the flow layout.
*/
void FlowWidget::addNavigableWidget(QWidget *widget_to_add)
{
widget_to_add->installEventFilter(this);
flowLayout->addWidget(widget_to_add);
}
void FlowWidget::insertWidgetAtIndex(QWidget *toInsert, int index)
{
flowLayout->insertWidgetAtIndex(toInsert, index);
@ -200,66 +177,6 @@ QLayoutItem *FlowWidget::itemAt(int index) const
return flowLayout->itemAt(index);
}
void FlowWidget::keyPressEvent(QKeyEvent *event)
{
if (moveFocus(event)) {
event->accept();
return;
}
QWidget::keyPressEvent(event);
}
bool FlowWidget::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() == QEvent::KeyPress && moveFocus(static_cast<QKeyEvent *>(event))) {
return true;
}
return QWidget::eventFilter(watched, event);
}
bool FlowWidget::moveFocus(QKeyEvent *event)
{
// Keyboard navigation between the flow items: arrow keys move focus just
// like clicking the sibling tiles would. Only items that can take keyboard
// focus (e.g. the deck-preview tiles in shared-deck links) are visited.
const bool moveForward = event->key() == Qt::Key_Right || event->key() == Qt::Key_Down;
const bool moveBackward = event->key() == Qt::Key_Left || event->key() == Qt::Key_Up;
if (!moveForward && !moveBackward) {
return false;
}
QList<QWidget *> focusableItems;
for (int i = 0; i < flowLayout->count(); ++i) {
QWidget *item = flowLayout->itemAt(i)->widget();
if (item != nullptr && (item->focusPolicy() & Qt::TabFocus)) {
focusableItems.append(item);
}
}
if (focusableItems.isEmpty()) {
return false;
}
int currentIndex = -1;
for (int i = 0; i < focusableItems.size(); ++i) {
if (focusableItems.at(i)->hasFocus()) {
currentIndex = i;
break;
}
}
const int delta = moveForward ? 1 : -1;
int nextIndex;
if (currentIndex < 0) {
nextIndex = moveForward ? 0 : focusableItems.size() - 1;
} else {
nextIndex = (currentIndex + delta + focusableItems.size()) % focusableItems.size();
}
focusableItems.value(nextIndex)->setFocus();
event->accept();
return true;
}
int FlowWidget::count() const
{
return flowLayout->count();

View file

@ -11,7 +11,6 @@
#include "../../../layouts/flow_layout.h"
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QLoggingCategory>
#include <QScrollArea>
#include <QWidget>
@ -29,8 +28,7 @@ public:
Qt::ScrollBarPolicy horizontalPolicy,
Qt::ScrollBarPolicy verticalPolicy);
void addWidget(QWidget *widget_to_add);
void addNavigableWidget(QWidget *widget_to_add);
void addWidget(QWidget *widget_to_add) const;
void insertWidgetAtIndex(QWidget *toInsert, int index);
void removeWidget(QWidget *widgetToRemove) const;
void clearLayout();
@ -45,15 +43,9 @@ public slots:
void setSpacing(int hSpacing, int vSpacing);
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
private:
/// @brief Moves keyboard focus to an adjacent flow item for an arrow-key event.
/// @return True when the event was an arrow key and was handled.
bool moveFocus(QKeyEvent *event);
Qt::Orientation flowDirection;
QHBoxLayout *mainLayout;
FlowLayout *flowLayout;

View file

@ -28,9 +28,6 @@ DeckEditorMenu::DeckEditorMenu(AbstractTabDeckEditor *parent) : QMenu(parent), d
aSaveDeckAs = new QAction(QString(), this);
connect(aSaveDeckAs, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actSaveDeckAs);
aShareDeck = new QAction(QString(), this);
connect(aShareDeck, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actShareDeck);
aLoadDeckFromClipboard = new QAction(QString(), this);
connect(aLoadDeckFromClipboard, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actLoadDeckFromClipboard);
@ -99,7 +96,6 @@ DeckEditorMenu::DeckEditorMenu(AbstractTabDeckEditor *parent) : QMenu(parent), d
addMenu(loadRecentDeckMenu);
addAction(aSaveDeck);
addAction(aSaveDeckAs);
addAction(aShareDeck);
addSeparator();
addAction(aLoadDeckFromClipboard);
addMenu(editDeckInClipboardMenu);
@ -124,7 +120,6 @@ void DeckEditorMenu::setSaveStatus(bool newStatus)
{
aSaveDeck->setEnabled(newStatus);
aSaveDeckAs->setEnabled(newStatus);
aShareDeck->setEnabled(newStatus);
aSaveDeckToClipboard->setEnabled(newStatus);
aSaveDeckToClipboardNoSetInfo->setEnabled(newStatus);
aSaveDeckToClipboardRaw->setEnabled(newStatus);
@ -162,7 +157,6 @@ void DeckEditorMenu::retranslateUi()
aClearRecents->setText(tr("Clear"));
aSaveDeck->setText(tr("&Save deck"));
aSaveDeckAs->setText(tr("Save deck &as..."));
aShareDeck->setText(tr("Share deck..."));
aLoadDeckFromClipboard->setText(tr("Load deck from cl&ipboard..."));

View file

@ -21,8 +21,7 @@ public:
QAction *aNewDeck, *aLoadDeck, *aClearRecents, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard,
*aEditDeckInClipboard, *aEditDeckInClipboardRaw, *aSaveDeckToClipboard, *aSaveDeckToClipboardNoSetInfo,
*aSaveDeckToClipboardRaw, *aSaveDeckToClipboardRawNoSetInfo, *aPrintDeck, *aLoadDeckFromWebsite,
*aExportDeckDecklist, *aExportDeckDecklistXyz, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout, *aShareDeck,
*aClose;
*aExportDeckDecklist, *aExportDeckDecklistXyz, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout, *aClose;
QMenu *loadRecentDeckMenu, *analyzeDeckMenu, *editDeckInClipboardMenu, *saveDeckToClipboardMenu;
void setSaveStatus(bool newStatus);

View file

@ -38,10 +38,6 @@ class BannerShaderConfig : public QObject
Q_PROPERTY(QColor colorA READ colorA WRITE setColorA NOTIFY colorAChanged)
Q_PROPERTY(QColor colorB READ colorB WRITE setColorB NOTIFY colorBChanged)
Q_PROPERTY(QColor accent READ accent WRITE setAccent NOTIFY accentChanged)
Q_PROPERTY(QColor glowColor READ glowColor WRITE setGlowColor NOTIFY glowColorChanged)
Q_PROPERTY(QColor brandStrong READ brandStrong WRITE setBrandStrong NOTIFY brandStrongChanged)
Q_PROPERTY(QColor brandSoft READ brandSoft WRITE setBrandSoft NOTIFY brandSoftChanged)
Q_PROPERTY(qreal vignetteMin READ vignetteMin WRITE setVignetteMin NOTIFY vignetteMinChanged)
Q_PROPERTY(bool logoVisible READ logoVisible WRITE setLogoVisible NOTIFY logoVisibleChanged)
Q_PROPERTY(qreal logoGlow READ logoGlow WRITE setLogoGlow NOTIFY logoGlowChanged)
@ -189,54 +185,6 @@ public:
}
}
QColor glowColor() const
{
return m_glowColor;
}
void setGlowColor(const QColor &c)
{
if (c != m_glowColor) {
m_glowColor = c;
emit glowColorChanged();
}
}
QColor brandStrong() const
{
return m_brandStrong;
}
void setBrandStrong(const QColor &c)
{
if (c != m_brandStrong) {
m_brandStrong = c;
emit brandStrongChanged();
}
}
QColor brandSoft() const
{
return m_brandSoft;
}
void setBrandSoft(const QColor &c)
{
if (c != m_brandSoft) {
m_brandSoft = c;
emit brandSoftChanged();
}
}
qreal vignetteMin() const
{
return m_vignetteMin;
}
void setVignetteMin(qreal v)
{
if (v != m_vignetteMin) {
m_vignetteMin = v;
emit vignetteMinChanged();
}
}
bool logoVisible() const
{
return m_logoVisible;
@ -274,10 +222,6 @@ signals:
void colorAChanged();
void colorBChanged();
void accentChanged();
void glowColorChanged();
void brandStrongChanged();
void brandSoftChanged();
void vignetteMinChanged();
void logoVisibleChanged();
void logoGlowChanged();
@ -295,16 +239,9 @@ private:
bool m_frontIsA = true;
// Curated fallback seed values -- BannerHost overwrites these with
// palette-derived colours (see shader_banner_widget.cpp) before the first
// paint, so they only matter as a safe pre-first-apply default.
QColor m_colorA{0x1A, 0x1A, 0x20};
QColor m_colorB{0x0E, 0x0E, 0x12};
QColor m_accent{0x8B, 0xDD, 0x6B};
QColor m_glowColor{Qt::white};
QColor m_brandStrong{0x13, 0x97, 0x40};
QColor m_brandSoft{0xC9, 0xFD, 0x62};
qreal m_vignetteMin = 0.62;
bool m_logoVisible = false;
qreal m_logoGlow = 1.0;

View file

@ -1,15 +0,0 @@
#ifndef BRAND_COLORS_H
#define BRAND_COLORS_H
#include <QColor>
/** @brief Cockatrice brand green.
*
* Single source of truth for the onboarding brand accent: it backs the
* banner's shader-accent uniform as the curated fallback when the active
* palette resolves no usable Highlight, and it preseads the wizard's
* QuickSetupPanel so a freshly generated palette keeps the brand identity
* until the user picks their own look. */
inline const QColor kCockatriceBrandGreen(0x8B, 0xDD, 0x6B);
#endif // BRAND_COLORS_H

View file

@ -14,31 +14,10 @@
#include <QGroupBox>
#include <QLabel>
#include <QMessageBox>
#include <QPalette>
#include <QVBoxLayout>
#include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/personal_settings.h>
namespace
{
/** @brief A theme's shipped identity accent, immune to any user- or auto-
* generated palette that may currently be masking appColor(). */
QColor themeIdentityAccent(const QString &themeDirPath, const QString &themeName)
{
for (const QString &scheme : {QStringLiteral("Light"), QStringLiteral("Dark")}) {
const PaletteConfig cfg = ThemeManager::loadDefaultPaletteConfig(themeDirPath, themeName, scheme);
if (cfg.appColors.contains(AppColor::AccentStrong)) {
return cfg.appColors.value(AppColor::AccentStrong);
}
const QColor highlight = cfg.colors.value(QPalette::Active).value(QPalette::Highlight);
if (highlight.isValid()) {
return highlight;
}
}
return {};
}
} // namespace
ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
{
themeCombo = new QComboBox(this);
@ -51,15 +30,6 @@ ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
quickSetupPanel = new QuickSetupPanel(this);
// Seed the picker from the current theme's own identity accent rather than
// a hardcoded brand green: Plasma seeds violet, Fusion green, etc., and it
// is immune to stale generated palettes that may mask appColor(). This was
// initially a brand-green workaround from before Fusion became the default.
// setAccentColor blocks signals, so this never triggers a generation.
lastSeededTheme = SettingsCache::instance().getThemeName();
quickSetupPanel->setAccentColor(
themeIdentityAccent(themeManager->getAvailableThemes().value(lastSeededTheme), lastSeededTheme));
connect(themeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onThemeChanged);
connect(schemeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onSchemeChanged);
connect(quickSetupPanel, &QuickSetupPanel::valueChanged, this, &ThemeSetupPage::onGenerateFromAccent);
@ -77,8 +47,7 @@ ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
// Mirrors AppearanceSettingsPage's identical listener for the combo-sync
// half of this.
connect(themeManager, &ThemeManager::themeChanged, this, [this] {
const QString newTheme = SettingsCache::instance().getThemeName();
const QString newDir = themeManager->getAvailableThemes().value(newTheme);
const QString newDir = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
const ThemeConfig cfg = ThemeConfig::fromThemeDir(newDir);
const QString current = cfg.colorScheme;
@ -87,14 +56,6 @@ ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
schemeCombo->setCurrentIndex(idx >= 0 ? idx : 0);
schemeCombo->blockSignals(false);
// Keep the picker's accent in step with the theme's own identity; the
// swatch seeded at construction would otherwise stay stale (e.g. green
// from a previous theme) when the user toggles themes.
if (newTheme != lastSeededTheme) {
lastSeededTheme = newTheme;
quickSetupPanel->setAccentColor(themeIdentityAccent(newDir, newTheme));
}
maybeAutoGeneratePalette();
});
@ -197,14 +158,8 @@ void ThemeSetupPage::maybeAutoGeneratePalette()
const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
const QString scheme = resolvedScheme();
// The theme dir may resolve to the user profile even for built-in themes
// (getAvailableThemes gives the user copy precedence), so consult the
// shipped palette too -- both via loadDefaultPaletteConfig's system fallback.
// Without it, scheme flips regenerate a fresh palette from the picker accent
// and clobber the curated colours the theme explicitly ships.
if (PaletteConfig::fromScheme(dirPath, scheme).hasPalette() ||
ThemeManager::loadDefaultPaletteConfig(dirPath, SettingsCache::instance().getThemeName(), scheme)
.hasPalette()) {
PaletteConfig::fromDefault(dirPath, scheme).hasPalette()) {
return; // theme already has something real to show -- leave it alone
}

View file

@ -53,9 +53,6 @@ private:
QComboBox *homeTabBackgroundCombo;
bool paletteDirty = false;
/// Theme whose identity accent currently seeds the picker.
QString lastSeededTheme;
};
#endif // THEME_SETUP_PAGE_H

View file

@ -16,8 +16,6 @@ Item {
property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0)
property vector4d uColorB: Qt.vector4d(bannerConfig.colorB.r, bannerConfig.colorB.g, bannerConfig.colorB.b, 1.0)
property vector4d uAccent: Qt.vector4d(bannerConfig.accent.r, bannerConfig.accent.g, bannerConfig.accent.b, 1.0)
property vector4d uGlowColor: Qt.vector4d(bannerConfig.glowColor.r, bannerConfig.glowColor.g, bannerConfig.glowColor.b, 1.0)
property real uVignetteMin: bannerConfig.vignetteMin
property real uLogoGlow: bannerConfig.logoGlow
fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb"
}
@ -35,62 +33,30 @@ Item {
property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0)
property vector4d uColorB: Qt.vector4d(bannerConfig.colorB.r, bannerConfig.colorB.g, bannerConfig.colorB.b, 1.0)
property vector4d uAccent: Qt.vector4d(bannerConfig.accent.r, bannerConfig.accent.g, bannerConfig.accent.b, 1.0)
property vector4d uGlowColor: Qt.vector4d(bannerConfig.glowColor.r, bannerConfig.glowColor.g, bannerConfig.glowColor.b, 1.0)
property real uVignetteMin: bannerConfig.vignetteMin
property real uLogoGlow: bannerConfig.logoGlow
fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb"
}
// The white logo sits at full opacity on top of the static gradient plate
// no glow, no breathing. The plate matches home_widget's QPainter composite.
Item {
id: logoHost
visible: bannerConfig.logoVisible
// The hero logo itself breathes cleanly over a 0.51.0 opacity range
Image {
id: logo
anchors.centerIn: parent
visible: bannerConfig.logoVisible
source: "qrc:/resources/cockatrice-logo-white.svg"
width: root.height * 0.6
height: width
height: width * (sourceSize.height > 0 ? sourceSize.height / Math.max(sourceSize.width, 1) : 1)
fillMode: Image.PreserveAspectFit
smooth: true
opacity: 0.5 + 0.5 * bannerConfig.logoGlow
sourceSize: Qt.size(256, 256)
// The full-color logo renders beneath the white mark and is consumed as
// a texture (layer.enabled) by the plate shader's silhouette mask, so
// the gradient is clipped to the bird exactly as the SVG's gradient
// paths are. It is never drawn to the screen itself.
Image {
id: silhouetteMask
anchors.fill: parent
source: "qrc:/resources/cockatrice.svg"
sourceSize: Qt.size(256, 256)
fillMode: Image.PreserveAspectFit
smooth: true
visible: false
layer.enabled: true
layer.smooth: true
}
Behavior on opacity { NumberAnimation { duration: 300; easing.type: Easing.InOutSine } }
// The logo's gradient plate, drawn behind the mark: a linear
// AccentSoft (light) -> AccentStrong (dark) sheet along the same
// top-left -> bottom-right userSpaceOnUse axis the baked-in SVG used,
// clipped to the bird silhouette via uSilhouette. The white highlight
// path above is theme independent. Sized to the logo itself no
// rounded badge, matching home_widget's QPainter composite. Static.
// Small ShaderEffect, Qt 6.4-safe.
ShaderEffect {
id: brandPlate
anchors.fill: parent
property vector4d uStrong: Qt.vector4d(bannerConfig.brandStrong.r, bannerConfig.brandStrong.g,
bannerConfig.brandStrong.b, 1.0)
property vector4d uSoft: Qt.vector4d(bannerConfig.brandSoft.r, bannerConfig.brandSoft.g,
bannerConfig.brandSoft.b, 1.0)
property var uSilhouette: silhouetteMask
fragmentShader: "qrc:/onboarding/shaders/brand_plate.frag.qsb"
}
Image {
id: logoImage
anchors.fill: parent
source: "qrc:/resources/cockatrice-logo-white.svg"
sourceSize: Qt.size(256, 256)
fillMode: Image.PreserveAspectFit
smooth: true
transform: Scale {
origin.x: logo.width / 2
origin.y: logo.height / 2
xScale: 0.94 + 0.06 * bannerConfig.logoGlow
yScale: 0.94 + 0.06 * bannerConfig.logoGlow
}
}
}
}

View file

@ -1,10 +1,7 @@
#include "shader_banner_widget.h"
#include "../../theme_manager.h"
#include "banner_shader_config.h"
#include "brand_colors.h"
#include <QApplication>
#include <QPainter>
#include <QQmlContext>
#include <QQmlEngine>
@ -14,98 +11,11 @@
namespace
{
// Curated near-black stage -- used only when the active palette resolves no
// usable window colour. Matches the banner's original design (dark and quiet
// so the accent stands out) and satisfies design-plans §2.1's "identity
// survives a bare palette".
constexpr QRgb kFallbackColorA = 0x1A1A20;
constexpr QRgb kFallbackColorB = 0x0E0E12;
struct SuggestedColors
{
QColor colorA;
QColor colorB;
QColor accent;
QColor glowColor;
QColor brandStrong;
QColor brandSoft;
qreal vignetteMin = 0.62;
};
SuggestedColors suggestedBannerColors()
{
const QPalette &pal = qApp->palette();
const QColor window = pal.color(QPalette::Active, QPalette::Window);
// Identity accent: the theme's [AppColors] AccentStrong, which appColor()
// resolves to QPalette::Highlight when a theme doesn't pin AccentStrong.
// Reading bare Highlight ignored curated accent tokens (Plasma's violet
// vs Default's green) whenever a palette didn't set the role itself.
const QColor accentStrong = themeManager->appColor(AppColor::AccentStrong);
if (!window.isValid() || !accentStrong.isValid()) {
return {QColor(kFallbackColorA),
QColor(kFallbackColorB),
kCockatriceBrandGreen,
QColor(Qt::white),
kCockatriceBrandGreen,
QColor(0xC9, 0xFD, 0x62),
0.62};
}
// The theme's brand pair: AccentStrong is the deep green, AccentSoft the
// lime. These two appColors form the logo's "surrounding gradient" (deep
// core grading out to the soft, brand-toned glow) on both the banner and
// the home screen.
const QColor brandStrong = accentStrong;
const QColor brandSoft = themeManager->appColor(AppColor::AccentSoft);
// Dress the stage for the scheme so the banner never fights the
// surrounding window in either mode. Dark palettes keep the original
// quiet near-black stage (lightness 29 → 16) with the theme's window
// hue; light palettes get a pastel "frosted accent" treatment built from
// the accent hue instead of a plain near-white copy: a coloured wash that
// clearly belongs to the theme.
const qreal luma = 0.299 * window.red() + 0.587 * window.green() + 0.114 * window.blue();
const bool lightStage = luma > 115.0;
if (lightStage) {
const int hue = accentStrong.hslHue();
// Achromatic accents (grey) get a neutral near-white stage instead.
const int stageSat = hue < 0 ? 0 : 64;
const int hueSafe = hue < 0 ? 0 : hue;
// Depth is what stops a light stage reading as a washed-out near-white
// copy of the page behind the banner: deepen the lower pastel band and
// raise saturation so the hue is clearly present while staying frosted.
auto pastel = [hueSafe, stageSat](int lightness) { return QColor::fromHsl(hueSafe, stageSat, lightness); };
auto pastelLower = [hueSafe](int lightness) { return QColor::fromHsl(hueSafe, 76, lightness); };
// Brightness-lifted accent for additive glows: the raw accent on a
// light stage must be mid-bright to read instead of washing out, so
// lift lightness and saturation together.
const int accentLightness = qBound(158, accentStrong.lightness() + 82, 198);
const int accentSaturation = hue < 0 ? 0 : qMax(accentStrong.hslSaturation(), 180);
const QColor liftedAccent =
hue < 0 ? accentStrong : QColor::fromHsl(hueSafe, accentSaturation, accentLightness);
// The centre glow (and logo tint in QML) uses the deep accent itself:
// a coloured halo/fill behind the logo instead of a white or black one.
return {pastel(214), pastelLower(186), liftedAccent, accentStrong, brandStrong, brandSoft, 0.80};
}
// Dark stage: force the window hue down to the banner's curated darkness,
// scaling saturation away so chromatic palettes tint it without going
// muddy. The accent is the bright, brand-driven tone (hue from the accent
// itself, never the -- often grey -- window), and it drives both the
// embers/fog and the logo glow so the mark tints like the light stage.
auto stage = [&window](int lightness) {
const int hue = window.hslHue();
const int saturation = hue < 0 ? 0 : qBound(0, qRound(window.hslSaturation() * (lightness / 40.0)), 255);
return QColor::fromHsl(hue, saturation, lightness);
};
const int accentHue = accentStrong.hslHue();
const int accentHueSafe = accentHue < 0 ? 0 : accentHue;
const int accentLightness = qBound(150, accentStrong.lightness() + 70, 185);
const int accentSaturation = accentHue < 0 ? 0 : qMax(accentStrong.hslSaturation(), 160);
const QColor accent =
accentHue < 0 ? accentStrong : QColor::fromHsl(accentHueSafe, accentSaturation, accentLightness);
return {stage(29), stage(16), accent, accent, brandStrong, brandSoft, 0.62};
}
// Near-black base palette -- the background is dark and quiet so the green
// accent stands out.
constexpr QRgb kColorA = 0x1A1A20;
constexpr QRgb kColorB = 0x0E0E12;
constexpr QRgb kAccent = 0x8BDD6B;
} // namespace
class GradientFallbackWidget : public QWidget
@ -113,27 +23,15 @@ class GradientFallbackWidget : public QWidget
public:
using QWidget::QWidget;
void setColors(const QColor &a, const QColor &b)
{
if (a != colorA || b != colorB) {
colorA = a;
colorB = b;
}
}
protected:
void paintEvent(QPaintEvent *) override
{
QPainter painter(this);
QLinearGradient gradient(0, 0, width(), height());
gradient.setColorAt(0.0, colorA);
gradient.setColorAt(1.0, colorB);
gradient.setColorAt(0.0, QColor(kColorA));
gradient.setColorAt(1.0, QColor(kColorB));
painter.fillRect(rect(), gradient);
}
private:
QColor colorA{QColor(kFallbackColorA)};
QColor colorB{QColor(kFallbackColorB)};
};
BannerHost::BannerHost(QWidget *parent) : QWidget(parent)
@ -164,9 +62,6 @@ BannerHost::BannerHost(QWidget *parent) : QWidget(parent)
connect(&clock, &QTimer::timeout, this, &BannerHost::tick);
clock.setInterval(16); // ~60fps; the shader itself is cheap, this is just a wall clock
connect(themeManager, &ThemeManager::themeChanged, this, &BannerHost::applyThemeColors);
applyThemeColors();
applyMotifPreset(currentMotif);
updateAspect();
}
@ -228,9 +123,9 @@ void BannerHost::applyMotifPreset(Motif motif)
const Preset p = presetFor(motif);
config->setColorA(bannerColorA);
config->setColorB(bannerColorB);
config->setAccent(bannerAccent);
config->setColorA(QColor(kColorA));
config->setColorB(QColor(kColorB));
config->setAccent(QColor(kAccent));
config->setLogoVisible(motif == Motif::Welcome);
if (isFirstApply) {
@ -288,34 +183,8 @@ void BannerHost::hideEvent(QHideEvent *event)
clock.stop();
}
void BannerHost::applyThemeColors()
{
const SuggestedColors colors = suggestedBannerColors();
bannerColorA = colors.colorA;
bannerColorB = colors.colorB;
bannerAccent = colors.accent;
if (usingFallback) {
fallback->setColors(bannerColorA, bannerColorB);
fallback->update();
} else if (config) {
config->setColorA(bannerColorA);
config->setColorB(bannerColorB);
config->setAccent(bannerAccent);
config->setGlowColor(colors.glowColor);
config->setBrandStrong(colors.brandStrong);
config->setBrandSoft(colors.brandSoft);
config->setVignetteMin(colors.vignetteMin);
}
}
void BannerHost::tick()
{
// Palette previews (e.g. accent drags in the wizard's QuickSetupPanel)
// apply qApp->palette() without firing themeChanged, so re-derive here;
// BannerShaderConfig's setters are equality-guarded, so this is a no-op
// unless the colours actually changed.
applyThemeColors();
if (config) {
qreal t = elapsed.elapsed() / 1000.0;
config->setTime(t);

View file

@ -1,7 +1,6 @@
#ifndef SHADER_BANNER_WIDGET_H
#define SHADER_BANNER_WIDGET_H
#include <QColor>
#include <QElapsedTimer>
#include <QTimer>
#include <QWidget>
@ -54,7 +53,6 @@ protected:
private slots:
void tick();
void onSceneGraphFailed();
void applyThemeColors();
private:
struct Preset
@ -75,12 +73,6 @@ private:
BannerShaderConfig *config = nullptr;
GradientFallbackWidget *fallback = nullptr;
// Palette-derived banner colours -- the theme's window hue forced down to
// the banner's curated darkness, plus the theme's Highlight as accent.
QColor bannerColorA;
QColor bannerColorB;
QColor bannerAccent;
QTimer clock;
QElapsedTimer elapsed;
Motif currentMotif = Motif::Welcome;

View file

@ -28,8 +28,6 @@ layout(std140, binding = 0) uniform buf
vec4 uColorA;
vec4 uColorB;
vec4 uAccent;
vec4 uGlowColor;
float uVignetteMin;
float uLogoGlow;
};
@ -121,7 +119,7 @@ vec3 backgroundField(vec2 uv, float time)
// Accent-coloured fog layer: flowNoise peaks above 0.6 contribute accent
float fog = flowNoise(uv * 0.8 + vec2(100.0, 50.0), time * 0.02);
col += uAccent.rgb * max(fog - 0.6, 0.0) * 0.14;
col += uAccent.rgb * max(fog - 0.6, 0.0) * 0.10;
return col;
}
@ -138,17 +136,14 @@ vec3 motifWelcome(vec2 uv, vec3 bg, float t)
vec2 center = vec2(asp * 0.5, 0.5);
float cDist = length(ac - center);
// Centre bloom at logo position; intensity scales with uLogoGlow. The
// QML brandGlow halo now supplies the primary logo surround (the two
// brand appColors), so this shader bloom is deliberately kept as a subtle
// ambience rather than a competing glow.
// Centre bloom at logo position; intensity scales with uLogoGlow
float centreLight = bloom(cDist, 0.08 * asp, 0.40 * asp);
col += uGlowColor.rgb * centreLight * 0.20 * uLogoGlow;
col += centreLight * 0.20 * uLogoGlow;
// Flow-noise shimmer gated by Gaussian mask at centre
float shimmer = flowNoise(ac * 0.8 + vec2(55.0, 33.0), t * 0.05) * 0.5 + 0.5;
float shimmerMask = exp(-(cDist * cDist) / (0.18 * asp * 0.18 * asp));
col += uGlowColor.rgb * shimmer * shimmerMask * 0.04 * uLogoGlow;
col += shimmer * shimmerMask * 0.04 * uLogoGlow;
// 48 ember particles: hash-seeded position, speed, size, brightness.
// Embers within a distance threshold of centre are deflected into an
@ -167,8 +162,8 @@ vec3 motifWelcome(vec2 uv, vec3 bg, float t)
float pX = baseX * asp + sin(t * driftFreq + fi * 1.7) * driftAmp * asp;
float pY = fract(baseY + t * riseSpeed);
float size = 0.010 + hash21(vec2(fi * 2.9, uSeed * 4.7)) * 0.014;
float bright = 0.18 + hash21(vec2(fi * 6.1, uSeed * 0.9)) * 0.32;
float size = 0.006 + hash21(vec2(fi * 2.9, uSeed * 4.7)) * 0.012;
float bright = 0.15 + hash21(vec2(fi * 6.1, uSeed * 0.9)) * 0.30;
// Fade out near top/bottom edges
float edgeFade = smoothstep(0.0, 0.12, pY) * smoothstep(1.0, 0.88, pY);
@ -237,7 +232,7 @@ vec3 motifCardDatabase(vec2 uv, vec3 bg, float t)
// Semi-transparent dark fill
float fill = smoothstep(0.015, -0.005, d);
col = mix(col, uColorB.rgb * 0.60, fill * 0.62);
col = mix(col, uColorB.rgb * 0.55, fill * 0.50);
// Accent outline
float edge = smoothstep(0.035, 0.0, abs(d));
@ -314,7 +309,7 @@ vec3 motifAccount(vec2 uv, vec3 bg, float t)
// Node glow via bloom; intensity modulated by pulse
float dist = length(ac - pos);
col += uAccent.rgb * bloom(dist, 0.018, 0.08) * mix(0.30, 0.55, pulse);
col += uAccent.rgb * bloom(dist, 0.018, 0.08) * mix(0.20, 0.45, pulse);
}
// Edges: connect nodes within a radius threshold
@ -328,19 +323,19 @@ vec3 motifAccount(vec2 uv, vec3 bg, float t)
vec2 ba = nodePos[j] - nodePos[i];
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
float lineDist = length(pa - ba * h);
col += uAccent.rgb * smoothstep(0.010, 0.0, lineDist) * strength * 0.14;
col += uAccent.rgb * smoothstep(0.010, 0.0, lineDist) * strength * 0.10;
}
}
}
// Central bloom at banner centre
float cDist = length(ac - center);
col += uAccent.rgb * bloom(cDist, 0.04, 0.25) * 0.20;
col += uAccent.rgb * bloom(cDist, 0.04, 0.25) * 0.12;
// Periodic expanding ring from centre
float ripplePhase = t * 0.4;
float rippleDist = abs(cDist - fract(ripplePhase) * asp * 0.7);
col += uAccent.rgb * smoothstep(0.02, 0.0, rippleDist) * 0.14;
col += uAccent.rgb * smoothstep(0.02, 0.0, rippleDist) * 0.10;
return col;
}
@ -461,8 +456,6 @@ void main()
else if (uMode < 4.5) col = motifPreferences(uv, bg, t);
else col = motifFinish(uv, bg, t);
// Corner vignette; uVignetteMin is scheme-driven (0.62 on dark stages,
// gentler on light ones so near-white corners don't go muddy grey).
col *= mix(uVignetteMin, 1.0, vignette(uv));
col *= mix(0.62, 1.0, vignette(uv));
fragColor = vec4(col, 1.0) * qt_Opacity;
}

View file

@ -1,42 +0,0 @@
#version 440
// The logo's gradient plate, drawn by us rather than the baked-in SVG: a
// linear blend between the two brand appColors (light AccentSoft at the
// top-left grading to dark AccentStrong at the bottom-right, mirroring
// cockatrice.svg's linearGradient4265-7-8 userSpaceOnUse axis), clipped to
// the bird's full silhouette via the full-color logo's alpha (uSilhouette).
// The white highlight path (cockatrice-logo-white) is overlaid in QML on top,
// exactly as the SVG stacks its white path over the gradient paths. Fully
// static: no glow, no breathing — the plate just sits there like the home
// widget's QPainter composite.
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf
{
mat4 qt_Matrix;
float qt_Opacity;
vec4 uStrong;
vec4 uSoft;
};
// The full-color logo's alpha channel acts as the silhouette mask: the
// gradient only appears inside the bird, exactly like the SVG's gradient paths.
layout(binding = 1) uniform sampler2D uSilhouette;
void main()
{
// Recreate cockatrice.svg's own gradient geometry (linearGradient4265-7-8,
// userSpaceOnUse): light AccentSoft at the start point S=(-8.097,-97.746),
// dark AccentStrong at the end E=(162.455,295.208), on the SVG's 300x300
// canvas. Normalized to UV space, V=E-S=(0.5685,1.3098), so
// t = dot(uv - S_norm, V)/|V|^2 with S_norm=(-0.0270,-0.3258).
float t = clamp(dot(qt_TexCoord0 - vec2(-0.02699, -0.32582), vec2(0.56851, 1.30985)) / 2.03891, 0.0, 1.0);
vec3 color = mix(uSoft.rgb, uStrong.rgb, t);
// Anti-aliased silhouette clip from the full-color logo's alpha.
float alpha = texture(uSilhouette, qt_TexCoord0).a;
fragColor = vec4(color * alpha, alpha) * qt_Opacity;
}

View file

@ -1,19 +1,13 @@
#include "printing_selector_card_overlay_widget.h"
#include "../../../client/settings/cache_settings.h"
#include "../../card_picture_loader/card_picture_loader.h"
#include "../cards/card_info_picture_widget.h"
#include "printing_selector_card_display_widget.h"
#include <QApplication>
#include <QFileDialog>
#include <QImageReader>
#include <QLabel>
#include <QMenu>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPixmapCache>
#include <QScreen>
#include <QVBoxLayout>
#include <QtMath>
#include <libcockatrice/card/database/card_database_manager.h>
@ -21,49 +15,6 @@
#include <libcockatrice/settings/card_override_settings.h>
#include <utility>
namespace
{
/**
* @brief Places the preview beside the highlighted action inside the given screen.
*
* Side-aware: hugs the side of the action that has room, aligned with its row, then
* clamps every edge so the preview always lands fully on-screen on first show.
*/
QPoint
previewPositionNear(const QRect &actionRect, const QSize &labelSize, const QRect &screenGeometry, int previewOffset)
{
const bool rightFits = actionRect.right() + previewOffset + labelSize.width() <= screenGeometry.right();
const bool leftFits = actionRect.left() - previewOffset - labelSize.width() >= screenGeometry.left();
int x;
if (rightFits) {
x = actionRect.right() + previewOffset;
} else if (leftFits) {
x = actionRect.left() - previewOffset - labelSize.width();
} else {
x = actionRect.left();
}
x = qMax(screenGeometry.left(), x);
x = qMin(screenGeometry.right() - labelSize.width() + 1, x);
const bool belowFits = actionRect.bottom() + previewOffset + labelSize.height() <= screenGeometry.bottom();
const bool aboveFits = actionRect.top() - previewOffset - labelSize.height() >= screenGeometry.top();
int y;
if (belowFits) {
y = actionRect.bottom() + previewOffset;
} else if (aboveFits) {
y = actionRect.top() - previewOffset - labelSize.height();
} else {
y = actionRect.top();
}
y = qMax(screenGeometry.top(), y);
y = qMin(screenGeometry.bottom() - labelSize.height() + 1, y);
return {x, y};
}
} // namespace
/**
* @brief Constructs a PrintingSelectorCardOverlayWidget for displaying a card overlay.
*
@ -99,31 +50,6 @@ PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *pa
initializePinBadge();
// Parent the preview to this overlay so it is destroyed with it (Qt::ToolTip keeps
// it a frameless, non-activating top-level window despite the parent).
cardOverridePreviewLabel = new QLabel(this, Qt::ToolTip);
cardOverridePreviewLabel->setWindowFlag(Qt::FramelessWindowHint);
cardOverridePreviewLabel->setAttribute(Qt::WA_ShowWithoutActivating);
cardOverridePreviewLabel->setScaledContents(true);
cardOverridePreviewLabel->hide();
// While the preview is visible, keep it honest: when the hovered printing's art
// resolves (all alternate printings share the root card's CardInfo), redraw it in place.
if (rootCard.getCardPtr()) {
connect(rootCard.getCardPtr().data(), &CardInfo::pixmapUpdated, this, [this] {
if (cardOverridePreviewLabel->isVisible()) {
refreshPreview();
}
});
}
// Alt-Tab / app-inactive must not strand the floating preview.
connect(qApp, &QGuiApplication::applicationStateChanged, this, [this](Qt::ApplicationState state) {
if (state != Qt::ApplicationActive) {
hidePreview();
}
});
// Update when this overlay emits cardPreferenceChanged or when size/scale changes
connect(this, &PrintingSelectorCardOverlayWidget::cardPreferenceChanged, this,
&PrintingSelectorCardOverlayWidget::updatePinBadgeVisibility);
@ -259,23 +185,19 @@ void PrintingSelectorCardOverlayWidget::leaveEvent(QEvent *event)
}
/**
* @brief Creates and shows the card-overlay context menu.
* @brief Creates and shows a custom context menu when the right mouse button is clicked.
*
* The menu includes the card art preference (Pin/Unpin Printing), the Image Overrides
* submenu (Load Custom Image, Clear Custom Image, and one entry per alternate printing with a
* live preview), and the Show Related cards submenu.
* The context menu includes an option to show related cards, which displays a submenu with actions
* for each related card. When an action is triggered, the card information is updated, and the
* printing selector is shown.
*
* @param point The local position the menu should pop at.
* @param point The position of the mouse when the right-click occurred.
*/
void PrintingSelectorCardOverlayWidget::customMenu(QPoint point)
{
QMenu menu;
hidePreview(); // Clear any preview state left over from a previous menu run.
// Submenus are owned by the stack-allocated top-level menu (addMenu() does not
// transfer ownership).
auto *preferenceMenu = new QMenu(tr("Preference"), &menu);
auto *preferenceMenu = new QMenu(tr("Preference"));
menu.addMenu(preferenceMenu);
const auto &preferredProviderId =
@ -297,66 +219,8 @@ void PrintingSelectorCardOverlayWidget::customMenu(QPoint point)
});
}
menu.addSeparator();
auto *overrideMenu = new QMenu(tr("Image Overrides"), &menu);
auto *loadCustomAction = overrideMenu->addAction(tr("Load Custom Image..."));
auto *clearOverrideAction = overrideMenu->addAction(tr("Clear Custom Image"));
// Nothing to clear on a card that has no local override yet.
clearOverrideAction->setEnabled(CardPictureLoader::hasLocalOverrides(rootCard));
overrideMenu->addSeparator();
const auto &allSets = rootCard.getInfo().getSets();
for (const auto &set : allSets) {
for (const auto &printing : set) {
if (printing == rootCard.getPrinting()) {
continue;
}
// The submenu is already scoped to this card, so the rows lead with set +
// collector; only printings with a distinct display name add their own name.
const CardSetPtr cardSet = printing.getSet();
if (!cardSet) {
continue;
}
QString label = tr("%1 %2").arg(cardSet->getCorrectedShortName(), printing.getProperty("num"));
auto *action = overrideMenu->addAction(label);
ExactCard overrideCard(rootCard.getCardPtr(), printing);
action->setData(QVariant::fromValue(overrideCard));
connect(action, &QAction::triggered, this, [this, overrideCard]() {
CardPictureLoader::getInstance().installPrintingOverride(rootCard, overrideCard);
QPixmapCache::clear();
rootCard.emitPixmapUpdated(); // refresh the overlay art in place, like the other paths
});
}
}
connect(clearOverrideAction, &QAction::triggered, this, [this]() {
CardPictureLoader::deleteAllLocalOverrides(rootCard);
QPixmapCache::clear();
rootCard.emitPixmapUpdated(); // force UI refresh
});
connect(loadCustomAction, &QAction::triggered, this, &PrintingSelectorCardOverlayWidget::loadCustomImage);
connect(overrideMenu, &QMenu::hovered, this, &PrintingSelectorCardOverlayWidget::showPreviewForAction);
connect(overrideMenu, &QMenu::aboutToHide, this, &PrintingSelectorCardOverlayWidget::hidePreview);
connect(overrideMenu, &QMenu::triggered, this, &PrintingSelectorCardOverlayWidget::hidePreview);
menu.addMenu(overrideMenu);
menu.addSeparator();
// filling out the related cards submenu
auto *relatedMenu = new QMenu(tr("Show Related cards"), &menu);
auto *relatedMenu = new QMenu(tr("Show Related cards"));
menu.addMenu(relatedMenu);
auto relatedCards = rootCard.getInfo().getAllRelatedCards();
if (relatedCards.isEmpty()) {
@ -371,11 +235,7 @@ void PrintingSelectorCardOverlayWidget::customMenu(QPoint point)
});
}
}
// The preview anchors itself to this popup's global geometry while it is open, so the
// pointer must stay valid for the whole exec() and be dropped before the stack unwinds.
previewSourceMenu = overrideMenu;
menu.exec(this->mapToGlobal(point));
previewSourceMenu = nullptr;
}
/**
@ -431,136 +291,3 @@ void PrintingSelectorCardOverlayWidget::initializePinBadge()
pinBadge->setVisible(false);
pinBadge->raise();
}
/**
* @brief Asks for an image file and installs it as the card's custom art.
*
* Unreadable files answer with a visible warning instead of a silent no-op.
*/
void PrintingSelectorCardOverlayWidget::loadCustomImage()
{
QString filePath = QFileDialog::getOpenFileName(this, tr("Select Card Image"), QString(),
tr("Images (*.png *.jpg *.jpeg *.webp)"));
if (filePath.isEmpty()) {
return;
}
QPixmap pixmap(filePath);
if (pixmap.isNull()) {
// No silent paths: a file that cannot be read answers visibly instead of a no-op.
QMessageBox::warning(this, tr("Load Custom Image"), tr("The selected file could not be read as an image."));
return;
}
CardPictureLoader::getInstance().saveCardImageToLocalStorage(rootCard, pixmap, true);
QPixmapCache::clear();
rootCard.emitPixmapUpdated();
}
/**
* @brief Shows the hover preview for a highlighted printing entry in the Image Overrides submenu.
*
* QMenu::hovered fires on keyboard highlight too, so the preview appears when arrows walk
* onto a printing entry, not only under the mouse.
*
* Non-printing entries (e.g., Load Custom Image, Clear Custom Image) hide the preview.
*
* @param action The action that was highlighted.
*/
void PrintingSelectorCardOverlayWidget::showPreviewForAction(QAction *action)
{
if (!action) {
hidePreview();
return;
}
const QVariant data = action->data();
if (!data.canConvert<ExactCard>()) {
hidePreview();
return;
}
const ExactCard previewCard = qvariant_cast<ExactCard>(data);
if (previewCard.isEmpty()) {
hidePreview();
return;
}
hoveredOverrideCard = previewCard;
hoveredOverrideAction = action;
refreshPreview();
}
/**
* @brief Renders the hover preview for the currently highlighted printing.
*
* The preview shows the loading placeholder while the art is pending and swaps in the real
* art when it resolves. The label is positioned against its already-resized geometry so the
* first-ever show at the screen's edges stays fully on-screen.
*/
void PrintingSelectorCardOverlayWidget::refreshPreview()
{
if (hoveredOverrideCard.isEmpty()) {
hidePreview();
return;
}
constexpr QSize previewSize(240, 336);
constexpr int previewOffset = 20;
QPixmap pixmap;
CardPictureLoader::getPixmap(pixmap, hoveredOverrideCard, previewSize);
if (pixmap.isNull()) {
// Keep the preview honest while loading: show the loading placeholder instead of a void.
// Fetch at the logical size and let the label scale it, so the placeholder matches the
// real art's footprint rather than doubling on HiDPI displays.
CardPictureLoader::getCardBackLoadingInProgressPixmap(pixmap, previewSize);
}
cardOverridePreviewLabel->setPixmap(pixmap);
// QPixmap::size() is physical pixels; the label layout must use the device-independent size
// so the preview keeps a constant footprint across DPI settings (QScreen geometry is logical).
const QSize labelSize = pixmap.deviceIndependentSize().toSize();
cardOverridePreviewLabel->resize(labelSize);
// Anchor the preview to the walked submenu popup rather than QCursor::pos(), which is idle
// under keyboard-only operation: a keyboard-highlighted row must preview at the same place as
// a hovered one. The mouse path is unchanged in effect — the popup sits under the cursor, so
// the preview stays beside the row in both modalities.
const QMenu *popup = previewSourceMenu;
if (!popup || !popup->isVisible() || !hoveredOverrideAction) {
hidePreview();
return;
}
const QRect popupGeometry = popup->geometry();
const QRect actionRectLocal = popup->actionGeometry(hoveredOverrideAction);
const QRect actionRect(popupGeometry.topLeft() + actionRectLocal.topLeft(), actionRectLocal.size());
QScreen *screen = QGuiApplication::screenAt(popupGeometry.center());
if (!screen) {
hidePreview();
return;
}
const QRect &screenGeometry = screen->geometry();
cardOverridePreviewLabel->move(previewPositionNear(actionRect, labelSize, screenGeometry, previewOffset));
cardOverridePreviewLabel->show();
}
/**
* @brief Hides the hover preview and forgets the currently highlighted printing.
*/
void PrintingSelectorCardOverlayWidget::hidePreview()
{
hoveredOverrideCard = ExactCard();
hoveredOverrideAction = nullptr;
if (cardOverridePreviewLabel) {
cardOverridePreviewLabel->hide();
}
}

View file

@ -13,9 +13,6 @@
#include <libcockatrice/models/deck_list/deck_list_model.h>
class QAction;
class QMenu;
class PrintingSelectorCardOverlayWidget : public QWidget
{
Q_OBJECT
@ -46,19 +43,11 @@ private slots:
private:
void initializePinBadge();
void loadCustomImage();
void showPreviewForAction(QAction *action);
void refreshPreview();
void hidePreview();
CardInfoPictureWidget *cardInfoPicture;
AllZonesCardAmountWidget *allZonesCardAmountWidget;
QLabel *pinBadge = nullptr;
AbstractTabDeckEditor *deckEditor;
ExactCard rootCard;
QLabel *cardOverridePreviewLabel = nullptr;
ExactCard hoveredOverrideCard;
QMenu *previewSourceMenu = nullptr;
QAction *hoveredOverrideAction = nullptr;
};
#endif // PRINTING_SELECTOR_CARD_OVERLAY_WIDGET_H

View file

@ -369,7 +369,7 @@ void GameSelector::joinGame(const ServerInfo_Game &game, const bool asSpectator,
return;
}
bool overrideRestrictions = tabSupervisor->canOverrideGameRestrictions();
bool overrideRestrictions = !tabSupervisor->getAdminLocked();
// Joining a full game without override privileges silently becomes a
// spectator join, so ask first instead of surprising the player.
@ -462,7 +462,7 @@ void GameSelector::enableButtonsForIndex(const QModelIndex &current)
}
const ServerInfo_Game &game = gameListModel->getGame(current.data(Qt::UserRole).toInt());
bool overrideRestrictions = tabSupervisor->canOverrideGameRestrictions();
bool overrideRestrictions = !tabSupervisor->getAdminLocked();
spectateButton->setEnabled(game.spectators_allowed() || overrideRestrictions);
joinButton->setEnabled(game.player_count() < game.max_players() || overrideRestrictions);

View file

@ -113,7 +113,7 @@ int RemoteDeckList_TreeModel::rowCount(const QModelIndex &parent) const
int RemoteDeckList_TreeModel::columnCount(const QModelIndex & /*parent*/) const
{
return 4;
return 3;
}
QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) const
@ -121,7 +121,7 @@ QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) cons
if (!index.isValid()) {
return QVariant();
}
if (index.column() >= 4) {
if (index.column() >= 3) {
return QVariant();
}
@ -134,29 +134,12 @@ QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) cons
switch (index.column()) {
case 0:
return node->getName();
case 3:
// Report the node's own bit, not the inherited effective
// state, so it stays in step with what publishing toggles.
if (node->isPublic()) {
return tr("Public");
}
return isEffectivelyPublic(node) ? tr("Public (inherited)") : tr("Private");
default:
return QVariant();
}
}
case Qt::DecorationRole:
return index.column() == 0 ? dirIcon : QVariant();
case Qt::ToolTipRole:
if (index.column() == 3) {
if (node->isPublic()) {
return tr("This folder is visible to other users");
}
return isEffectivelyPublic(node)
? tr("This folder is private, but a parent folder is public (inherited).")
: tr("This folder is only visible to you");
}
return QVariant();
default:
return QVariant();
}
@ -170,13 +153,6 @@ QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) cons
return file->getId();
case 2:
return file->getUploadTime();
case 3:
// Report the node's own bit, not the inherited effective
// state, so it stays in step with what publishing toggles.
if (file->isPublic()) {
return tr("Public");
}
return isEffectivelyPublic(file) ? tr("Public (inherited)") : tr("Private");
default:
return QVariant();
}
@ -185,16 +161,6 @@ QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) cons
return index.column() == 0 ? fileIcon : QVariant();
case Qt::TextAlignmentRole:
return index.column() == 1 ? Qt::AlignRight : Qt::AlignLeft;
case Qt::ToolTipRole:
if (index.column() == 3) {
if (file->isPublic()) {
return tr("This deck is visible to other users");
}
return isEffectivelyPublic(file)
? tr("This deck is private, but a parent folder is public (inherited).")
: tr("This deck is only visible to you");
}
return QVariant();
default:
return QVariant();
}
@ -217,8 +183,6 @@ QVariant RemoteDeckList_TreeModel::headerData(int section, Qt::Orientation orien
return tr("ID");
case 2:
return tr("Upload time");
case 3:
return tr("Visibility");
default:
return QVariant();
}
@ -275,14 +239,13 @@ void RemoteDeckList_TreeModel::addFileToTree(const ServerInfo_DeckStorage_TreeIt
time.setSecsSinceEpoch(fileInfo.creation_time());
beginInsertRows(nodeToIndex(parent), parent->size(), parent->size());
parent->append(new FileNode(QString::fromStdString(file.name()), file.id(), time, parent, fileInfo.is_public()));
parent->append(new FileNode(QString::fromStdString(file.name()), file.id(), time, parent));
endInsertRows();
}
void RemoteDeckList_TreeModel::addFolderToTree(const ServerInfo_DeckStorage_TreeItem &folder, DirectoryNode *parent)
{
DirectoryNode *newItem = addNamedFolderToTree(QString::fromStdString(folder.name()), parent);
newItem->setIsPublic(folder.folder().is_public());
const ServerInfo_DeckStorage_Folder &folderInfo = folder.folder();
const int folderItemsSize = folderInfo.items_size();
for (int i = 0; i < folderItemsSize; ++i) {
@ -322,21 +285,6 @@ void RemoteDeckList_TreeModel::refreshTree()
client->sendCommand(pend);
}
bool RemoteDeckList_TreeModel::isEffectivelyPublic(const Node *node) const
{
if (node == nullptr || node == root) {
return false;
}
const Node *current = node;
while (current != nullptr) {
if (current->isPublic()) {
return true;
}
current = current->getParent();
}
return false;
}
void RemoteDeckList_TreeModel::clearTree()
{
beginResetModel();

View file

@ -27,11 +27,9 @@ public:
protected:
DirectoryNode *parent;
QString name;
bool publicFlag;
public:
explicit Node(const QString &_name, DirectoryNode *_parent = nullptr)
: parent(_parent), name(_name), publicFlag(false)
explicit Node(const QString &_name, DirectoryNode *_parent = nullptr) : parent(_parent), name(_name)
{
}
virtual ~Node() = default;
@ -43,14 +41,6 @@ public:
{
return name;
}
[[nodiscard]] bool isPublic() const
{
return publicFlag;
}
void setIsPublic(bool _public)
{
publicFlag = _public;
}
};
class DirectoryNode : public Node, public QList<Node *>
{
@ -69,14 +59,9 @@ public:
QDateTime uploadTime;
public:
FileNode(const QString &_name,
int _id,
const QDateTime &_uploadTime,
DirectoryNode *_parent = nullptr,
bool _isPublic = false)
FileNode(const QString &_name, int _id, const QDateTime &_uploadTime, DirectoryNode *_parent = nullptr)
: Node(_name, _parent), id(_id), uploadTime(_uploadTime)
{
setIsPublic(_isPublic);
}
[[nodiscard]] int getId() const
{
@ -124,11 +109,6 @@ public:
{
return root;
}
/**
* @brief Whether a node is visible to other users (own flag or inherited
* from any ancestor folder).
*/
[[nodiscard]] bool isEffectivelyPublic(const Node *node) const;
void addFileToTree(const ServerInfo_DeckStorage_TreeItem &file, DirectoryNode *parent);
void addFolderToTree(const ServerInfo_DeckStorage_TreeItem &folder, DirectoryNode *parent);
DirectoryNode *addNamedFolderToTree(const QString &name, DirectoryNode *parent);

View file

@ -37,7 +37,6 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent,
aDetails = new QAction(QString(), this);
aChat = new QAction(QString(), this);
aShowGames = new QAction(QString(), this);
aViewPublicDecks = new QAction(QString(), this);
aAddToBuddyList = new QAction(QString(), this);
aRemoveFromBuddyList = new QAction(QString(), this);
aAddToIgnoreList = new QAction(QString(), this);
@ -65,7 +64,6 @@ void UserContextMenu::retranslateUi()
aDetails->setText(tr("User &details"));
aChat->setText(tr("Private &chat"));
aShowGames->setText(tr("Show this user's &games"));
aViewPublicDecks->setText(tr("View this user's &public decks"));
aAddToBuddyList->setText(tr("Add to &buddy list"));
aRemoveFromBuddyList->setText(tr("Remove from &buddy list"));
aAddToIgnoreList->setText(tr("Add to &ignore list"));
@ -378,9 +376,6 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
}
menu->addAction(aDetails);
menu->addAction(aShowGames);
if (userLevel.testFlag(ServerInfo_User::IsRegistered)) {
menu->addAction(aViewPublicDecks);
}
menu->addAction(aChat);
const QList<GameInviteOption> inviteOptions = inviteOptionsForUser(userName);
if (!inviteOptions.isEmpty()) {
@ -460,7 +455,6 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
aChat->setEnabled(anotherUser && online && !userListProxy->isUserIgnored(userName));
aShowGames->setEnabled(online);
aReport->setEnabled(anotherUser);
aViewPublicDecks->setEnabled(anotherUser);
aAddToBuddyList->setEnabled(anotherUser);
aRemoveFromBuddyList->setEnabled(anotherUser);
aAddToIgnoreList->setEnabled(anotherUser);
@ -487,8 +481,6 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
execChat(userName);
} else if (actionClicked == aShowGames) {
execShowGames(userName);
} else if (actionClicked == aViewPublicDecks) {
execViewPublicDecks(userName);
} else if (actionClicked == aAddToBuddyList) {
execAddToBuddy(userName);
} else if (actionClicked == aRemoveFromBuddyList) {
@ -612,11 +604,6 @@ void UserContextMenu::execShowGames(const QString &userName)
client->sendCommand(pend);
}
void UserContextMenu::execViewPublicDecks(const QString &userName)
{
tabSupervisor->openTabPublicDecks(userName);
}
void UserContextMenu::execAddToBuddy(const QString &userName)
{
Command_AddToList cmd;

View file

@ -37,7 +37,6 @@ private:
QAction *aUserName;
QAction *aDetails;
QAction *aShowGames;
QAction *aViewPublicDecks;
QAction *aChat;
QAction *aAddToBuddyList, *aRemoveFromBuddyList;
QAction *aAddToIgnoreList, *aRemoveFromIgnoreList;
@ -112,7 +111,6 @@ public:
void execInvite(const QString &userName);
void execDetails(const QString &userName);
void execShowGames(const QString &userName);
void execViewPublicDecks(const QString &userName);
void execAddToBuddy(const QString &userName);
void execRemoveFromBuddy(const QString &userName);
void execAddToIgnore(const QString &userName);

View file

@ -59,8 +59,8 @@ AppearanceSettingsPage::AppearanceSettingsPage()
connect(&schemeCombo, &QComboBox::currentIndexChanged, this,
[this] { themeManager->setColorScheme(schemeCombo.currentData().toString()); });
// Qt widget style; "System" lets the application decide
styleCombo.addItem(tr("System"), QStringLiteral("System"));
// Qt widget style; "Default" lets the application decide
styleCombo.addItem(tr("Default"), QStringLiteral("Default"));
for (const QString &key : QStyleFactory::keys()) {
styleCombo.addItem(key, key);
}
@ -132,10 +132,6 @@ AppearanceSettingsPage::AppearanceSettingsPage()
connect(&homeTabDisplayCardNameCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(),
&AppearanceSettings::setHomeTabDisplayCardName);
homeTabBackgroundDimCheckBox.setChecked(settings.appearance().getHomeTabBackgroundDim());
connect(&homeTabBackgroundDimCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(),
&AppearanceSettings::setHomeTabBackgroundDim);
for (const auto &entry : HomeTabButtonColor::all()) {
homeTabButtonColorSourceBox.addItem(QObject::tr(entry.trKey));
}
@ -154,7 +150,6 @@ AppearanceSettingsPage::AppearanceSettingsPage()
homeTabGrid->addWidget(&homeTabDisplayCardNameCheckBox, 2, 0, 1, 2);
homeTabGrid->addWidget(&homeTabButtonColorSourceLabel, 3, 0);
homeTabGrid->addWidget(&homeTabButtonColorSourceBox, 3, 1);
homeTabGrid->addWidget(&homeTabBackgroundDimCheckBox, 4, 0, 1, 2);
homeTabGroupBox = new QGroupBox;
homeTabGroupBox->setLayout(homeTabGrid);
@ -513,12 +508,9 @@ void AppearanceSettingsPage::retranslateUi()
homeTabBackgroundShuffleFrequencyLabel.setText(tr("Home tab background shuffle frequency:"));
homeTabBackgroundShuffleFrequencySpinBox.setSpecialValueText(tr("Disabled"));
homeTabDisplayCardNameCheckBox.setText(tr("Display card name of background in bottom right"));
homeTabBackgroundDimCheckBox.setText(tr("Dim the home tab background"));
homeTabBackgroundDimCheckBox.setToolTip(
tr("Draw a translucent overlay over the home tab background so buttons and text stand out"));
homeTabButtonColorSourceLabel.setText(tr("Home tab button color:"));
homeTabButtonColorSourceBox.setToolTip(
tr("Use the theme's identity accent colors, or extract colors from the background image"));
tr("Automatic: extract from background if present, otherwise use theme default"));
playmatGroupBox->setTitle(tr("Playmat settings"));
playmatVisibilityLabel.setText(tr("Playmat visibility:"));

View file

@ -41,7 +41,6 @@ private:
QLabel homeTabBackgroundShuffleFrequencyLabel;
QSpinBox homeTabBackgroundShuffleFrequencySpinBox;
QCheckBox homeTabDisplayCardNameCheckBox;
QCheckBox homeTabBackgroundDimCheckBox;
QLabel homeTabButtonColorSourceLabel;
QComboBox homeTabButtonColorSourceBox;

View file

@ -1,21 +1,15 @@
#include "general_settings_page.h"
#include "../../../client/settings/cache_settings.h"
#include "../interface/card_picture_loader/card_picture_loader.h"
#include "../main.h"
#include "../server/user/user_info_connection.h"
#include "update/client/release_channel.h"
#include <QCoreApplication>
#include <QFile>
#include <QFileDialog>
#include <QGridLayout>
#include <QLineEdit>
#include <QMessageBox>
#include <QTranslator>
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/download_settings.h>
#include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/personal_settings.h>
#include <libcockatrice/settings/tabs_settings.h>
@ -52,27 +46,10 @@ GeneralSettingsPage::GeneralSettingsPage()
connect(&languageBox, qOverload<int>(&QComboBox::currentIndexChanged), this,
&GeneralSettingsPage::languageBoxChanged);
// card text & images language, independent of the UI language
cardLanguageBox.addItem(tr("English"), "en");
for (const QString &code : CardLocalization::supportedLanguages()) {
cardLanguageBox.addItem(CardLocalization::languageDisplayName(code), code);
}
const int cardLangIndex = cardLanguageBox.findData(SettingsCache::instance().cardsDisplay().getCardLang());
cardLanguageBox.setCurrentIndex(cardLangIndex < 0 ? 0 : cardLangIndex);
connect(&cardLanguageBox, qOverload<int>(&QComboBox::currentIndexChanged), this,
&GeneralSettingsPage::cardLanguageBoxChanged);
auto *languageGrid = new QGridLayout;
languageGrid->addWidget(&languageLabel, 0, 0);
languageGrid->addWidget(&languageBox, 0, 1);
languageGrid->addWidget(&cardLanguageLabel, 1, 0);
languageGrid->addWidget(&cardLanguageBox, 1, 1);
languageGrid->addWidget(&cardLanguageNoteLabel, 2, 1);
languageGrid->addWidget(&advertiseTranslationPageLabel, 3, 1, Qt::AlignRight);
cardLanguageNoteLabel.setWordWrap(true);
cardLanguageNoteLabel.setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
languageGrid->addWidget(&advertiseTranslationPageLabel, 1, 1, Qt::AlignRight);
languageGroupBox = new QGroupBox;
languageGroupBox->setLayout(languageGrid);
@ -435,52 +412,6 @@ void GeneralSettingsPage::languageBoxChanged(int index)
SettingsCache::instance().personal().setLang(languageBox.itemData(index).toString());
}
void GeneralSettingsPage::cardLanguageBoxChanged(int index)
{
const QString lang = cardLanguageBox.itemData(index).toString();
SettingsCache::instance().cardsDisplay().setCardLang(lang);
// Switching to a non-default language only takes effect after the card
// database is re-imported with that language selected; English data is always
// present, so switching back to English needs no prompt.
if (lang == "en") {
return;
}
// The binary cache does not track the language its entries were imported in,
// and the downloaded pictures were fetched with English art names, so both are
// stale until Oracle re-imports the database in the new language: drop them.
QFile::remove(SettingsCache::instance().getCardDatabasePath() + ".cache");
CardPictureLoader::clearNetworkCache();
CardPictureLoader::clearPixmapCache();
// Art is resolved by the translated card name for non-English languages, so the
// matching Scryfall URL is added to the top of the download list. It stays
// visible in the deck editor settings, where it can be removed or reordered.
const bool localizedUrlAdded = SettingsCache::instance().downloads().addLocalizedScryfallUrl();
QString message = tr("<p>The card database only contains English card data. To see cards in <b>%1</b>, "
"<b>Oracle</b> must run once with this language selected and re-import the card "
"database.</p>"
"<p>The cached database and the downloaded card pictures have been cleared, so a "
"re-import is picked up without stale entries.</p>")
.arg(cardLanguageBox.itemText(index));
if (localizedUrlAdded) {
message += tr("<p>The Scryfall URL that resolves card art by translated name was added to the top of your "
"download list. You can remove or reorder it any time.</p>");
}
message += tr("<p>Run Oracle now?</p>");
const QMessageBox::StandardButton answer = QMessageBox::question(
this, tr("Card text & images language changed"), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
// The answer only controls whether Oracle starts right away; the caches stay
// cleared so the next import or launch rebuilds them in the new language.
if (answer == QMessageBox::Yes) {
emit cardDatabaseUpdateRequested();
}
}
void GeneralSettingsPage::updateStartupServerControlsVisibility()
{
const int index = startupTabSelector.currentIndex();
@ -498,10 +429,6 @@ void GeneralSettingsPage::retranslateUi()
languageGroupBox->setTitle(tr("Language settings"));
languageLabel.setText(tr("Language:"));
cardLanguageBox.setItemText(0, tr("English"));
cardLanguageLabel.setText(tr("Card text & images language:"));
cardLanguageNoteLabel.setText(
tr("Foreign card names, text and art apply after you update the card database (Oracle)."));
advertiseTranslationPageLabel.setText(
QString("<a href='%1'>%2</a>").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations")));

View file

@ -23,10 +23,6 @@ public:
static QStringList findQmFiles();
static QString languageName(const QString &lang);
signals:
/// Request to re-import the card database with the newly selected card language
void cardDatabaseUpdateRequested();
private slots:
void deckPathButtonClicked();
void filtersPathButtonClicked();
@ -37,7 +33,6 @@ private slots:
void tokenDatabasePathButtonClicked();
void resetAllPathsClicked();
void languageBoxChanged(int index);
void cardLanguageBoxChanged(int index);
void updateStartupServerControlsVisibility();
private:
@ -51,10 +46,6 @@ private:
QComboBox languageBox;
QLabel advertiseTranslationPageLabel;
QLabel cardLanguageLabel;
QComboBox cardLanguageBox;
QLabel cardLanguageNoteLabel;
QLabel updateReleaseChannelLabel;
QComboBox updateReleaseChannelBox;
QCheckBox startupUpdateCheckCheckBox;

View file

@ -11,7 +11,6 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../cards/additional_info/deck_color_identity.h"
#include "../client/network/interfaces/deck_stats_interface.h"
#include "../client/network/interfaces/tapped_out_interface.h"
#include "../deck_editor/deck_state_manager.h"
@ -20,7 +19,6 @@
#include "../interface/widgets/dialogs/dlg_load_deck.h"
#include "../interface/widgets/dialogs/dlg_load_deck_from_clipboard.h"
#include "../interface/widgets/dialogs/dlg_load_deck_from_website.h"
#include "../interface/widgets/dialogs/dlg_share_deck.h"
#include "../utility/visibility_change_listener.h"
#include "tab_supervisor.h"
@ -325,7 +323,6 @@ bool AbstractTabDeckEditor::actSaveDeck()
Command_DeckUpload cmd;
cmd.set_deck_id(static_cast<google::protobuf::uint32>(loadedDeck.lastLoadInfo.remoteDeckId));
cmd.set_deck_list(deckString.toStdString());
cmd.set_color_identity(getDeckColorIdentity(loadedDeck.deckList, CardDatabaseManager::query()).toStdString());
PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, &AbstractTabDeckEditor::saveDeckRemoteFinished);
@ -385,27 +382,6 @@ bool AbstractTabDeckEditor::actSaveDeckAs()
return true;
}
/**
* @brief Opens the deck share dialog with the current deck preselected.
*/
void AbstractTabDeckEditor::actShareDeck()
{
AbstractClient *client = tabSupervisor->getServerClient();
if (client->getStatus() != StatusLoggedIn) {
QMessageBox::information(this, tr("Share deck"), tr("You must be connected to the server to share a deck."));
return;
}
const QSharedPointer<DeckList> deck = deckStateManager->getDeckListShared();
if (deck->isBlankDeck()) {
QMessageBox::information(this, tr("Share deck"), tr("The deck is empty. Add cards before sharing it."));
return;
}
DlgShareDeck shareDialog(client, deck, this);
shareDialog.exec();
}
/**
* @brief Callback for remote deck save completion.
* @param response Server response.

Some files were not shown because too many files have changed in this diff Show more