Merge branch 'master' into tooomm-qt5

This commit is contained in:
tooomm 2026-09-19 16:05:00 +02:00 committed by GitHub
commit b3813c202b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
162 changed files with 4862 additions and 653 deletions

View file

@ -403,6 +403,7 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp 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/tabs/api/commander_spellbook/handle_commander_brackets.cpp
src/interface/widgets/onboarding/banner_shader_config.h 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.cpp
src/interface/widgets/onboarding/first_run_wizard.h src/interface/widgets/onboarding/first_run_wizard.h
src/interface/widgets/onboarding/first_run_wizard_page.cpp src/interface/widgets/onboarding/first_run_wizard_page.cpp
@ -530,6 +531,7 @@ qt6_add_shaders(
"src/interface/widgets/onboarding/shaders" "src/interface/widgets/onboarding/shaders"
FILES FILES
src/interface/widgets/onboarding/shaders/brand_banner.frag src/interface/widgets/onboarding/shaders/brand_banner.frag
src/interface/widgets/onboarding/shaders/brand_plate.frag
) )
qt6_add_resources( qt6_add_resources(

View file

@ -63,6 +63,8 @@
<file>resources/icons/mana/W.svg</file> <file>resources/icons/mana/W.svg</file>
<file>resources/backgrounds/home.png</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/card_triplet.svg</file>
<file>resources/backgrounds/placeholder_printing_selector.svg</file> <file>resources/backgrounds/placeholder_printing_selector.svg</file>
@ -363,6 +365,8 @@
<file>resources/usericons/pawn_single.svg</file> <file>resources/usericons/pawn_single.svg</file>
<file>resources/usericons/pawn_double.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_single.svg</file>
<file>resources/usericons/pawn_donator_double.svg</file> <file>resources/usericons/pawn_donator_double.svg</file>
<file>resources/usericons/pawn_judge_single.svg</file> <file>resources/usericons/pawn_judge_single.svg</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

After

Width:  |  Height:  |  Size: 16 MiB

Before After
Before After

View file

@ -0,0 +1,343 @@
<?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>

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,211 @@
<?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>

After

Width:  |  Height:  |  Size: 9.1 KiB

View file

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

View file

@ -0,0 +1,59 @@
#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 <QApplication>
#include <QBuffer> #include <QBuffer>
#include <QDebug> #include <QDebug>
#include <QDirIterator> #include <QDir>
#include <QFileInfo> #include <QFileInfo>
#include <QMainWindow> #include <QMainWindow>
#include <QMovie> #include <QMovie>
@ -20,6 +20,7 @@
#include <QThread> #include <QThread>
#include <algorithm> #include <algorithm>
#include <libcockatrice/settings/cache_storage_settings.h> #include <libcockatrice/settings/cache_storage_settings.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/download_settings.h> #include <libcockatrice/settings/download_settings.h>
#include <libcockatrice/settings/paths_settings.h> #include <libcockatrice/settings/paths_settings.h>
#include <utility> #include <utility>
@ -37,8 +38,10 @@ CardPictureLoader::CardPictureLoader() : QObject(nullptr)
&CardPictureLoader::picsPathChanged); &CardPictureLoader::picsPathChanged);
connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this, connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this,
&CardPictureLoader::picDownloadChanged); &CardPictureLoader::picDownloadChanged);
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
&CardPictureLoader::cardLangChanged);
qRegisterMetaType<ExactCard>(); qRegisterMetaType<ExactCard>("ExactCard");
connect(worker, &CardPictureLoaderWorker::imageLoaded, this, &CardPictureLoader::imageLoaded); connect(worker, &CardPictureLoaderWorker::imageLoaded, this, &CardPictureLoader::imageLoaded);
statusBar = new CardPictureLoaderStatusBar(nullptr); statusBar = new CardPictureLoaderStatusBar(nullptr);
@ -206,7 +209,49 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
card.emitPixmapUpdated(); card.emitPixmapUpdated();
} }
void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap) 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)
{ {
if (pixmap.isNull() || !card) { if (pixmap.isNull() || !card) {
return; return;
@ -266,8 +311,9 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const
QFileInfo outInfo(baseDir.filePath(relativePath)); QFileInfo outInfo(baseDir.filePath(relativePath));
// Do not overwrite existing files // Automatic cache writes (FILESYSTEM_CACHE) must never clobber an explicit user override.
if (outInfo.exists()) { // Only the explicit override paths pass allowOverwrite == true.
if (!allowOverwrite && outInfo.exists()) {
return; return;
} }
@ -288,6 +334,122 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const
} }
} }
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() void CardPictureLoader::clearPixmapCache()
{ {
QPixmapCache::clear(); QPixmapCache::clear();
@ -327,25 +489,11 @@ void CardPictureLoader::picsPathChanged()
QPixmapCache::clear(); QPixmapCache::clear();
} }
bool CardPictureLoader::hasCustomArt() void CardPictureLoader::cardLangChanged()
{ {
auto picsPath = SettingsCache::instance().paths().getPicsPath(); // Localized images are fetched via a different URL, but the in-memory
QDirIterator it(picsPath, QDir::Dirs | QDir::NoDotAndDotDot); // pixmap cache is keyed by card name/uuid, so drop everything cached
// (including failure timestamps) to force a reload in the new language.
// Check if there is at least one non-directory file in the pics path, other QPixmapCache::clear();
// than in the "downloadedPics" subdirectory. failedAt.clear();
while (it.hasNext()) {
QFileInfo dir(it.nextFileInfo());
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,10 +97,17 @@ public:
static void cacheCardPixmaps(const QList<ExactCard> &cards); static void cacheCardPixmaps(const QList<ExactCard> &cards);
/** /**
* @brief Check if the user has custom card art in the picsPath directory. * @brief Check if a local override image already exists for the card.
* @return True if any custom art exists. * @param card The card to check.
* @return True if the card has at least one locally stored override image.
*/ */
static bool hasCustomArt(); 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);
/** /**
* @brief Clears the in-memory QPixmap cache for all cards. * @brief Clears the in-memory QPixmap cache for all cards.
@ -120,7 +127,9 @@ public slots:
* @param image Loaded QImage. * @param image Loaded QImage.
*/ */
void imageLoaded(const ExactCard &card, const QImage &image); void imageLoaded(const ExactCard &card, const QImage &image);
void saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap); 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);
private slots: private slots:
/** /**
@ -134,6 +143,12 @@ private slots:
* Clears the QPixmap cache to reload images. * Clears the QPixmap cache to reload images.
*/ */
void picsPathChanged(); 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 #endif

View file

@ -94,6 +94,10 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
candidatePaths << picsPath + "/downloadedPics/" + setName + "/" + nameVariant; 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) { for (const QString &path : candidatePaths) {
QFileInfo fileInfo(path); QFileInfo fileInfo(path);
QDir dir = fileInfo.dir(); QDir dir = fileInfo.dir();
@ -105,7 +109,8 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName,
QStringList files = dir.entryList(QDir::Files); QStringList files = dir.entryList(QDir::Files);
for (const QString &file : files) { for (const QString &file : files) {
if (!file.startsWith(baseName)) { QFileInfo fi(file);
if (fi.completeBaseName() != baseName) {
continue; continue;
} }

View file

@ -84,8 +84,8 @@ void CardPictureLoaderWorker::queueRequest(const QUrl &url, CardPictureLoaderWor
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) == SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE && CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
cache->metaData(url).isValid()) { cache->metaData(url).isValid()) {
// If we hit a cached url, we get to make the request for free, since it won't contribute towards the // A request that will be served from the disk cache never touches the network and therefore
// rate-limit // doesn't use up any of the rate limit, so it gets to skip the queue.
makeRequest(url, worker); makeRequest(url, worker);
return; return;
} }
@ -107,10 +107,13 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/,/*;q=0.8"); req.setRawHeader("Accept", "image/avif,image/webp,image/apng,image/,/*;q=0.8");
bool useNetworkCache = // Cached entries are served straight from the disk cache even when picture downloads are
!picDownload && static_cast<CardPictureLoaderCacheMethod::CacheMethod>( // enabled: re-fetching an already-cached image would burn the rate limit for nothing. Only a
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) == // genuine cache miss goes to the network, and only when downloads are enabled.
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE; bool useNetworkCache = static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
CardPictureLoaderCacheMethod::CacheMethod::NETWORK_CACHE &&
(cache->metaData(url).isValid() || !picDownload);
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, req.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
useNetworkCache ? QNetworkRequest::AlwaysCache : QNetworkRequest::AlwaysNetwork); useNetworkCache ? QNetworkRequest::AlwaysCache : QNetworkRequest::AlwaysNetwork);

View file

@ -94,7 +94,8 @@ void CardPictureToLoad::populateSetUrls()
} }
} }
for (const QString &urlTemplate : urlTemplates) { const QStringList orderedTemplates = urlTemplates;
for (const QString &urlTemplate : orderedTemplates) {
QString transformedUrl = transformUrl(urlTemplate); QString transformedUrl = transformUrl(urlTemplate);
if (!transformedUrl.isEmpty()) { if (!transformedUrl.isEmpty()) {
@ -282,8 +283,15 @@ QString CardPictureToLoad::transformUrl(const QString &urlTemplate) const
} }
// language setting // language setting
transformMap["!sflang!"] = QString(QCoreApplication::translate( const QString cardLang = SettingsCache::instance().cardsDisplay().getCardLang();
"PictureLoader", "en", "code for scryfall's language property, not available for all languages")); 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);
}
QString transformedUrl = urlTemplate; QString transformedUrl = urlTemplate;
for (const QString &prop : transformMap.keys()) { for (const QString &prop : transformMap.keys()) {

View file

@ -1,6 +1,5 @@
#include "palette_editor_dialog.h" #include "palette_editor_dialog.h"
#include "../../client/settings/cache_settings.h"
#include "../theme_manager.h" #include "../theme_manager.h"
#include "palette_generator.h" #include "palette_generator.h"
#include "palette_grid_widget.h" #include "palette_grid_widget.h"
@ -11,31 +10,11 @@
#include <QDialogButtonBox> #include <QDialogButtonBox>
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
#include <QFileInfo>
#include <QFrame> #include <QFrame>
#include <QGuiApplication>
#include <QLabel> #include <QLabel>
#include <QLoggingCategory>
#include <QMessageBox> #include <QMessageBox>
#include <QPushButton> #include <QPushButton>
#include <QStyleHints>
#include <QTimer> #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) PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QString &_themeName, QWidget *parent)
: QDialog(parent), themeDirPath(_themeDirPath), themeName(_themeName) : QDialog(parent), themeDirPath(_themeDirPath), themeName(_themeName)
@ -46,14 +25,7 @@ PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QSt
// Resolve a writable directory for saving. Built-in (Default / Fusion) and // Resolve a writable directory for saving. Built-in (Default / Fusion) and
// other read-only theme directories must be customised in the user-writable // other read-only theme directories must be customised in the user-writable
// themes directory; otherwise the write would fail or be lost on upgrade. // themes directory; otherwise the write would fail or be lost on upgrade.
if (!themeDirPath.isEmpty() && isDirReallyWritable(themeDirPath)) { saveDir = ThemeManager::writableThemeDir(themeName);
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 // Load both scheme configs upfront so switching is instant
loadSchemes(); loadSchemes();
@ -214,7 +186,7 @@ void PaletteEditorDialog::retranslateUi()
resetBtn->setToolTip(tr("Discard unsaved edits and restore the last saved palette")); 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())); saveBtn->setToolTip(tr("Write palette-%1.toml and reload the theme").arg(loadedScheme.toLower()));
if (saveDir.isEmpty() || !isDirReallyWritable(saveDir)) { if (saveDir.isEmpty() || !ThemeManager::isDirReallyWritable(saveDir)) {
saveBtn->setEnabled(false); saveBtn->setEnabled(false);
saveBtn->setToolTip(tr("Cannot save: this theme has no writable directory")); saveBtn->setToolTip(tr("Cannot save: this theme has no writable directory"));
} }
@ -297,7 +269,7 @@ void PaletteEditorDialog::onSave()
if (it.key() == loadedScheme) { if (it.key() == loadedScheme) {
continue; continue;
} }
if (it.value().colors == savedConfig.value(it.key()).colors) { if (it.value() == savedConfig.value(it.key())) {
continue; continue;
} }
if (!ThemeManager::commitPalette(saveDir, it.key(), it.value())) { if (!ThemeManager::commitPalette(saveDir, it.key(), it.value())) {
@ -308,7 +280,7 @@ void PaletteEditorDialog::onSave()
} }
// Commit the active scheme last so the global colour scheme matches. // Commit the active scheme last so the global colour scheme matches.
if (workingConfig[loadedScheme].colors != savedConfig.value(loadedScheme).colors) { if (workingConfig[loadedScheme] != savedConfig.value(loadedScheme)) {
if (!ThemeManager::commitPalette(saveDir, loadedScheme, workingConfig[loadedScheme])) { if (!ThemeManager::commitPalette(saveDir, loadedScheme, workingConfig[loadedScheme])) {
QMessageBox::warning(this, tr("Save failed"), QMessageBox::warning(this, tr("Save failed"),
tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(loadedScheme), saveDir)); tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(loadedScheme), saveDir));

View file

@ -150,6 +150,17 @@ PaletteConfig fromAccent(const QColor &accent, int intensity, const QString &sch
cfg.colors[CG::Disabled][CR::HighlightedText] = disText; cfg.colors[CG::Disabled][CR::HighlightedText] = disText;
cfg.colors[CG::Inactive][CR::HighlightedText] = dark ? Qt::white : Qt::black; 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 // BrightText
QColor bright; QColor bright;
if (achromatic) { if (achromatic) {

View file

@ -1,5 +1,7 @@
#include "palette_grid_widget.h" #include "palette_grid_widget.h"
#include "../theme_manager.h"
#include <QApplication> #include <QApplication>
#include <QGridLayout> #include <QGridLayout>
#include <QLabel> #include <QLabel>
@ -45,6 +47,11 @@ static const QMap<QPalette::ColorRole, const char *> ROLE_DESCRIPTIONS = {
{QPalette::Shadow, QT_TR_NOOP("Very dark shadow colour")}, {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) PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent)
{ {
scroll = new QScrollArea(this); scroll = new QScrollArea(this);
@ -122,6 +129,46 @@ void PaletteGridWidget::buildGrid(QWidget *host)
grid->addWidget(btn, row + 1, col + 1, Qt::AlignHCenter | Qt::AlignVCenter); 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) void PaletteGridWidget::changeEvent(QEvent *e)
@ -166,6 +213,16 @@ void PaletteGridWidget::loadPalette(const PaletteConfig &cfg)
colorButtons[group][role]->setColor(color); 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 PaletteConfig PaletteGridWidget::currentPaletteConfig() const
@ -176,5 +233,12 @@ PaletteConfig PaletteGridWidget::currentPaletteConfig() const
cfg.colors[group][role] = colorButtons[group][role]->getColor(); 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; return cfg;
} }

View file

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

View file

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

View file

@ -16,7 +16,7 @@ QString ThemeConfig::toIni() const
out += "[Appearance]\n"; out += "[Appearance]\n";
out += QString("ColorScheme = %1\n").arg(colorScheme.isEmpty() ? "System" : colorScheme); out += QString("ColorScheme = %1\n").arg(colorScheme.isEmpty() ? "System" : colorScheme);
out += "\n[Style]\n"; out += "\n[Style]\n";
out += QString("Name = %1\n").arg(styleName.isEmpty() ? "Default" : styleName); out += QString("Name = %1\n").arg(styleName.isEmpty() ? "System" : styleName);
return out; return out;
} }
@ -96,7 +96,7 @@ bool ThemeConfig::save(const QString &themeDirPath) const
bool PaletteConfig::hasPalette() const bool PaletteConfig::hasPalette() const
{ {
return !colors.isEmpty(); return !colors.isEmpty() || !appColors.isEmpty();
} }
QString PaletteConfig::toToml() const QString PaletteConfig::toToml() const
@ -133,6 +133,24 @@ QString PaletteConfig::toToml() const
out += "\n"; 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; return out;
} }
@ -152,6 +170,7 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath)
} }
QMetaEnum roleEnum = QMetaEnum::fromType<QPalette::ColorRole>(); QMetaEnum roleEnum = QMetaEnum::fromType<QPalette::ColorRole>();
QMetaEnum appEnum = QMetaEnum::fromType<AppColor::Role>();
QString currentSection; QString currentSection;
QPalette::ColorGroup currentGroup = QPalette::Active; QPalette::ColorGroup currentGroup = QPalette::Active;
@ -202,6 +221,26 @@ 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)) { if (!currentSection.startsWith("Palette", Qt::CaseInsensitive)) {
continue; continue;
} }
@ -216,11 +255,7 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath)
continue; continue;
} }
QColor color(value); cfg.colors[currentGroup][static_cast<QPalette::ColorRole>(roleInt)] = color;
if (color.isValid()) {
cfg.colors[currentGroup][static_cast<QPalette::ColorRole>(roleInt)] = color;
}
} }
return cfg; return cfg;

View file

@ -3,9 +3,25 @@
#include <QColor> #include <QColor>
#include <QMap> #include <QMap>
#include <QObject>
#include <QPalette> #include <QPalette>
#include <QString> #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 struct ThemeConfig
{ {
QString colorScheme; QString colorScheme;
@ -21,7 +37,16 @@ struct ThemeConfig
struct PaletteConfig struct PaletteConfig
{ {
QMap<QPalette::ColorGroup, QMap<QPalette::ColorRole, QColor>> colors; 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; bool hasPalette() const;
QString toToml() const; QString toToml() const;

View file

@ -6,6 +6,7 @@
#include <QApplication> #include <QApplication>
#include <QColor> #include <QColor>
#include <QDebug> #include <QDebug>
#include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QLibraryInfo> #include <QLibraryInfo>
#include <QMap> #include <QMap>
@ -21,7 +22,7 @@
#include <Qt> #include <Qt>
#include <libcockatrice/settings/paths_settings.h> #include <libcockatrice/settings/paths_settings.h>
#define NONE_THEME_NAME "Default" #define SYSTEM_THEME_NAME "System"
#define FUSION_THEME_NAME "Fusion" #define FUSION_THEME_NAME "Fusion"
#define STYLE_CSS_NAME "style.css" #define STYLE_CSS_NAME "style.css"
#define HANDZONE_BG_NAME "handzone" #define HANDZONE_BG_NAME "handzone"
@ -95,7 +96,7 @@ struct PaletteColorInfo
static QString usableDefaultStyle(const QString &style) static QString usableDefaultStyle(const QString &style)
{ {
// The Windows 11 native style is broken: when the OS default // The Windows 11 native style is broken: when the OS default
// ("Default" theme selection) would use it, fall back to the Vista style. // ("System" theme selection) would use it, fall back to the Vista style.
// Explicitly choosing "windows11" in a theme is still honored. // Explicitly choosing "windows11" in a theme is still honored.
return style.compare("windows11", Qt::CaseInsensitive) == 0 ? QStringLiteral("windowsvista") : style; return style.compare("windows11", Qt::CaseInsensitive) == 0 ? QStringLiteral("windowsvista") : style;
} }
@ -118,10 +119,16 @@ ThemeManager::ThemeManager(QObject *parent) : QObject(parent)
void ThemeManager::ensureThemeDirectoryExists() void ThemeManager::ensureThemeDirectoryExists()
{ {
if (SettingsCache::instance().getThemeName().isEmpty() || auto &settings = SettingsCache::instance();
!getAvailableThemes().contains(SettingsCache::instance().getThemeName())) {
// 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())) {
qCInfo(ThemeManagerLog) << "Theme name not set, setting default value"; qCInfo(ThemeManagerLog) << "Theme name not set, setting default value";
SettingsCache::instance().setThemeName(NONE_THEME_NAME); settings.setThemeName(FUSION_THEME_NAME);
} }
} }
@ -184,11 +191,32 @@ QString ThemeManager::assetPath(QStringView prefix) const
return resolvedPlain.isEmpty() ? prefix.toString() : resolvedPlain; return resolvedPlain.isEmpty() ? prefix.toString() : resolvedPlain;
} }
bool ThemeManager::isBuiltInTheme() // 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)
{ {
const auto themeName = SettingsCache::instance().getThemeName(); 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;
}
return themeName == NONE_THEME_NAME || themeName == FUSION_THEME_NAME; 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;
} }
// System (read-only) themes location, relative to the application binary. // System (read-only) themes location, relative to the application binary.
@ -213,9 +241,7 @@ QStringMap &ThemeManager::getAvailableThemes()
// load themes from user profile dir // load themes from user profile dir
dir.setPath(SettingsCache::instance().paths().getThemesPath()); dir.setPath(SettingsCache::instance().paths().getThemesPath());
// add default value availableThemes.insert(SYSTEM_THEME_NAME, dir.absoluteFilePath("System"));
availableThemes.insert(NONE_THEME_NAME, dir.absoluteFilePath("Default"));
availableThemes.insert(FUSION_THEME_NAME, dir.absoluteFilePath("Fusion")); availableThemes.insert(FUSION_THEME_NAME, dir.absoluteFilePath("Fusion"));
for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) { for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) {
@ -331,7 +357,7 @@ bool ThemeManager::commitPalette(const QString &themeDirPath, const QString &col
void ThemeManager::setColorScheme(const QString &scheme) void ThemeManager::setColorScheme(const QString &scheme)
{ {
const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName()); const QString dirPath = writableThemeDir(SettingsCache::instance().getThemeName());
ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath); ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath);
cfg.colorScheme = scheme; cfg.colorScheme = scheme;
@ -342,7 +368,7 @@ void ThemeManager::setColorScheme(const QString &scheme)
void ThemeManager::setStyleName(const QString &styleName) void ThemeManager::setStyleName(const QString &styleName)
{ {
const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName()); const QString dirPath = writableThemeDir(SettingsCache::instance().getThemeName());
ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath); ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath);
cfg.styleName = styleName; cfg.styleName = styleName;
@ -373,7 +399,7 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
Q_UNUSED(activeScheme) Q_UNUSED(activeScheme)
#endif #endif
QString styleName = themeCfg.styleName; QString styleName = themeCfg.styleName;
if (styleName.isEmpty() || styleName.compare("Default", Qt::CaseInsensitive) == 0) { if (styleName.isEmpty() || styleName.compare("System", Qt::CaseInsensitive) == 0) {
if (themeName == FUSION_THEME_NAME) { if (themeName == FUSION_THEME_NAME) {
styleName = "Fusion"; styleName = "Fusion";
} else { } else {
@ -416,6 +442,8 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
qApp->setPalette(base); qApp->setPalette(base);
qApp->setStyle(style); qApp->setStyle(style);
currentAppColors = palCfg.appColors;
// Force every widget to re-polish and repaint immediately rather than // Force every widget to re-polish and repaint immediately rather than
// waiting for natural expose events, which produces a patchwork of old // waiting for natural expose events, which produces a patchwork of old
// and new colours during a live preview. // and new colours during a live preview.
@ -428,6 +456,35 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
style->polish(widget); style->polish(widget);
widget->update(); 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() void ThemeManager::themeChangedSlot()
@ -464,8 +521,19 @@ void ThemeManager::themeChangedSlot()
// ── Load palette: custom first, then theme default ──────────────────── // ── Load palette: custom first, then theme default ────────────────────
PaletteConfig palette = PaletteConfig::fromScheme(dirPath, activeScheme); PaletteConfig palette = PaletteConfig::fromScheme(dirPath, activeScheme);
if (!palette.hasPalette()) { const PaletteConfig themeDefault = ThemeManager::loadDefaultPaletteConfig(dirPath, themeName, activeScheme);
palette = 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;
} }
applyStyleAndPalette(themeName, themeCfg, palette, activeScheme); applyStyleAndPalette(themeName, themeCfg, palette, activeScheme);

View file

@ -50,6 +50,7 @@ private:
QString currentThemePath; QString currentThemePath;
std::array<QBrush, Role::MaxRole + 1> brushes; std::array<QBrush, Role::MaxRole + 1> brushes;
QStringMap availableThemes; QStringMap availableThemes;
QMap<AppColor::Role, QColor> currentAppColors;
/* /*
Internal cache for multiple backgrounds Internal cache for multiple backgrounds
*/ */
@ -65,7 +66,16 @@ protected:
const QString &activeScheme); const QString &activeScheme);
public: public:
bool isBuiltInTheme(); // 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);
// Explicit color scheme of the theme: theme.cfg's ColorScheme setting // Explicit color scheme of the theme: theme.cfg's ColorScheme setting
// (Dark/Light), falling back to the OS color scheme when it is "System". // (Dark/Light), falling back to the OS color scheme when it is "System".
bool isDarkMode(const QString &themeDirPath) const; bool isDarkMode(const QString &themeDirPath) const;
@ -115,12 +125,17 @@ public:
void reloadCurrentTheme(); void reloadCurrentTheme();
void previewPalette(const PaletteConfig &cfg, const QString &scheme); 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 &getBgBrush(Role zone);
QBrush getExtraBgBrush(Role zone, int zoneId = 0); QBrush getExtraBgBrush(Role zone, int zoneId = 0);
protected slots: protected slots:
void themeChangedSlot(); void themeChangedSlot();
signals: signals:
void themeChanged(); void themeChanged();
void paletteChanged();
}; };
extern ThemeManager *themeManager; extern ThemeManager *themeManager;

View file

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

View file

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

View file

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

View file

@ -345,7 +345,9 @@ ExactCard DeckEditorDeckDockWidget::getCurrentCard()
if (!current.isValid()) { if (!current.isValid()) {
return {}; return {};
} }
const QString cardName = current.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); // 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 cardProviderID = current.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString(); const QString cardProviderID = current.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString();
const QModelIndex gparent = current.parent().parent(); const QModelIndex gparent = current.parent().parent();
@ -518,8 +520,11 @@ void DeckEditorDeckDockWidget::syncBannerCardComboBoxSelectionWithDeck()
void DeckEditorDeckDockWidget::setSelectedIndex(const QModelIndex &newCardIndex, bool preserveWidgetFocus) void DeckEditorDeckDockWidget::setSelectedIndex(const QModelIndex &newCardIndex, bool preserveWidgetFocus)
{ {
const QModelIndex proxyIndex = proxy->mapFromSource(newCardIndex);
deckView->clearSelection(); deckView->clearSelection();
deckView->setCurrentIndex(newCardIndex); deckView->setCurrentIndex(proxyIndex);
deckView->scrollTo(proxyIndex);
recursiveExpand(newCardIndex); recursiveExpand(newCardIndex);
if (!preserveWidgetFocus) { if (!preserveWidgetFocus) {

View file

@ -1,13 +1,20 @@
#include "deck_state_manager.h" #include "deck_state_manager.h"
#include "../../../client/settings/cache_settings.h"
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list_history_manager.h> #include <libcockatrice/deck_list/deck_list_history_manager.h>
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h> #include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
#include <libcockatrice/settings/cards_display_settings.h>
DeckStateManager::DeckStateManager(QObject *parent) DeckStateManager::DeckStateManager(QObject *parent)
: QObject(parent), deckList(QSharedPointer<DeckList>(new DeckList)), : QObject(parent), deckList(QSharedPointer<DeckList>(new DeckList)),
deckListModel(new DeckListModel(this, deckList)), historyManager(new DeckListHistoryManager(this)) 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] { connect(historyManager, &DeckListHistoryManager::undoRedoStateChanged, this, [this] {
setModified(true); setModified(true);
emit historyChanged(); emit historyChanged();
@ -260,7 +267,10 @@ bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx)
return false; return false;
} }
QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); // 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 providerId = idx.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString(); QString providerId = idx.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString();
QModelIndex gparent = idx.parent().parent(); QModelIndex gparent = idx.parent().parent();
@ -277,7 +287,7 @@ bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx)
QString reason = tr("Moved to %1 1 × \"%2\" (%3)") // QString reason = tr("Moved to %1 1 × \"%2\" (%3)") //
.arg(otherZoneName) .arg(otherZoneName)
.arg(cardName) .arg(displayCardName)
.arg(providerId); .arg(providerId);
return modifyDeck(reason, [&idx, &cardName, &providerId, &otherZoneName](auto model) { return modifyDeck(reason, [&idx, &cardName, &providerId, &otherZoneName](auto model) {
@ -291,9 +301,8 @@ bool DeckStateManager::removeCardAtIndex(const QModelIndex &idx)
return false; return false;
} }
QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); QString reason =
tr("Removed \"%1\" (all copies)").arg(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()); }); return modifyDeck(reason, [&idx](auto model) { return model->removeRow(idx.row(), idx.parent()); });
} }

View file

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

View file

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

View file

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

View file

@ -11,6 +11,8 @@
#include "home_tab_button_color.h" #include "home_tab_button_color.h"
#include <QGroupBox> #include <QGroupBox>
#include <QLabel>
#include <QLinearGradient>
#include <QPainter> #include <QPainter>
#include <QPainterPath> #include <QPainterPath>
#include <QPushButton> #include <QPushButton>
@ -21,8 +23,7 @@
#include <libcockatrice/settings/paths_settings.h> #include <libcockatrice/settings/paths_settings.h>
HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) 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); layout = new QGridLayout(this);
@ -54,6 +55,8 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
// Lambda is cleaner to read than overloading this // Lambda is cleaner to read than overloading this
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabDisplayCardNameChanged, this, connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabDisplayCardNameChanged, this,
[this] { repaint(); }); [this] { repaint(); });
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundDimChanged, this,
[this] { repaint(); });
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
&HomeWidget::initializeBackgroundFromSource); &HomeWidget::initializeBackgroundFromSource);
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
@ -61,12 +64,18 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
// Scheme flips (light/dark/system with an OS switch) fire on themeManager, // Scheme flips (light/dark/system with an OS switch) fire on themeManager,
// not on SettingsCache::themeChanged, so re-resolve the variant background. // not on SettingsCache::themeChanged, so re-resolve the variant background.
connect(themeManager, &ThemeManager::themeChanged, this, &HomeWidget::initializeBackgroundFromSource); 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, connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabButtonColorChanged, this,
&HomeWidget::updateButtonsToBackgroundColor); &HomeWidget::updateButtonsToBackgroundColor);
} }
void HomeWidget::initializeBackgroundFromSource() 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) { if (CardDatabaseManager::getInstance()->getLoadStatus() != LoadStatus::Ok) {
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this, connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this,
&HomeWidget::initializeBackgroundFromSource); &HomeWidget::initializeBackgroundFromSource);
@ -105,32 +114,24 @@ void HomeWidget::loadBackgroundSourceDeck()
backgroundSourceDeck = deckOpt.has_value() ? deckOpt.value().deckList : DeckList(); backgroundSourceDeck = deckOpt.has_value() ? deckOpt.value().deckList : DeckList();
} }
static bool isDefaultBackgroundAndTheme() static QPair<QColor, QColor> paletteDerivedButtonColors()
{ {
QString sourceId = SettingsCache::instance().appearance().getHomeTabBackgroundSource(); return {themeManager->appColor(AppColor::AccentStrong), themeManager->appColor(AppColor::AccentSoft)};
return themeManager->isBuiltInTheme() && BackgroundSources::fromId(sourceId) == BackgroundSources::Theme;
} }
QPair<QColor, QColor> HomeWidget::determineButtonColor() const QPair<QColor, QColor> HomeWidget::determineButtonColor() const
{ {
static QPair defaultColor = {QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80)};
auto colorSource = auto colorSource =
HomeTabButtonColor::intToSource(SettingsCache::instance().appearance().getHomeTabButtonColorSourceIndex()); HomeTabButtonColor::intToSource(SettingsCache::instance().appearance().getHomeTabButtonColorSourceIndex());
switch (colorSource) { switch (colorSource) {
case HomeTabButtonColor::Automatic: { case HomeTabButtonColor::FromThemeColors:
if (isDefaultBackgroundAndTheme()) { return paletteDerivedButtonColors();
return defaultColor;
} else {
return extractDominantColors(background);
}
}
case HomeTabButtonColor::FromBackground: case HomeTabButtonColor::FromBackground:
return extractDominantColors(background); return extractDominantColors(background);
} }
return defaultColor; return paletteDerivedButtonColors();
} }
void HomeWidget::setRandomCard(ExactCard &newCard) void HomeWidget::setRandomCard(ExactCard &newCard)
@ -234,10 +235,10 @@ QGroupBox *HomeWidget::createButtons()
QVBoxLayout *boxLayout = new QVBoxLayout; QVBoxLayout *boxLayout = new QVBoxLayout;
boxLayout->setAlignment(Qt::AlignHCenter); boxLayout->setAlignment(Qt::AlignHCenter);
QLabel *logoLabel = new QLabel; logoLabel = new QLabel;
logoLabel->setPixmap(overlay.scaledToWidth(200, Qt::SmoothTransformation));
logoLabel->setAlignment(Qt::AlignCenter); logoLabel->setAlignment(Qt::AlignCenter);
boxLayout->addWidget(logoLabel); boxLayout->addWidget(logoLabel);
updateLogoOverlay();
boxLayout->addSpacing(25); boxLayout->addSpacing(25);
connectButton = new HomeStyledButton("Connect/Play", gradientColors); connectButton = new HomeStyledButton("Connect/Play", gradientColors);
@ -365,13 +366,15 @@ void HomeWidget::paintEvent(QPaintEvent *event)
painter.drawPixmap(topLeft, toDraw); painter.drawPixmap(topLeft, toDraw);
} }
// Draw translucent black overlay with rounded corners if (SettingsCache::instance().appearance().getHomeTabBackgroundDim()) {
QRectF overlayRect(5, 5, width() - 10, height() - 10); // Draw translucent black overlay with rounded corners
QPainterPath roundedRectPath; QRectF overlayRect(5, 5, width() - 10, height() - 10);
roundedRectPath.addRoundedRect(overlayRect, 20, 20); QPainterPath roundedRectPath;
roundedRectPath.addRoundedRect(overlayRect, 20, 20);
QColor semiTransparentBlack(0, 0, 0, static_cast<int>(255 * 0.33)); QColor semiTransparentBlack(0, 0, 0, static_cast<int>(255 * 0.33));
painter.fillPath(roundedRectPath, semiTransparentBlack); painter.fillPath(roundedRectPath, semiTransparentBlack);
}
// Card name overlay (above the attribution, bottom-right) // Card name overlay (above the attribution, bottom-right)
QString cardName; QString cardName;
@ -436,3 +439,56 @@ void HomeWidget::paintEvent(QPaintEvent *event)
QWidget::paintEvent(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,6 +15,9 @@
#include <QWidget> #include <QWidget>
#include <libcockatrice/network/client/abstract/abstract_client.h> #include <libcockatrice/network/client/abstract/abstract_client.h>
class QGridLayout;
class QLabel;
class HomeWidget : public QWidget class HomeWidget : public QWidget
{ {
@ -41,13 +44,14 @@ private:
QPixmap background; QPixmap background;
CardInfoPictureArtCropWidget *backgroundSourceCard = nullptr; CardInfoPictureArtCropWidget *backgroundSourceCard = nullptr;
DeckList backgroundSourceDeck; DeckList backgroundSourceDeck;
QPixmap overlay; QLabel *logoLabel = nullptr;
QPair<QColor, QColor> gradientColors; QPair<QColor, QColor> gradientColors;
HomeStyledButton *connectButton; HomeStyledButton *connectButton;
void setRandomCard(ExactCard &newCard); void setRandomCard(ExactCard &newCard);
void loadBackgroundSourceDeck(); void loadBackgroundSourceDeck();
QPair<QColor, QColor> determineButtonColor() const; QPair<QColor, QColor> determineButtonColor() const;
void updateLogoOverlay();
}; };
#endif // HOME_WIDGET_H #endif // HOME_WIDGET_H

View file

@ -38,6 +38,10 @@ class BannerShaderConfig : public QObject
Q_PROPERTY(QColor colorA READ colorA WRITE setColorA NOTIFY colorAChanged) Q_PROPERTY(QColor colorA READ colorA WRITE setColorA NOTIFY colorAChanged)
Q_PROPERTY(QColor colorB READ colorB WRITE setColorB NOTIFY colorBChanged) Q_PROPERTY(QColor colorB READ colorB WRITE setColorB NOTIFY colorBChanged)
Q_PROPERTY(QColor accent READ accent WRITE setAccent NOTIFY accentChanged) 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(bool logoVisible READ logoVisible WRITE setLogoVisible NOTIFY logoVisibleChanged)
Q_PROPERTY(qreal logoGlow READ logoGlow WRITE setLogoGlow NOTIFY logoGlowChanged) Q_PROPERTY(qreal logoGlow READ logoGlow WRITE setLogoGlow NOTIFY logoGlowChanged)
@ -185,6 +189,54 @@ 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 bool logoVisible() const
{ {
return m_logoVisible; return m_logoVisible;
@ -222,6 +274,10 @@ signals:
void colorAChanged(); void colorAChanged();
void colorBChanged(); void colorBChanged();
void accentChanged(); void accentChanged();
void glowColorChanged();
void brandStrongChanged();
void brandSoftChanged();
void vignetteMinChanged();
void logoVisibleChanged(); void logoVisibleChanged();
void logoGlowChanged(); void logoGlowChanged();
@ -239,9 +295,16 @@ private:
bool m_frontIsA = true; 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_colorA{0x1A, 0x1A, 0x20};
QColor m_colorB{0x0E, 0x0E, 0x12}; QColor m_colorB{0x0E, 0x0E, 0x12};
QColor m_accent{0x8B, 0xDD, 0x6B}; 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; bool m_logoVisible = false;
qreal m_logoGlow = 1.0; qreal m_logoGlow = 1.0;

View file

@ -0,0 +1,15 @@
#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,10 +14,31 @@
#include <QGroupBox> #include <QGroupBox>
#include <QLabel> #include <QLabel>
#include <QMessageBox> #include <QMessageBox>
#include <QPalette>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <libcockatrice/settings/paths_settings.h> #include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/personal_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) ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
{ {
themeCombo = new QComboBox(this); themeCombo = new QComboBox(this);
@ -30,6 +51,15 @@ ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
quickSetupPanel = new QuickSetupPanel(this); 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(themeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onThemeChanged);
connect(schemeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onSchemeChanged); connect(schemeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onSchemeChanged);
connect(quickSetupPanel, &QuickSetupPanel::valueChanged, this, &ThemeSetupPage::onGenerateFromAccent); connect(quickSetupPanel, &QuickSetupPanel::valueChanged, this, &ThemeSetupPage::onGenerateFromAccent);
@ -47,7 +77,8 @@ ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
// Mirrors AppearanceSettingsPage's identical listener for the combo-sync // Mirrors AppearanceSettingsPage's identical listener for the combo-sync
// half of this. // half of this.
connect(themeManager, &ThemeManager::themeChanged, this, [this] { connect(themeManager, &ThemeManager::themeChanged, this, [this] {
const QString newDir = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName()); const QString newTheme = SettingsCache::instance().getThemeName();
const QString newDir = themeManager->getAvailableThemes().value(newTheme);
const ThemeConfig cfg = ThemeConfig::fromThemeDir(newDir); const ThemeConfig cfg = ThemeConfig::fromThemeDir(newDir);
const QString current = cfg.colorScheme; const QString current = cfg.colorScheme;
@ -56,6 +87,14 @@ ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent)
schemeCombo->setCurrentIndex(idx >= 0 ? idx : 0); schemeCombo->setCurrentIndex(idx >= 0 ? idx : 0);
schemeCombo->blockSignals(false); 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(); maybeAutoGeneratePalette();
}); });
@ -158,8 +197,14 @@ void ThemeSetupPage::maybeAutoGeneratePalette()
const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName()); const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName());
const QString scheme = resolvedScheme(); 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() || if (PaletteConfig::fromScheme(dirPath, scheme).hasPalette() ||
PaletteConfig::fromDefault(dirPath, scheme).hasPalette()) { ThemeManager::loadDefaultPaletteConfig(dirPath, SettingsCache::instance().getThemeName(), scheme)
.hasPalette()) {
return; // theme already has something real to show -- leave it alone return; // theme already has something real to show -- leave it alone
} }

View file

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

View file

@ -16,6 +16,8 @@ Item {
property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0) 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 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 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 property real uLogoGlow: bannerConfig.logoGlow
fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb" fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb"
} }
@ -33,30 +35,62 @@ Item {
property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0) 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 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 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 property real uLogoGlow: bannerConfig.logoGlow
fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb" fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb"
} }
// The hero logo itself breathes cleanly over a 0.51.0 opacity range // The white logo sits at full opacity on top of the static gradient plate
Image { // no glow, no breathing. The plate matches home_widget's QPainter composite.
id: logo Item {
anchors.centerIn: parent id: logoHost
visible: bannerConfig.logoVisible visible: bannerConfig.logoVisible
source: "qrc:/resources/cockatrice-logo-white.svg" anchors.centerIn: parent
width: root.height * 0.6 width: root.height * 0.6
height: width * (sourceSize.height > 0 ? sourceSize.height / Math.max(sourceSize.width, 1) : 1) height: width
fillMode: Image.PreserveAspectFit
smooth: true
opacity: 0.5 + 0.5 * bannerConfig.logoGlow
sourceSize: Qt.size(256, 256)
Behavior on opacity { NumberAnimation { duration: 300; easing.type: Easing.InOutSine } } // 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
}
transform: Scale { // The logo's gradient plate, drawn behind the mark: a linear
origin.x: logo.width / 2 // AccentSoft (light) -> AccentStrong (dark) sheet along the same
origin.y: logo.height / 2 // top-left -> bottom-right userSpaceOnUse axis the baked-in SVG used,
xScale: 0.94 + 0.06 * bannerConfig.logoGlow // clipped to the bird silhouette via uSilhouette. The white highlight
yScale: 0.94 + 0.06 * bannerConfig.logoGlow // 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
} }
} }
} }

View file

@ -1,7 +1,10 @@
#include "shader_banner_widget.h" #include "shader_banner_widget.h"
#include "../../theme_manager.h"
#include "banner_shader_config.h" #include "banner_shader_config.h"
#include "brand_colors.h"
#include <QApplication>
#include <QPainter> #include <QPainter>
#include <QQmlContext> #include <QQmlContext>
#include <QQmlEngine> #include <QQmlEngine>
@ -11,11 +14,98 @@
namespace namespace
{ {
// Near-black base palette -- the background is dark and quiet so the green // Curated near-black stage -- used only when the active palette resolves no
// accent stands out. // usable window colour. Matches the banner's original design (dark and quiet
constexpr QRgb kColorA = 0x1A1A20; // so the accent stands out) and satisfies design-plans §2.1's "identity
constexpr QRgb kColorB = 0x0E0E12; // survives a bare palette".
constexpr QRgb kAccent = 0x8BDD6B; 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};
}
} // namespace } // namespace
class GradientFallbackWidget : public QWidget class GradientFallbackWidget : public QWidget
@ -23,15 +113,27 @@ class GradientFallbackWidget : public QWidget
public: public:
using QWidget::QWidget; using QWidget::QWidget;
void setColors(const QColor &a, const QColor &b)
{
if (a != colorA || b != colorB) {
colorA = a;
colorB = b;
}
}
protected: protected:
void paintEvent(QPaintEvent *) override void paintEvent(QPaintEvent *) override
{ {
QPainter painter(this); QPainter painter(this);
QLinearGradient gradient(0, 0, width(), height()); QLinearGradient gradient(0, 0, width(), height());
gradient.setColorAt(0.0, QColor(kColorA)); gradient.setColorAt(0.0, colorA);
gradient.setColorAt(1.0, QColor(kColorB)); gradient.setColorAt(1.0, colorB);
painter.fillRect(rect(), gradient); painter.fillRect(rect(), gradient);
} }
private:
QColor colorA{QColor(kFallbackColorA)};
QColor colorB{QColor(kFallbackColorB)};
}; };
BannerHost::BannerHost(QWidget *parent) : QWidget(parent) BannerHost::BannerHost(QWidget *parent) : QWidget(parent)
@ -62,6 +164,9 @@ BannerHost::BannerHost(QWidget *parent) : QWidget(parent)
connect(&clock, &QTimer::timeout, this, &BannerHost::tick); connect(&clock, &QTimer::timeout, this, &BannerHost::tick);
clock.setInterval(16); // ~60fps; the shader itself is cheap, this is just a wall clock 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); applyMotifPreset(currentMotif);
updateAspect(); updateAspect();
} }
@ -123,9 +228,9 @@ void BannerHost::applyMotifPreset(Motif motif)
const Preset p = presetFor(motif); const Preset p = presetFor(motif);
config->setColorA(QColor(kColorA)); config->setColorA(bannerColorA);
config->setColorB(QColor(kColorB)); config->setColorB(bannerColorB);
config->setAccent(QColor(kAccent)); config->setAccent(bannerAccent);
config->setLogoVisible(motif == Motif::Welcome); config->setLogoVisible(motif == Motif::Welcome);
if (isFirstApply) { if (isFirstApply) {
@ -183,8 +288,34 @@ void BannerHost::hideEvent(QHideEvent *event)
clock.stop(); 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() 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) { if (config) {
qreal t = elapsed.elapsed() / 1000.0; qreal t = elapsed.elapsed() / 1000.0;
config->setTime(t); config->setTime(t);

View file

@ -1,6 +1,7 @@
#ifndef SHADER_BANNER_WIDGET_H #ifndef SHADER_BANNER_WIDGET_H
#define SHADER_BANNER_WIDGET_H #define SHADER_BANNER_WIDGET_H
#include <QColor>
#include <QElapsedTimer> #include <QElapsedTimer>
#include <QTimer> #include <QTimer>
#include <QWidget> #include <QWidget>
@ -53,6 +54,7 @@ protected:
private slots: private slots:
void tick(); void tick();
void onSceneGraphFailed(); void onSceneGraphFailed();
void applyThemeColors();
private: private:
struct Preset struct Preset
@ -73,6 +75,12 @@ private:
BannerShaderConfig *config = nullptr; BannerShaderConfig *config = nullptr;
GradientFallbackWidget *fallback = 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; QTimer clock;
QElapsedTimer elapsed; QElapsedTimer elapsed;
Motif currentMotif = Motif::Welcome; Motif currentMotif = Motif::Welcome;

View file

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

View file

@ -0,0 +1,42 @@
#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,13 +1,19 @@
#include "printing_selector_card_overlay_widget.h" #include "printing_selector_card_overlay_widget.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../card_picture_loader/card_picture_loader.h"
#include "../cards/card_info_picture_widget.h" #include "../cards/card_info_picture_widget.h"
#include "printing_selector_card_display_widget.h" #include "printing_selector_card_display_widget.h"
#include <QApplication>
#include <QFileDialog>
#include <QImageReader> #include <QImageReader>
#include <QLabel> #include <QLabel>
#include <QMenu> #include <QMenu>
#include <QMessageBox>
#include <QMouseEvent> #include <QMouseEvent>
#include <QPixmapCache>
#include <QScreen>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QtMath> #include <QtMath>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
@ -15,6 +21,49 @@
#include <libcockatrice/settings/card_override_settings.h> #include <libcockatrice/settings/card_override_settings.h>
#include <utility> #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. * @brief Constructs a PrintingSelectorCardOverlayWidget for displaying a card overlay.
* *
@ -50,6 +99,31 @@ PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *pa
initializePinBadge(); 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 // Update when this overlay emits cardPreferenceChanged or when size/scale changes
connect(this, &PrintingSelectorCardOverlayWidget::cardPreferenceChanged, this, connect(this, &PrintingSelectorCardOverlayWidget::cardPreferenceChanged, this,
&PrintingSelectorCardOverlayWidget::updatePinBadgeVisibility); &PrintingSelectorCardOverlayWidget::updatePinBadgeVisibility);
@ -185,19 +259,23 @@ void PrintingSelectorCardOverlayWidget::leaveEvent(QEvent *event)
} }
/** /**
* @brief Creates and shows a custom context menu when the right mouse button is clicked. * @brief Creates and shows the card-overlay context menu.
* *
* The context menu includes an option to show related cards, which displays a submenu with actions * The menu includes the card art preference (Pin/Unpin Printing), the Image Overrides
* for each related card. When an action is triggered, the card information is updated, and the * submenu (Load Custom Image, Clear Custom Image, and one entry per alternate printing with a
* printing selector is shown. * live preview), and the Show Related cards submenu.
* *
* @param point The position of the mouse when the right-click occurred. * @param point The local position the menu should pop at.
*/ */
void PrintingSelectorCardOverlayWidget::customMenu(QPoint point) void PrintingSelectorCardOverlayWidget::customMenu(QPoint point)
{ {
QMenu menu; QMenu menu;
auto *preferenceMenu = new QMenu(tr("Preference")); 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);
menu.addMenu(preferenceMenu); menu.addMenu(preferenceMenu);
const auto &preferredProviderId = const auto &preferredProviderId =
@ -219,8 +297,66 @@ 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 // filling out the related cards submenu
auto *relatedMenu = new QMenu(tr("Show Related cards")); auto *relatedMenu = new QMenu(tr("Show Related cards"), &menu);
menu.addMenu(relatedMenu); menu.addMenu(relatedMenu);
auto relatedCards = rootCard.getInfo().getAllRelatedCards(); auto relatedCards = rootCard.getInfo().getAllRelatedCards();
if (relatedCards.isEmpty()) { if (relatedCards.isEmpty()) {
@ -235,7 +371,11 @@ 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)); menu.exec(this->mapToGlobal(point));
previewSourceMenu = nullptr;
} }
/** /**
@ -291,3 +431,136 @@ void PrintingSelectorCardOverlayWidget::initializePinBadge()
pinBadge->setVisible(false); pinBadge->setVisible(false);
pinBadge->raise(); 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,6 +13,9 @@
#include <libcockatrice/models/deck_list/deck_list_model.h> #include <libcockatrice/models/deck_list/deck_list_model.h>
class QAction;
class QMenu;
class PrintingSelectorCardOverlayWidget : public QWidget class PrintingSelectorCardOverlayWidget : public QWidget
{ {
Q_OBJECT Q_OBJECT
@ -43,11 +46,19 @@ private slots:
private: private:
void initializePinBadge(); void initializePinBadge();
void loadCustomImage();
void showPreviewForAction(QAction *action);
void refreshPreview();
void hidePreview();
CardInfoPictureWidget *cardInfoPicture; CardInfoPictureWidget *cardInfoPicture;
AllZonesCardAmountWidget *allZonesCardAmountWidget; AllZonesCardAmountWidget *allZonesCardAmountWidget;
QLabel *pinBadge = nullptr; QLabel *pinBadge = nullptr;
AbstractTabDeckEditor *deckEditor; AbstractTabDeckEditor *deckEditor;
ExactCard rootCard; ExactCard rootCard;
QLabel *cardOverridePreviewLabel = nullptr;
ExactCard hoveredOverrideCard;
QMenu *previewSourceMenu = nullptr;
QAction *hoveredOverrideAction = nullptr;
}; };
#endif // PRINTING_SELECTOR_CARD_OVERLAY_WIDGET_H #endif // PRINTING_SELECTOR_CARD_OVERLAY_WIDGET_H

View file

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

View file

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

View file

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

View file

@ -1,15 +1,21 @@
#include "general_settings_page.h" #include "general_settings_page.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../interface/card_picture_loader/card_picture_loader.h"
#include "../main.h" #include "../main.h"
#include "../server/user/user_info_connection.h" #include "../server/user/user_info_connection.h"
#include "update/client/release_channel.h" #include "update/client/release_channel.h"
#include <QCoreApplication> #include <QCoreApplication>
#include <QFile>
#include <QFileDialog> #include <QFileDialog>
#include <QGridLayout> #include <QGridLayout>
#include <QLineEdit> #include <QLineEdit>
#include <QMessageBox>
#include <QTranslator> #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/paths_settings.h>
#include <libcockatrice/settings/personal_settings.h> #include <libcockatrice/settings/personal_settings.h>
#include <libcockatrice/settings/tabs_settings.h> #include <libcockatrice/settings/tabs_settings.h>
@ -46,10 +52,27 @@ GeneralSettingsPage::GeneralSettingsPage()
connect(&languageBox, qOverload<int>(&QComboBox::currentIndexChanged), this, connect(&languageBox, qOverload<int>(&QComboBox::currentIndexChanged), this,
&GeneralSettingsPage::languageBoxChanged); &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; auto *languageGrid = new QGridLayout;
languageGrid->addWidget(&languageLabel, 0, 0); languageGrid->addWidget(&languageLabel, 0, 0);
languageGrid->addWidget(&languageBox, 0, 1); languageGrid->addWidget(&languageBox, 0, 1);
languageGrid->addWidget(&advertiseTranslationPageLabel, 1, 1, Qt::AlignRight); 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);
languageGroupBox = new QGroupBox; languageGroupBox = new QGroupBox;
languageGroupBox->setLayout(languageGrid); languageGroupBox->setLayout(languageGrid);
@ -412,6 +435,52 @@ void GeneralSettingsPage::languageBoxChanged(int index)
SettingsCache::instance().personal().setLang(languageBox.itemData(index).toString()); 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() void GeneralSettingsPage::updateStartupServerControlsVisibility()
{ {
const int index = startupTabSelector.currentIndex(); const int index = startupTabSelector.currentIndex();
@ -429,6 +498,10 @@ void GeneralSettingsPage::retranslateUi()
languageGroupBox->setTitle(tr("Language settings")); languageGroupBox->setTitle(tr("Language settings"));
languageLabel.setText(tr("Language:")); 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( advertiseTranslationPageLabel.setText(
QString("<a href='%1'>%2</a>").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations"))); QString("<a href='%1'>%2</a>").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations")));

View file

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

View file

@ -1,5 +1,6 @@
#include "archidekt_api_response_deck_display_widget.h" #include "archidekt_api_response_deck_display_widget.h"
#include "../../../../../../client/settings/cache_settings.h"
#include "../../../../../deck_loader/card_node_function.h" #include "../../../../../deck_loader/card_node_function.h"
#include "../../../../../deck_loader/deck_loader.h" #include "../../../../../deck_loader/deck_loader.h"
#include "../../../../cards/card_size_widget.h" #include "../../../../cards/card_size_widget.h"
@ -10,6 +11,7 @@
#include <QSortFilterProxyModel> #include <QSortFilterProxyModel>
#include <libcockatrice/card/import/card_name_normalizer.h> #include <libcockatrice/card/import/card_name_normalizer.h>
#include <libcockatrice/settings/cards_display_settings.h>
ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWidget *parent, ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWidget *parent,
ArchidektApiResponseDeck _response, ArchidektApiResponseDeck _response,
@ -120,6 +122,9 @@ ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWi
} }
model = new DeckListModel(this); model = new DeckListModel(this);
model->setDisplayLanguage(SettingsCache::instance().cardsDisplay().getCardLang());
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, model,
[this](const QString &lang) { model->setDisplayLanguage(lang); });
connect(model, &DeckListModel::modelReset, this, &ArchidektApiResponseDeckDisplayWidget::decklistModelReset); connect(model, &DeckListModel::modelReset, this, &ArchidektApiResponseDeckDisplayWidget::decklistModelReset);
auto decklist = QSharedPointer<DeckList>(new DeckList); auto decklist = QSharedPointer<DeckList>(new DeckList);

View file

@ -13,6 +13,7 @@
#include <libcockatrice/protocol/pb/event_list_rooms.pb.h> #include <libcockatrice/protocol/pb/event_list_rooms.pb.h>
#include <libcockatrice/protocol/pb/event_server_message.pb.h> #include <libcockatrice/protocol/pb/event_server_message.pb.h>
#include <libcockatrice/protocol/pb/response_join_room.pb.h> #include <libcockatrice/protocol/pb/response_join_room.pb.h>
#include <libcockatrice/protocol/pb/room_commands.pb.h>
#include <libcockatrice/protocol/pb/session_commands.pb.h> #include <libcockatrice/protocol/pb/session_commands.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
@ -185,25 +186,37 @@ void TabServer::processServerMessageEvent(const Event_ServerMessage &event)
void TabServer::joinRoom(int id, bool setCurrent) void TabServer::joinRoom(int id, bool setCurrent)
{ {
TabRoom *room = tabSupervisor->getRoomTabs().value(id); TabRoom *room = tabSupervisor->getRoomTabs().value(id);
if (!room) { if (room) {
Command_JoinRoom cmd; if (setCurrent) {
cmd.set_room_id(id); tabSupervisor->setCurrentWidget((QWidget *)room);
}
PendingCommand *pend = client->prepareSessionCommand(cmd);
pend->setExtraData(setCurrent);
connect(pend, &PendingCommand::finished, this,
[this, id](const Response &r, const CommandContainer &c, const QVariant &v) {
joinRoomFinished(r, c, v, id);
});
client->sendCommand(pend);
return; return;
} }
if (setCurrent) { auto pendingIt = pendingRoomJoins.find(id);
tabSupervisor->setCurrentWidget((QWidget *)room); if (pendingIt != pendingRoomJoins.end()) {
// A join for this room is already in flight: the room tab opens when its response
// arrives. Fold the new request into the pending one so that, for example, clicking
// a room the selector is auto-joining does not send a second Command_JoinRoom - the
// server would reject that duplicate with RespContextError.
if (setCurrent) {
pendingIt.value() = true;
}
return;
} }
pendingRoomJoins.insert(id, setCurrent);
Command_JoinRoom cmd;
cmd.set_room_id(id);
PendingCommand *pend = client->prepareSessionCommand(cmd);
pend->setExtraData(setCurrent);
connect(
pend, &PendingCommand::finished, this,
[this, id](const Response &r, const CommandContainer &c, const QVariant &v) { joinRoomFinished(r, c, v, id); });
client->sendCommand(pend);
} }
void TabServer::joinRoomFinished(const Response &r, void TabServer::joinRoomFinished(const Response &r,
@ -211,34 +224,72 @@ void TabServer::joinRoomFinished(const Response &r,
const QVariant &extraData, const QVariant &extraData,
int roomId) int roomId)
{ {
const bool setCurrent = pendingRoomJoins.value(roomId, extraData.toBool());
pendingRoomJoins.remove(roomId);
const bool healedJoin = healedRoomJoins.contains(roomId);
healedRoomJoins.remove(roomId);
switch (r.response_code()) { switch (r.response_code()) {
case Response::RespOk: case Response::RespOk:
break; break;
case Response::RespNameNotFound: case Response::RespNameNotFound:
QMessageBox::critical(this, tr("Error"), if (setCurrent) {
tr("Failed to join the server room: it doesn't exist on the server.")); QMessageBox::critical(this, tr("Error"),
tr("Failed to join the server room: it doesn't exist on the server."));
}
emit roomJoinFailed(roomId); emit roomJoinFailed(roomId);
return; return;
case Response::RespContextError: case Response::RespContextError:
QMessageBox::critical( if (healedJoin) {
this, tr("Error"), // The rejoin below was already answered and the server still rejects the join, so
tr("The server thinks you are in the server room but your client is unable to display it. " // the stale-membership heal cannot help: surface the error. The guard was already
"Try restarting your client.")); // released above so a later user-initiated join may try a fresh heal.
emit roomJoinFailed(roomId); if (setCurrent) {
QMessageBox::critical(
this, tr("Error"),
tr("The server thinks you are in the server room but your client is unable to display it. "
"Try restarting your client."));
}
emit roomJoinFailed(roomId);
return;
}
// The server already had us registered in the room even though no tab was open,
// usually because two join attempts for the same room overlapped. Leaving and
// rejoining makes the server reply with a fresh RespOk so the tab is displayed
// without requiring a client restart. The guard above covers exactly the rejoin that
// leaveAndRejoinRoom triggers, so a server that keeps replying with RespContextError
// gets one heal attempt per join instead of an endless recursion.
healedRoomJoins.insert(roomId);
leaveAndRejoinRoom(roomId, setCurrent);
return; return;
case Response::RespUserLevelTooLow: case Response::RespUserLevelTooLow:
QMessageBox::critical(this, tr("Error"), if (setCurrent) {
tr("You do not have the required permission to join this server room.")); QMessageBox::critical(this, tr("Error"),
tr("You do not have the required permission to join this server room."));
}
emit roomJoinFailed(roomId); emit roomJoinFailed(roomId);
return; return;
default: default:
QMessageBox::critical( if (setCurrent) {
this, tr("Error"), QMessageBox::critical(
tr("Failed to join the server room due to an unknown error: %1.").arg(r.response_code())); this, tr("Error"),
tr("Failed to join the server room due to an unknown error: %1.").arg(r.response_code()));
}
emit roomJoinFailed(roomId); emit roomJoinFailed(roomId);
return; return;
} }
const Response_JoinRoom &resp = r.GetExtension(Response_JoinRoom::ext); const Response_JoinRoom &resp = r.GetExtension(Response_JoinRoom::ext);
emit roomJoined(resp.room_info(), extraData.toBool()); emit roomJoined(resp.room_info(), setCurrent);
}
void TabServer::leaveAndRejoinRoom(int roomId, bool setCurrent)
{
// Clear the stale room membership server-side. The leave is sent before the rejoin below,
// so the server no longer considers us a member by the time the join arrives. The leave
// response is intentionally not awaited: commands are processed in send order on the
// connection, and a failed leave (RespNotInRoom) only means the membership was already gone.
client->sendCommand(client->prepareRoomCommand(Command_LeaveRoom(), roomId));
joinRoom(roomId, setCurrent);
} }

View file

@ -10,6 +10,8 @@
#include "tab.h" #include "tab.h"
#include <QGroupBox> #include <QGroupBox>
#include <QHash>
#include <QSet>
#include <QTextBrowser> #include <QTextBrowser>
#include <QTreeWidget> #include <QTreeWidget>
@ -58,10 +60,17 @@ private slots:
int roomId); int roomId);
private: private:
void leaveAndRejoinRoom(int roomId, bool setCurrent);
AbstractClient *client; AbstractClient *client;
RoomSelector *roomSelector; RoomSelector *roomSelector;
QTextBrowser *serverInfoBox; QTextBrowser *serverInfoBox;
bool shouldEmitUpdate = false; bool shouldEmitUpdate = false;
/** Room ids with a join command in flight, mapped to whether the tab should be focused once it opens. */
QHash<int, bool> pendingRoomJoins;
/** Room ids for which a stale-membership heal (leave + rejoin) is currently in flight. Released as soon as the
* rejoin has been answered, so a heal is attempted at most once per join. */
QSet<int> healedRoomJoins;
public: public:
TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client); TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client);

View file

@ -1451,6 +1451,11 @@ bool TabSupervisor::getAdminLocked() const
return tabAdmin->getLocked(); return tabAdmin->getLocked();
} }
bool TabSupervisor::canOverrideGameRestrictions() const
{
return !getAdminLocked() || (userInfo->user_level() & ServerInfo_User::IsJudge);
}
void TabSupervisor::processNotifyUserEvent(const Event_NotifyUser &event) void TabSupervisor::processNotifyUserEvent(const Event_NotifyUser &event)
{ {

View file

@ -171,6 +171,7 @@ public:
[[nodiscard]] QList<GameInviteOption> getGameInviteLinksForRoom(int roomId) const; [[nodiscard]] QList<GameInviteOption> getGameInviteLinksForRoom(int roomId) const;
void sendInviteToUser(const QString &userName, const QString &inviteText); void sendInviteToUser(const QString &userName, const QString &inviteText);
[[nodiscard]] bool getAdminLocked() const; [[nodiscard]] bool getAdminLocked() const;
[[nodiscard]] bool canOverrideGameRestrictions() const;
void closeEvent(QCloseEvent *event) override; void closeEvent(QCloseEvent *event) override;
bool switchToGameTabIfAlreadyExists(const int gameId); bool switchToGameTabIfAlreadyExists(const int gameId);
static void actShowPopup(const QString &message); static void actShowPopup(const QString &message);

View file

@ -33,6 +33,7 @@
#include "../interface/widgets/dialogs/dlg_update.h" #include "../interface/widgets/dialogs/dlg_update.h"
#include "../interface/widgets/dialogs/dlg_view_log.h" #include "../interface/widgets/dialogs/dlg_view_log.h"
#include "../interface/widgets/onboarding/first_run_wizard.h" #include "../interface/widgets/onboarding/first_run_wizard.h"
#include "../interface/widgets/settings_page/general_settings_page.h"
#include "../interface/widgets/tabs/tab_game.h" #include "../interface/widgets/tabs/tab_game.h"
#include "../interface/widgets/tabs/tab_server.h" #include "../interface/widgets/tabs/tab_server.h"
#include "../interface/widgets/tabs/tab_supervisor.h" #include "../interface/widgets/tabs/tab_supervisor.h"
@ -246,6 +247,8 @@ void MainWindow::actFullScreen(bool checked)
void MainWindow::actSettings() void MainWindow::actSettings()
{ {
DlgSettings dlg(this); DlgSettings dlg(this);
auto *generalPage = qobject_cast<GeneralSettingsPage *>(dlg.page(DlgSettings::GeneralPage));
connect(generalPage, &GeneralSettingsPage::cardDatabaseUpdateRequested, this, &MainWindow::actCheckCardUpdates);
dlg.exec(); dlg.exec();
} }

View file

@ -2,7 +2,7 @@
# #
# Add themes subfolders # Add themes subfolders
set(defthemes Default Fabric Fusion Leather Plasma VelvetMarble) set(defthemes Fabric Fusion Leather Plasma VelvetMarble System)
if(UNIX) if(UNIX)
if(APPLE) if(APPLE)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

View file

@ -0,0 +1,74 @@
# Fabric — dark identity palette (navy-cloth chrome)
[Palette]
WindowText = #e8ecf2
Button = #2b374a
Light = #3a4759
Midlight = #333f50
Dark = #141a23
Mid = #252c38
Text = #e8ecf2
BrightText = #8ec2ff
ButtonText = #f2f4f8
Base = #222c3d
Window = #18202e
Shadow = #0b0f15
Highlight = #4a82e6
HighlightedText = #ffffff
Link = #7fb0f2
LinkVisited = #a98ad9
AlternateBase = #1d2636
ToolTipBase = #2b3546
ToolTipText = #f2f4f8
PlaceholderText = #6ee8ecf2
Accent = #4a82e6
[Palette.Disabled]
WindowText = #9d9d9d
Button = #18202e
Light = #3a4759
Midlight = #333f50
Dark = #141a23
Mid = #252c38
Text = #9d9d9d
BrightText = #8ec2ff
ButtonText = #787878
Base = #18202e
Window = #18202e
Shadow = #0b0f15
Highlight = #222d40
HighlightedText = #9d9d9d
Link = #308cc6
LinkVisited = #b450ff
AlternateBase = #1d2636
ToolTipBase = #2b3546
ToolTipText = #f2f4f8
PlaceholderText = #6ee8ecf2
Accent = #9d9d9d
[Palette.Inactive]
WindowText = #e8ecf2
Button = #2b374a
Light = #3a4759
Midlight = #333f50
Dark = #141a23
Mid = #252c38
Text = #e8ecf2
BrightText = #8ec2ff
ButtonText = #f2f4f8
Base = #222c3d
Window = #18202e
Shadow = #0b0f15
Highlight = #222d40
HighlightedText = #ffffff
Link = #7fb0f2
LinkVisited = #a98ad9
AlternateBase = #1d2636
ToolTipBase = #2b3546
ToolTipText = #f2f4f8
PlaceholderText = #6ee8ecf2
Accent = #4a82e6
[AppColors]
AccentStrong = #3a6fd6
AccentSoft = #8fb8f2

View file

@ -0,0 +1,74 @@
# Fabric — light identity palette (navy-cloth chrome)
[Palette]
WindowText = #1c2330
Button = #c8d6ea
Light = #ffffff
Midlight = #b9c6dc
Dark = #8fa0bd
Mid = #a3b3cd
Text = #1c2330
BrightText = #1d4fa0
ButtonText = #141c29
Base = #f6f8fc
Window = #dbe3ef
Shadow = #5a6a85
Highlight = #2f6fd6
HighlightedText = #ffffff
Link = #2555b0
LinkVisited = #6a4bb8
AlternateBase = #e9eef7
ToolTipBase = #e9eff8
ToolTipText = #141c29
PlaceholderText = #7a1c2330
Accent = #2f6fd6
[Palette.Disabled]
WindowText = #787878
Button = #dbe3ef
Light = #ffffff
Midlight = #b9c6dc
Dark = #8fa0bd
Mid = #a3b3cd
Text = #787878
BrightText = #1d4fa0
ButtonText = #969696
Base = #dbe3ef
Window = #dbe3ef
Shadow = #5a6a85
Highlight = #cdd8e9
HighlightedText = #787878
Link = #0000ff
LinkVisited = #ff00ff
AlternateBase = #e9eef7
ToolTipBase = #e9eff8
ToolTipText = #141c29
PlaceholderText = #7a1c2330
Accent = #787878
[Palette.Inactive]
WindowText = #1c2330
Button = #c8d6ea
Light = #ffffff
Midlight = #b9c6dc
Dark = #8fa0bd
Mid = #a3b3cd
Text = #1c2330
BrightText = #1d4fa0
ButtonText = #141c29
Base = #f6f8fc
Window = #dbe3ef
Shadow = #5a6a85
Highlight = #cdd8e9
HighlightedText = #000000
Link = #2555b0
LinkVisited = #6a4bb8
AlternateBase = #e9eef7
ToolTipBase = #e9eff8
ToolTipText = #141c29
PlaceholderText = #7a1c2330
Accent = #2f6fd6
[AppColors]
AccentStrong = #3a6fd6
AccentSoft = #8fb8f2

View file

@ -0,0 +1,5 @@
[Appearance]
ColorScheme = System
[Style]
Name = Fusion

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Before After
Before After

View file

@ -11,15 +11,15 @@ ButtonText = #ffffffff
Base = #ff2d2d2d Base = #ff2d2d2d
Window = #ff1e1e1e Window = #ff1e1e1e
Shadow = #ff000000 Shadow = #ff000000
Highlight = #ff148c3c Highlight = #ff139740
HighlightedText = #ffffffff HighlightedText = #ffffffff
Link = #ff00f652 Link = #ffc9fd62
LinkVisited = #ff00d346 LinkVisited = #ff9ad43e
AlternateBase = #ff353535 AlternateBase = #ff353535
ToolTipBase = #ff3c3c3c ToolTipBase = #ff3c3c3c
ToolTipText = #ffd4d4d4 ToolTipText = #ffd4d4d4
PlaceholderText = #80ffffff PlaceholderText = #80ffffff
Accent = #ff00d346 Accent = #ffc9fd62
[Palette.Disabled] [Palette.Disabled]
WindowText = #ff9d9d9d WindowText = #ff9d9d9d
@ -34,7 +34,7 @@ ButtonText = #ff9d9d9d
Base = #ff1e1e1e Base = #ff1e1e1e
Window = #ff1e1e1e Window = #ff1e1e1e
Shadow = #ff000000 Shadow = #ff000000
Highlight = #ff148c3c Highlight = #ff139740
HighlightedText = #ffffffff HighlightedText = #ffffffff
Link = #ff308cc6 Link = #ff308cc6
LinkVisited = #ffff00ff LinkVisited = #ffff00ff
@ -59,11 +59,15 @@ Window = #ff1e1e1e
Shadow = #ff000000 Shadow = #ff000000
Highlight = #ff1e1e1e Highlight = #ff1e1e1e
HighlightedText = #ffffffff HighlightedText = #ffffffff
Link = #ff00f652 Link = #ffc9fd62
LinkVisited = #ff00d346 LinkVisited = #ff9ad43e
AlternateBase = #ff353535 AlternateBase = #ff353535
ToolTipBase = #ff3c3c3c ToolTipBase = #ff3c3c3c
ToolTipText = #ffd4d4d4 ToolTipText = #ffd4d4d4
PlaceholderText = #80ffffff PlaceholderText = #80ffffff
Accent = #ff1e1e1e Accent = #ff1e1e1e
[AppColors]
AccentStrong = #ff139740
AccentSoft = #ffc9fd62

View file

@ -11,15 +11,15 @@ ButtonText = #ff000000
Base = #ffffffff Base = #ffffffff
Window = #fff0f0f0 Window = #fff0f0f0
Shadow = #ff696969 Shadow = #ff696969
Highlight = #ff148c3c Highlight = #ff139740
HighlightedText = #ffffffff HighlightedText = #ffffffff
Link = #ff0d5f28 Link = #ff0e6b31
LinkVisited = #ff08401b LinkVisited = #ff0a521f
AlternateBase = #ffe9e7e3 AlternateBase = #ffe9e7e3
ToolTipBase = #ffffffdc ToolTipBase = #ffffffdc
ToolTipText = #ff000000 ToolTipText = #ff000000
PlaceholderText = #80000000 PlaceholderText = #80000000
Accent = #ff107532 Accent = #ff139740
[Palette.Disabled] [Palette.Disabled]
WindowText = #ff787878 WindowText = #ff787878
@ -34,7 +34,7 @@ ButtonText = #ff787878
Base = #fff0f0f0 Base = #fff0f0f0
Window = #fff0f0f0 Window = #fff0f0f0
Shadow = #ff000000 Shadow = #ff000000
Highlight = #ff148c3c Highlight = #ff139740
HighlightedText = #ffffffff HighlightedText = #ffffffff
Link = #ff0000ff Link = #ff0000ff
LinkVisited = #ffff00ff LinkVisited = #ffff00ff
@ -59,11 +59,15 @@ Window = #fff0f0f0
Shadow = #ff696969 Shadow = #ff696969
Highlight = #fff0f0f0 Highlight = #fff0f0f0
HighlightedText = #ff000000 HighlightedText = #ff000000
Link = #ff0d5f28 Link = #ff0e6b31
LinkVisited = #ff08401b LinkVisited = #ff0a521f
AlternateBase = #ffe9e7e3 AlternateBase = #ffe9e7e3
ToolTipBase = #ffffffdc ToolTipBase = #ffffffdc
ToolTipText = #ff000000 ToolTipText = #ff000000
PlaceholderText = #80000000 PlaceholderText = #80000000
Accent = #fff0f0f0 Accent = #fff0f0f0
[AppColors]
AccentStrong = #ff139740
AccentSoft = #ffc9fd62

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View file

@ -0,0 +1,74 @@
# Leather — dark identity palette (black-brown chrome with brass accents)
[Palette]
WindowText = #eee9e2
Button = #332d26
Light = #4a4239
Midlight = #3d3730
Dark = #131009
Mid = #26221c
Text = #eee9e2
BrightText = #e3c27e
ButtonText = #f5f1ea
Base = #26221d
Window = #1d1a16
Shadow = #0d0b07
Highlight = #4a5f8f
HighlightedText = #ffffff
Link = #cfa263
LinkVisited = #9a7db8
AlternateBase = #211d19
ToolTipBase = #37302a
ToolTipText = #f5f1ea
PlaceholderText = #6eeee9e2
Accent = #c9995a
[Palette.Disabled]
WindowText = #9d9d9d
Button = #1d1a16
Light = #4a4239
Midlight = #3d3730
Dark = #131009
Mid = #26221c
Text = #9d9d9d
BrightText = #e3c27e
ButtonText = #787878
Base = #1d1a16
Window = #1d1a16
Shadow = #0d0b07
Highlight = #2d2822
HighlightedText = #9d9d9d
Link = #308cc6
LinkVisited = #b450ff
AlternateBase = #211d19
ToolTipBase = #37302a
ToolTipText = #f5f1ea
PlaceholderText = #6eeee9e2
Accent = #9d9d9d
[Palette.Inactive]
WindowText = #eee9e2
Button = #332d26
Light = #4a4239
Midlight = #3d3730
Dark = #131009
Mid = #26221c
Text = #eee9e2
BrightText = #e3c27e
ButtonText = #f5f1ea
Base = #26221d
Window = #1d1a16
Shadow = #0d0b07
Highlight = #2d2822
HighlightedText = #ffffff
Link = #cfa263
LinkVisited = #9a7db8
AlternateBase = #211d19
ToolTipBase = #37302a
ToolTipText = #f5f1ea
PlaceholderText = #6eeee9e2
Accent = #c9995a
[AppColors]
AccentStrong = #b7823b
AccentSoft = #e4c58f

View file

@ -0,0 +1,74 @@
# Leather — light identity palette (black-brown chrome with brass accents)
[Palette]
WindowText = #2a2114
Button = #dfcbb0
Light = #ffffff
Midlight = #c6b08c
Dark = #a18a64
Mid = #b29a74
Text = #2a2114
BrightText = #7a531c
ButtonText = #1f1710
Base = #fdf8ee
Window = #f0e2cb
Shadow = #5d4d33
Highlight = #34508c
HighlightedText = #ffffff
Link = #8a5e1e
LinkVisited = #6b5190
AlternateBase = #f5ebd7
ToolTipBase = #fff7e8
ToolTipText = #1f1710
PlaceholderText = #7a2a2114
Accent = #a5712f
[Palette.Disabled]
WindowText = #787878
Button = #f0e2cb
Light = #ffffff
Midlight = #c6b08c
Dark = #a18a64
Mid = #b29a74
Text = #787878
BrightText = #7a531c
ButtonText = #969696
Base = #f0e2cb
Window = #f0e2cb
Shadow = #5d4d33
Highlight = #ecd9bb
HighlightedText = #787878
Link = #0000ff
LinkVisited = #ff00ff
AlternateBase = #f5ebd7
ToolTipBase = #fff7e8
ToolTipText = #1f1710
PlaceholderText = #7a2a2114
Accent = #787878
[Palette.Inactive]
WindowText = #2a2114
Button = #dfcbb0
Light = #ffffff
Midlight = #c6b08c
Dark = #a18a64
Mid = #b29a74
Text = #2a2114
BrightText = #7a531c
ButtonText = #1f1710
Base = #fdf8ee
Window = #f0e2cb
Shadow = #5d4d33
Highlight = #ecd9bb
HighlightedText = #000000
Link = #8a5e1e
LinkVisited = #6b5190
AlternateBase = #f5ebd7
ToolTipBase = #fff7e8
ToolTipText = #1f1710
PlaceholderText = #7a2a2114
Accent = #a5712f
[AppColors]
AccentStrong = #b7823b
AccentSoft = #e4c58f

View file

@ -0,0 +1,5 @@
[Appearance]
ColorScheme = System
[Style]
Name = Fusion

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View file

@ -0,0 +1,74 @@
# Plasma — dark identity palette (electric violet chrome with cyan sparks)
[Palette]
WindowText = #eeebff
Button = #2a2440
Light = #453d63
Midlight = #383252
Dark = #0e0c16
Mid = #211e31
Text = #eeebff
BrightText = #7fd7ff
ButtonText = #f6f4ff
Base = #1e1a2e
Window = #151220
Shadow = #08070e
Highlight = #6a4df0
HighlightedText = #ffffff
Link = #37c7e8
LinkVisited = #9b7aff
AlternateBase = #1a1726
ToolTipBase = #2c2745
ToolTipText = #f6f4ff
PlaceholderText = #6eeeebff
Accent = #6a4df0
[Palette.Disabled]
WindowText = #9d9d9d
Button = #151220
Light = #453d63
Midlight = #383252
Dark = #0e0c16
Mid = #211e31
Text = #9d9d9d
BrightText = #7fd7ff
ButtonText = #787878
Base = #151220
Window = #151220
Shadow = #08070e
Highlight = #211c32
HighlightedText = #9d9d9d
Link = #308cc6
LinkVisited = #b450ff
AlternateBase = #1a1726
ToolTipBase = #2c2745
ToolTipText = #f6f4ff
PlaceholderText = #6eeeebff
Accent = #9d9d9d
[Palette.Inactive]
WindowText = #eeebff
Button = #2a2440
Light = #453d63
Midlight = #383252
Dark = #0e0c16
Mid = #211e31
Text = #eeebff
BrightText = #7fd7ff
ButtonText = #f6f4ff
Base = #1e1a2e
Window = #151220
Shadow = #08070e
Highlight = #211c32
HighlightedText = #ffffff
Link = #37c7e8
LinkVisited = #9b7aff
AlternateBase = #1a1726
ToolTipBase = #2c2745
ToolTipText = #f6f4ff
PlaceholderText = #6eeeebff
Accent = #6a4df0
[AppColors]
AccentStrong = #5b3ee0
AccentSoft = #a28bf7

View file

@ -0,0 +1,74 @@
# Plasma — light identity palette (electric violet chrome with cyan sparks)
[Palette]
WindowText = #1d1830
Button = #cfc6f0
Light = #ffffff
Midlight = #b7adde
Dark = #9589c8
Mid = #a49ad2
Text = #1d1830
BrightText = #10406e
ButtonText = #120d26
Base = #f9f7ff
Window = #e3dcf7
Shadow = #554c85
Highlight = #5a3ee2
HighlightedText = #ffffff
Link = #0f8fb5
LinkVisited = #6a45d6
AlternateBase = #ece8fa
ToolTipBase = #f5f3ff
ToolTipText = #120d26
PlaceholderText = #7a1d1830
Accent = #5a3ee2
[Palette.Disabled]
WindowText = #787878
Button = #e3dcf7
Light = #ffffff
Midlight = #b7adde
Dark = #9589c8
Mid = #a49ad2
Text = #787878
BrightText = #10406e
ButtonText = #969696
Base = #e3dcf7
Window = #e3dcf7
Shadow = #554c85
Highlight = #d6ccf3
HighlightedText = #787878
Link = #0000ff
LinkVisited = #ff00ff
AlternateBase = #ece8fa
ToolTipBase = #f5f3ff
ToolTipText = #120d26
PlaceholderText = #7a1d1830
Accent = #787878
[Palette.Inactive]
WindowText = #1d1830
Button = #cfc6f0
Light = #ffffff
Midlight = #b7adde
Dark = #9589c8
Mid = #a49ad2
Text = #1d1830
BrightText = #10406e
ButtonText = #120d26
Base = #f9f7ff
Window = #e3dcf7
Shadow = #554c85
Highlight = #d6ccf3
HighlightedText = #000000
Link = #0f8fb5
LinkVisited = #6a45d6
AlternateBase = #ece8fa
ToolTipBase = #f5f3ff
ToolTipText = #120d26
PlaceholderText = #7a1d1830
Accent = #5a3ee2
[AppColors]
AccentStrong = #5b3ee0
AccentSoft = #a28bf7

View file

@ -0,0 +1,5 @@
[Appearance]
ColorScheme = System
[Style]
Name = Fusion

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

View file

@ -61,3 +61,7 @@ ToolTipBase = #ffffffdc
ToolTipText = #ff000000 ToolTipText = #ff000000
PlaceholderText = #6effffff PlaceholderText = #6effffff
[AppColors]
AccentStrong = #ff148c3c
AccentSoft = #ff78c850

View file

@ -0,0 +1,67 @@
[Palette]
WindowText = #ff000000
Button = #fff0f0f0
Light = #ffffffff
Midlight = #ffe3e3e3
Dark = #ffa0a0a0
Mid = #ffa0a0a0
Text = #ff000000
BrightText = #ffffffff
ButtonText = #ff000000
Base = #ffffffff
Window = #fff0f0f0
Shadow = #ff696969
HighlightedText = #ffffffff
Link = #ff0d5f28
LinkVisited = #ff08401b
AlternateBase = #ffe9e7e3
ToolTipBase = #ffffffdc
ToolTipText = #ff000000
PlaceholderText = #80000000
[Palette.Disabled]
WindowText = #ff787878
Button = #fff0f0f0
Light = #ffffffff
Midlight = #fff7f7f7
Dark = #ffa0a0a0
Mid = #ffa0a0a0
Text = #ff787878
BrightText = #ffffffff
ButtonText = #ff787878
Base = #fff0f0f0
Window = #fff0f0f0
Shadow = #ff000000
HighlightedText = #ffffffff
Link = #ff0000ff
LinkVisited = #ffff00ff
AlternateBase = #fff7f7f7
ToolTipBase = #ffffffdc
ToolTipText = #ff000000
PlaceholderText = #80000000
[Palette.Inactive]
WindowText = #ff000000
Button = #fff0f0f0
Light = #ffffffff
Midlight = #ffe3e3e3
Dark = #ffa0a0a0
Mid = #ffa0a0a0
Text = #ff000000
BrightText = #ffffffff
ButtonText = #ff000000
Base = #ffffffff
Window = #fff0f0f0
Shadow = #ff696969
HighlightedText = #ff000000
Link = #ff0d5f28
LinkVisited = #ff08401b
AlternateBase = #ffe9e7e3
ToolTipBase = #ffffffdc
ToolTipText = #ff000000
PlaceholderText = #80000000
[AppColors]
AccentStrong = #ff148c3c
AccentSoft = #ff78c850

View file

@ -2,4 +2,4 @@
ColorScheme = Light ColorScheme = Light
[Style] [Style]
Name = Default Name = System

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