diff --git a/.ci/Debian12/Dockerfile b/.ci/Debian12/Dockerfile index fc756aac2..e3df94ab5 100644 --- a/.ci/Debian12/Dockerfile +++ b/.ci/Debian12/Dockerfile @@ -18,12 +18,12 @@ RUN apt-get update && \ libssl-dev \ ninja-build \ protobuf-compiler \ - qt6-declarative-dev \ qt6-image-formats-plugins \ qt6-l10n-tools \ qt6-multimedia-dev \ - qt6-shadertools-dev \ + qt6-declarative-dev \ qt6-svg-dev \ + qt6-shadertools-dev \ qt6-tools-dev \ qt6-tools-dev-tools \ qt6-websockets-dev \ diff --git a/.ci/Debian13/Dockerfile b/.ci/Debian13/Dockerfile index bdecb56df..60e490c98 100644 --- a/.ci/Debian13/Dockerfile +++ b/.ci/Debian13/Dockerfile @@ -19,12 +19,12 @@ RUN apt-get update && \ libssl-dev \ ninja-build \ protobuf-compiler \ - qt6-declarative-dev \ qt6-image-formats-plugins \ qt6-l10n-tools \ qt6-multimedia-dev \ - qt6-shadertools-dev \ + qt6-declarative-dev \ qt6-svg-dev \ + qt6-shadertools-dev \ qt6-tools-dev \ qt6-tools-dev-tools \ qt6-websockets-dev \ diff --git a/.ci/Fedora43/Dockerfile b/.ci/Fedora43/Dockerfile index 463da5a51..4005bbf67 100644 --- a/.ci/Fedora43/Dockerfile +++ b/.ci/Fedora43/Dockerfile @@ -9,7 +9,7 @@ RUN dnf install -y \ ninja-build \ openssl-devel \ protobuf-devel \ - qt6-{qtdeclarative,qtmultimedia,qtshadertools,qtsvg,qttools,qtwebsockets}-devel \ + qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ qt6-qtimageformats \ rpm-build \ xz-devel \ diff --git a/.ci/Fedora44/Dockerfile b/.ci/Fedora44/Dockerfile index 62238e760..e0224cdc6 100644 --- a/.ci/Fedora44/Dockerfile +++ b/.ci/Fedora44/Dockerfile @@ -9,7 +9,7 @@ RUN dnf install -y \ ninja-build \ openssl-devel \ protobuf-devel \ - qt6-{qtdeclarative,qtmultimedia,qtshadertools,qtsvg,qttools,qtwebsockets}-devel \ + qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ qt6-qtimageformats \ rpm-build \ xz-devel \ diff --git a/.ci/Ubuntu24.04/Dockerfile b/.ci/Ubuntu24.04/Dockerfile index 715997474..10adc5e64 100644 --- a/.ci/Ubuntu24.04/Dockerfile +++ b/.ci/Ubuntu24.04/Dockerfile @@ -18,12 +18,12 @@ RUN apt-get update && \ libssl-dev \ ninja-build \ protobuf-compiler \ - qt6-declarative-dev \ qt6-image-formats-plugins \ qt6-l10n-tools \ qt6-multimedia-dev \ - qt6-shadertools-dev \ + qt6-declarative-dev \ qt6-svg-dev \ + qt6-shadertools-dev \ qt6-tools-dev \ qt6-tools-dev-tools \ qt6-websockets-dev \ diff --git a/.ci/Ubuntu26.04/Dockerfile b/.ci/Ubuntu26.04/Dockerfile index 96dd10763..1b6cf825f 100644 --- a/.ci/Ubuntu26.04/Dockerfile +++ b/.ci/Ubuntu26.04/Dockerfile @@ -19,12 +19,12 @@ RUN apt-get update && \ libssl-dev \ ninja-build \ protobuf-compiler \ - qt6-declarative-dev \ qt6-image-formats-plugins \ qt6-l10n-tools \ qt6-multimedia-dev \ - qt6-shadertools-dev \ + qt6-declarative-dev \ qt6-svg-dev \ + qt6-shadertools-dev \ qt6-tools-dev \ qt6-tools-dev-tools \ qt6-websockets-dev \ diff --git a/.ci/compile.sh b/.ci/compile.sh index f20432893..bd8c900c8 100755 --- a/.ci/compile.sh +++ b/.ci/compile.sh @@ -327,32 +327,4 @@ if [[ $MAKE_PACKAGE ]]; then BUILD_DIR="$BUILD_DIR" .ci/name_build.sh "$PACKAGE_SUFFIX" echo "::endgroup::" fi - - if [[ $RUNNER_OS == Windows ]]; then - echo "::group::Check installer for build-tree artifacts" - cd "$BUILD_DIR" - package="$(find . -maxdepth 1 -type f -name 'Cockatrice-*.exe' -print -quit)" - if [[ ! $package ]]; then - echo "::error file=$0::Could not find installer to inspect" - exit 1 - fi - seven_zip="$(command -v 7z || true)" - if [[ ! $seven_zip ]]; then - seven_zip="/c/Program Files/7-Zip/7z.exe" - fi - if [[ ! -f $seven_zip ]]; then - echo "::warning file=$0::7-Zip not found, skipping installer content check" - else - echo "Inspecting $package" - # Fail the build if the installer contains any path left behind by the MSBuild or - # Qt AUTOMOC tooling (build-tree artifacts must live in the build dir, not the install) - if "$seven_zip" l "$package" | - grep -E "_autogen|\.dir[\\/]|\.tlog|(^|[\\/])x64[\\/]|(^|[\\/])\.qt[\\/]|(^|[\\/])\.qsb[\\/]|(^|[\\/])\.lupdate[\\/]|CMakeFiles"; then - echo "::error file=$0::Installer contains build-tree artifacts" - exit 1 - fi - echo "Installer content is clean" - fi - echo "::endgroup::" - fi fi diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index bd528f245..6ca634389 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -272,8 +272,8 @@ jobs: make_package: 1 override_target: 13 package_suffix: "-macOS13_Intel" - qt_version: 6.11.* - qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets + qt_version: 6.11.1 + qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools soc: Intel type: Release use_ccache: 1 @@ -288,8 +288,8 @@ jobs: make_package: 1 override_target: 14 package_suffix: "-macOS14" - qt_version: 6.11.* - qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets + qt_version: 6.11.1 + qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools soc: Apple type: Release use_ccache: 1 @@ -304,8 +304,8 @@ jobs: make_package: 1 override_target: 15 package_suffix: "-macOS15" - qt_version: 6.11.* - qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets + qt_version: 6.11.1 + qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools soc: Apple type: Release use_ccache: 1 @@ -317,8 +317,8 @@ jobs: ccache_eviction_age: 7d cmake_generator: Ninja - qt_version: 6.11.* - qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets + qt_version: 6.11.1 + qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools soc: Apple type: Debug use_ccache: 1 @@ -332,8 +332,8 @@ jobs: cmake_generator_platform: x64 make_package: 1 package_suffix: "-Win10" - qt_version: 6.11.* - qt_modules: qtimageformats qtmultimedia qtshadertools qtwebsockets + qt_version: 6.11.1 + qt_modules: qtimageformats qtmultimedia qtwebsockets qtshadertools type: Release name: ${{ matrix.os }} ${{ matrix.target }}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} @@ -368,20 +368,18 @@ jobs: key: ccache-${{ matrix.runner }}_${{ matrix.override_target }}-Xcode${{ matrix.xcode }} path: ${{ env.CCACHE_DIR }} - - name: "[macOS] Install aqtinstall" - if: matrix.os == 'macOS' + - name: "Install aqtinstall" run: pipx install aqtinstall # Resolve given wildcard versions (e.g. Qt 6.6.*) to latest version via aqtinstall to avoid stale caches on new releases - - name: "[macOS] Resolve latest Qt from ${{ matrix.qt_version }} input" - if: matrix.os == 'macOS' + - name: "Resolve latest Qt patch version" env: QT_VERSION: ${{ matrix.qt_version }} id: resolve_qt_version shell: bash run: .ci/resolve_latest_aqt_qt_version.sh "$QT_VERSION" - - name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }}" + - name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries" if: matrix.os == 'macOS' id: restore_qt uses: actions/cache/restore@v6 @@ -391,22 +389,21 @@ jobs: # Using jurplel/install-qt-action to install Qt without using brew # Qt build using vcpkg either just fails or takes too long to build - - name: "[macOS] Install fat Qt ${{ matrix.qt_version }}" + - name: "[macOS] Install fat Qt ${{ steps.resolve_qt_version.outputs.version }}" if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' uses: jurplel/install-qt-action@v4 with: cache: false - # cache-key-prefix: Qt - dir: ${{ github.workspace }} # thinning script depends on this location + dir: ${{ github.workspace }} modules: ${{ matrix.qt_modules }} - version: ${{ matrix.qt_version }} + version: ${{ steps.resolve_qt_version.outputs.version }} - name: "[macOS] Create thin Qt libraries" if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' run: .ci/thin_macos_qtlib.sh - name: "[macOS] Cache thin Qt libraries" - if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' && github.ref == 'refs/heads/master' + if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' uses: actions/cache/save@v6 with: key: ${{ steps.restore_qt.outputs.cache-primary-key }} @@ -418,10 +415,10 @@ jobs: with: # Qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released aqtsource: git+https://github.com/miurahr/aqtinstall.git - cache: ${{ github.ref == 'refs/heads/master' }} + cache: true cache-key-prefix: Qt modules: ${{ matrix.qt_modules }} - version: ${{ matrix.qt_version }} + version: ${{ steps.resolve_qt_version.outputs.version }} - name: "[Windows] Install NSIS" if: matrix.os == 'Windows' @@ -453,7 +450,7 @@ jobs: PACKAGE_SUFFIX: '${{ matrix.package_suffix }}' TARGET_MACOS_VERSION: ${{ matrix.override_target }} USE_CCACHE: ${{ matrix.use_ccache }} - VCPKG_BINARY_SOURCES: "clear;files,${{ steps.vcpkg-cache.outputs.path }},${{ case(github.ref == 'refs/heads/master', 'readwrite', 'read') }}" + VCPKG_BINARY_SOURCES: 'clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite' VCPKG_DISABLE_METRICS: 1 VCPKG_FEATURE_FLAGS: dependencygraph run: .ci/compile.sh --server --test --vcpkg diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 967d94c58..255e8b045 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -76,7 +76,7 @@ jobs: uses: docker/build-push-action@v7 with: cache-from: type=gha,scope=${{ env.CACHE_SCOPE }} - cache-to: ${{ case(github.ref == 'refs/heads/master', format('type=gha,mode=max,scope={0}', env.CACHE_SCOPE), '') }} + cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }} context: . platforms: ${{ matrix.platform }} push: false diff --git a/CMakeLists.txt b/CMakeLists.txt index 293e25dd9..0da073464 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -293,7 +293,7 @@ if(UNIX) if(CPACK_GENERATOR STREQUAL "RPM") set(CPACK_RPM_PACKAGE_LICENSE "GPLv2") set(CPACK_RPM_MAIN_COMPONENT "cockatrice") - set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qtimageformats, qt6-qtmultimedia, qt6-qtsvg, qt6-qttools") + set(CPACK_RPM_PACKAGE_REQUIRES "protobuf, qt6-qttools, qt6-qtsvg, qt6-qtmultimedia, qt6-qtimageformats") set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games") set(CPACK_RPM_PACKAGE_URL "http://github.com/Cockatrice/Cockatrice") # stop directories from making package conflicts @@ -311,7 +311,7 @@ if(UNIX) set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) set(CPACK_DEBIAN_PACKAGE_SECTION "games") set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "http://github.com/Cockatrice/Cockatrice") - set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-image-formats-plugins, qt6-qpa-plugins") + set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6multimedia6, libqt6svg6, qt6-qpa-plugins, qt6-image-formats-plugins") set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "libqt6sql6-mysql") # for connecting servatrice to a mysql db endif() endif() diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in index b3cbcece8..5af116470 100644 --- a/cmake/NSIS.template.in +++ b/cmake/NSIS.template.in @@ -387,14 +387,19 @@ SectionEnd Section "un.Application" UnSecApplication SetShellVarContext all + RMDir /r "$INSTDIR\plugins" + RMDir /r "$INSTDIR\sounds" + RMDir /r "$INSTDIR\themes" + RMDir /r "$INSTDIR\translations" + Delete "$INSTDIR\*.exe" + Delete "$INSTDIR\*.dll" + Delete "$INSTDIR\qt.conf" + Delete "$INSTDIR\qdebug.txt" + Delete "$INSTDIR\servatrice.sql" + Delete "$INSTDIR\servatrice.ini.example" + RMDir "$INSTDIR" - ; Remove the entire application directory so any file that is not part of - ; the installed payload (e.g. build-tree artifacts such as *.dir folders, - ; *_autogen and *.tlog files from a build) cannot survive between an - ; uninstall and a fresh reinstall. - RMDir /r "$INSTDIR" - - RMDir /r "$SMPROGRAMS\Cockatrice" + RMDir "$SMPROGRAMS\Cockatrice" DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" SectionEnd diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 9b31310e6..63ccc4e9c 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -45,14 +45,11 @@ set(cockatrice_SOURCES src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp src/interface/widgets/dialogs/dlg_load_remote_deck.cpp src/interface/widgets/dialogs/dlg_local_game_options.cpp - src/interface/widgets/dialogs/dlg_login_prompt.cpp src/interface/widgets/dialogs/dlg_manage_sets.cpp src/interface/widgets/dialogs/dlg_my_reports.cpp src/interface/widgets/dialogs/dlg_register.cpp src/interface/widgets/dialogs/dlg_report_user.cpp src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp - src/interface/widgets/dialogs/dlg_share_deck.cpp - src/interface/widgets/dialogs/dlg_shared_decks_preview.cpp src/interface/widgets/dialogs/dlg_settings.cpp src/interface/widgets/dialogs/dlg_startup_card_check.cpp src/interface/widgets/dialogs/dlg_tip_of_the_day.cpp @@ -60,9 +57,6 @@ set(cockatrice_SOURCES src/interface/widgets/dialogs/dlg_view_log.cpp src/interface/widgets/dialogs/override_printing_warning.cpp src/interface/widgets/dialogs/tip_of_the_day.cpp - src/interface/widgets/deck_share/deck_share_utils.cpp - src/interface/widgets/deck_share/shared_deck_preview_widget.cpp - src/interface/widgets/deck_share/share_bar_widget.cpp src/filters/deck_filter_string.cpp src/filters/filter_builder.cpp src/filters/filter_tree_model.cpp @@ -169,7 +163,6 @@ set(cockatrice_SOURCES src/interface/palette_editor/palette_grid_widget.cpp src/interface/palette_editor/palette_editor_dialog.cpp src/interface/widgets/cards/additional_info/color_identity_widget.cpp - src/interface/widgets/cards/additional_info/deck_color_identity.cpp src/interface/widgets/cards/additional_info/mana_cost_widget.cpp src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp src/interface/widgets/cards/art_crop_attribution.cpp @@ -324,8 +317,6 @@ set(cockatrice_SOURCES src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp - src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.cpp - src/interface/widgets/visual_deck_storage/remote_public_decks_model.cpp src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp @@ -397,7 +388,6 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/tab_logs.cpp src/interface/widgets/tabs/tab_message.cpp src/interface/widgets/tabs/tab_moderation.cpp - src/interface/widgets/tabs/tab_public_decks.cpp src/interface/widgets/tabs/tab_report.cpp src/interface/widgets/tabs/tab_replays.cpp src/interface/widgets/tabs/tab_room.cpp @@ -413,7 +403,6 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp src/interface/widgets/onboarding/banner_shader_config.h - src/interface/widgets/onboarding/brand_colors.h src/interface/widgets/onboarding/first_run_wizard.cpp src/interface/widgets/onboarding/first_run_wizard.h src/interface/widgets/onboarding/first_run_wizard_page.cpp @@ -454,8 +443,6 @@ set(cockatrice_SOURCES src/interface/intents/intent_login.h src/interface/intents/intent_open_server_room_by_name.cpp src/interface/intents/intent_open_server_room_by_name.h - src/interface/intents/intent_open_shared_deck.cpp - src/interface/intents/intent_open_shared_deck.h src/interface/intents/url_parser.cpp src/interface/intents/url_parser.h src/interface/widgets/server/user/user_info_popup.cpp @@ -543,7 +530,6 @@ qt6_add_shaders( "src/interface/widgets/onboarding/shaders" FILES src/interface/widgets/onboarding/shaders/brand_banner.frag - src/interface/widgets/onboarding/shaders/brand_plate.frag ) qt6_add_resources( @@ -668,35 +654,18 @@ if(WIN32) set(qtconf_dest_dir .) install( - DIRECTORY "$/" + DIRECTORY "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/${CMAKE_BUILD_TYPE}/" DESTINATION ./ FILES_MATCHING PATTERN "*.dll" - PATTERN "*.pdb" EXCLUDE - PATTERN "*.dir*" EXCLUDE - PATTERN "*_autogen*" EXCLUDE - PATTERN "*.tlog*" EXCLUDE - PATTERN "CMakeFiles*" EXCLUDE - PATTERN "x64*" EXCLUDE - PATTERN ".qt*" EXCLUDE - PATTERN ".qsb*" EXCLUDE - PATTERN ".lupdate*" EXCLUDE ) install( DIRECTORY "${CMAKE_BINARY_DIR}/cockatrice/" DESTINATION ./ FILES_MATCHING + PATTERN "CMakeFiles" EXCLUDE PATTERN "*.ini" - PATTERN "CMakeFiles*" EXCLUDE - PATTERN "*.dir*" EXCLUDE - PATTERN "*_autogen*" EXCLUDE - PATTERN "*.tlog*" EXCLUDE - PATTERN "*.pdb" EXCLUDE - PATTERN "x64*" EXCLUDE - PATTERN ".qt*" EXCLUDE - PATTERN ".qsb*" EXCLUDE - PATTERN ".lupdate*" EXCLUDE ) # Qt plugins: audio, iconengines, imageformats, multimedia, platforms, printsupport, styles, tls @@ -749,6 +718,10 @@ Data = Resources\") " COMPONENT Runtime ) + + if(OPENSSL_FOUND) + install(FILES ${OPENSSL_INCLUDE_DIRS} DESTINATION ./) + endif() endif() if(Qt6LinguistTools_FOUND) diff --git a/cockatrice/cockatrice.qrc b/cockatrice/cockatrice.qrc index 14cf15b2f..e21bdb0be 100644 --- a/cockatrice/cockatrice.qrc +++ b/cockatrice/cockatrice.qrc @@ -63,8 +63,6 @@ resources/icons/mana/W.svg resources/backgrounds/home.png - resources/backgrounds/home-dark.png - resources/backgrounds/home-light.png resources/backgrounds/card_triplet.svg resources/backgrounds/placeholder_printing_selector.svg @@ -365,8 +363,6 @@ resources/usericons/pawn_single.svg resources/usericons/pawn_double.svg - resources/usericons/pawn_dev_single.svg - resources/usericons/pawn_dev_double.svg resources/usericons/pawn_donator_single.svg resources/usericons/pawn_donator_double.svg resources/usericons/pawn_judge_single.svg diff --git a/cockatrice/resources/backgrounds/home-dark.png b/cockatrice/resources/backgrounds/home-dark.png deleted file mode 100644 index 68f48e2c2..000000000 Binary files a/cockatrice/resources/backgrounds/home-dark.png and /dev/null differ diff --git a/cockatrice/resources/backgrounds/home-light.png b/cockatrice/resources/backgrounds/home-light.png deleted file mode 100644 index eaaaba932..000000000 Binary files a/cockatrice/resources/backgrounds/home-light.png and /dev/null differ diff --git a/cockatrice/resources/backgrounds/home.png b/cockatrice/resources/backgrounds/home.png index eaaaba932..68f48e2c2 100644 Binary files a/cockatrice/resources/backgrounds/home.png and b/cockatrice/resources/backgrounds/home.png differ diff --git a/cockatrice/resources/usericons/pawn_dev_double.svg b/cockatrice/resources/usericons/pawn_dev_double.svg deleted file mode 100644 index 57ed5c2da..000000000 --- a/cockatrice/resources/usericons/pawn_dev_double.svg +++ /dev/null @@ -1,343 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/cockatrice/resources/usericons/pawn_dev_single.svg b/cockatrice/resources/usericons/pawn_dev_single.svg deleted file mode 100644 index f7c4e7018..000000000 --- a/cockatrice/resources/usericons/pawn_dev_single.svg +++ /dev/null @@ -1,211 +0,0 @@ - - - -image/svg+xml - - diff --git a/cockatrice/src/game_graphics/board/abstract_card_item.cpp b/cockatrice/src/game_graphics/board/abstract_card_item.cpp index 3969b7d03..1410d0c80 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_item.cpp +++ b/cockatrice/src/game_graphics/board/abstract_card_item.cpp @@ -1,7 +1,6 @@ #include "abstract_card_item.h" #include "../../client/settings/cache_settings.h" -#include "../../interface/card_localization.h" #include "../../interface/card_picture_loader/card_picture_loader.h" #include "../game_scene.h" #include "../z_values.h" @@ -27,8 +26,6 @@ AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this, [this] { update(); }); - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this, - [this] { update(); }); refreshCardInfo(); connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::roundCardCornersChanged, this, @@ -174,7 +171,7 @@ void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedS if (SettingsCache::instance().debug().getShowCardId()) { prefix = "#" + QString::number(id) + " "; } - nameStr = prefix + CardLocalization::displayName(getCardInfo()); + nameStr = prefix + cardRef.name; } painter->drawText(QRectF(3 * scaleFactor, 3 * scaleFactor, translatedSize.width() - 6 * scaleFactor, translatedSize.height() - 6 * scaleFactor), diff --git a/cockatrice/src/interface/card_localization.h b/cockatrice/src/interface/card_localization.h deleted file mode 100644 index 0bfe4a764..000000000 --- a/cockatrice/src/interface/card_localization.h +++ /dev/null @@ -1,59 +0,0 @@ -#ifndef COCKATRICE_CARD_LOCALIZATION_H -#define COCKATRICE_CARD_LOCALIZATION_H - -#include "../client/settings/cache_settings.h" - -#include -#include -#include - -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 \ No newline at end of file diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp index b8a54761a..8c81d641d 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include #include #include #include @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -38,10 +37,8 @@ CardPictureLoader::CardPictureLoader() : QObject(nullptr) &CardPictureLoader::picsPathChanged); connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this, &CardPictureLoader::picDownloadChanged); - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this, - &CardPictureLoader::cardLangChanged); - qRegisterMetaType("ExactCard"); + qRegisterMetaType(); connect(worker, &CardPictureLoaderWorker::imageLoaded, this, &CardPictureLoader::imageLoaded); statusBar = new CardPictureLoaderStatusBar(nullptr); @@ -209,49 +206,7 @@ void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image) card.emitPixmapUpdated(); } -void CardPictureLoader::deleteAllLocalOverrides(const ExactCard &card) -{ - const QString picsRoot = SettingsCache::instance().paths().getPicsPath(); - if (picsRoot.isEmpty() || !card) { - return; - } - - QDir baseDir(picsRoot); - if (!baseDir.cd("downloadedPics")) { - return; - } - - const QString name = card.getInfo().getCorrectedName(); - - QString set, collector, uuid; - auto printing = card.getPrinting(); - if (printing.getSet()) { - set = printing.getSet()->getCorrectedShortName(); - collector = printing.getProperty("num"); - uuid = printing.getUuid(); - } - - for (const auto &scheme : CardPictureLoaderLocalSchemes::exportSchemes()) { - QString rel = CardPictureLoaderLocalSchemes::expandPattern(scheme.pattern, name, set, collector, uuid); - - if (rel.isEmpty()) { - continue; - } - - rel += ".png"; - rel = QDir::cleanPath(rel); - - QString fullPath = baseDir.filePath(rel); - - if (QFile::exists(fullPath)) { - QFile::remove(fullPath); - } - } -} - -void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, - const QPixmap &pixmap, - const bool allowOverwrite) +void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap) { if (pixmap.isNull() || !card) { return; @@ -311,9 +266,8 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, QFileInfo outInfo(baseDir.filePath(relativePath)); - // Automatic cache writes (FILESYSTEM_CACHE) must never clobber an explicit user override. - // Only the explicit override paths pass allowOverwrite == true. - if (!allowOverwrite && outInfo.exists()) { + // Do not overwrite existing files + if (outInfo.exists()) { return; } @@ -334,122 +288,6 @@ void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, } } -void CardPictureLoader::installPrintingOverrideOnLoad(const ExactCard &originalCard, const ExactCard &overrideCard) -{ - // Overriding a card with itself is the reset case, not a real override: every code path below - // would re-enter itself through emitPixmapUpdated(). Reject it outright. - if (originalCard == overrideCard) { - return; - } - - CardInfoPtr cardPtr = overrideCard.getCardPtr(); - if (!cardPtr) { - return; - } - - // Heap-allocate so the lambda can capture it before the connection is made - auto *connectionHandle = new QMetaObject::Connection; - - *connectionHandle = - connect(cardPtr.data(), &CardInfo::pixmapUpdated, cardPtr.data(), - [originalCard, overrideCard, connectionHandle, this](const PrintingInfo &printing) { - // All printings share the same CardInfo, so ignore updates triggered by any - // other printing (e.g., the original card re-loading from disk). - if (printing != overrideCard.getPrinting()) { - return; - } - - QPixmap pixmap; - if (QPixmapCache::find(overrideCard.getPixmapCacheKey(), &pixmap) && !pixmap.isNull()) { - // The override art has resolved — persist it and reflect it immediately. - // Retire the connection before emitting so the refresh can't re-enter. - saveCardImageToLocalStorage(originalCard, pixmap, /*allowOverwrite=*/true); - - QObject::disconnect(*connectionHandle); - delete connectionHandle; - - QPixmapCache::clear(); - originalCard.emitPixmapUpdated(); - return; - } - - // The art could not be resolved. Keep the connection armed so a late resolution - // still lands, and surface a visible refusal instead of a silent no-op. An - // override already on disk is left untouched and simply re-displayed. - QPixmapCache::clear(); - if (!hasLocalOverrides(originalCard)) { - QPixmap refusedPixmap; - getCardBackLoadingFailedPixmap(refusedPixmap, QSize(480, 672)); - QPixmapCache::insert(originalCard.getPixmapCacheKey(), refusedPixmap); - } - originalCard.emitPixmapUpdated(); - }); - - // Now enqueue; if the image is already loading (deduplicated in the worker), - // the signal will still fire when it completes - CardPictureLoader::getInstance().worker->enqueueImageLoad(overrideCard); -} - -void CardPictureLoader::installPrintingOverride(const ExactCard &originalCard, const ExactCard &overrideCard) -{ - // Same guard as installPrintingOverrideOnLoad: self-override is the reset case. - if (originalCard == overrideCard) { - return; - } - - QPixmap pixmap; - const QString key = overrideCard.getPixmapCacheKey(); - - if (QPixmapCache::find(key, &pixmap) && !pixmap.isNull()) { - // Already cached — save immediately; the caller refreshes the card. - saveCardImageToLocalStorage(originalCard, pixmap, /*allowOverwrite=*/true); - return; - } - - // Cache miss or previously failed load — enqueue load and wait for the signal. - installPrintingOverrideOnLoad(originalCard, overrideCard); -} - -bool CardPictureLoader::hasLocalOverrides(const ExactCard &card) -{ - const QString picsRoot = SettingsCache::instance().paths().getPicsPath(); - if (picsRoot.isEmpty() || !card) { - return false; - } - - QDir baseDir(picsRoot); - if (!baseDir.cd("downloadedPics")) { - return false; - } - - const QString name = card.getInfo().getCorrectedName(); - - QString set, collector, uuid; - const PrintingInfo printing = card.getPrinting(); - if (printing.getSet()) { - set = printing.getSet()->getCorrectedShortName(); - collector = printing.getProperty("num"); - uuid = printing.getUuid(); - } - - for (const auto &scheme : CardPictureLoaderLocalSchemes::exportSchemes()) { - QString rel = CardPictureLoaderLocalSchemes::expandPattern(scheme.pattern, name, set, collector, uuid); - - if (rel.isEmpty()) { - continue; - } - - rel += ".png"; - rel = QDir::cleanPath(rel); - - if (QFile::exists(baseDir.filePath(rel))) { - return true; - } - } - - return false; -} - void CardPictureLoader::clearPixmapCache() { QPixmapCache::clear(); @@ -489,11 +327,31 @@ void CardPictureLoader::picsPathChanged() QPixmapCache::clear(); } -void CardPictureLoader::cardLangChanged() +bool CardPictureLoader::hasCustomArt() { - // Localized images are fetched via a different URL, but the in-memory - // pixmap cache is keyed by card name/uuid, so drop everything cached - // (including failure timestamps) to force a reload in the new language. - QPixmapCache::clear(); - failedAt.clear(); + auto picsPath = SettingsCache::instance().paths().getPicsPath(); + QDirIterator it(picsPath, QDir::Dirs | QDir::NoDotAndDotDot); + + // Check if there is at least one non-directory file in the pics path, other + // than in the "downloadedPics" subdirectory. + while (it.hasNext()) { +#if (QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)) + QFileInfo dir(it.nextFileInfo()); +#else + // nextFileInfo() is only available in Qt 6.3+, for previous versions, we build + // the QFileInfo from a QString which requires more system calls. + QFileInfo dir(it.next()); +#endif + + if (it.fileName() == "downloadedPics") { + continue; + } + + QDirIterator subIt(it.filePath(), QDir::Files, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); + if (subIt.hasNext()) { + return true; + } + } + + return false; } diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader.h b/cockatrice/src/interface/card_picture_loader/card_picture_loader.h index 0a4934e6d..5c3ac84a3 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader.h +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader.h @@ -97,17 +97,10 @@ public: static void cacheCardPixmaps(const QList &cards); /** - * @brief Check if a local override image already exists for the card. - * @param card The card to check. - * @return True if the card has at least one locally stored override image. + * @brief Check if the user has custom card art in the picsPath directory. + * @return True if any custom art exists. */ - static bool hasLocalOverrides(const ExactCard &card); - - /** - * @brief Removes all locally stored override images for the card. - * @param card The card to remove the override images of. - */ - static void deleteAllLocalOverrides(const ExactCard &card); + static bool hasCustomArt(); /** * @brief Clears the in-memory QPixmap cache for all cards. @@ -127,9 +120,7 @@ public slots: * @param image Loaded QImage. */ void imageLoaded(const ExactCard &card, const QImage &image); - void saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap, bool allowOverwrite = false); - void installPrintingOverride(const ExactCard &originalCard, const ExactCard &overrideCard); - void installPrintingOverrideOnLoad(const ExactCard &originalCard, const ExactCard &overrideCard); + void saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap); private slots: /** @@ -143,12 +134,6 @@ private slots: * Clears the QPixmap cache to reload images. */ void picsPathChanged(); - - /** - * @brief Triggered when the card language setting changes. - * Clears the in-memory picture caches so images reload in the new language. - */ - void cardLangChanged(); }; #endif diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp index c82fca403..39621839a 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp @@ -94,10 +94,6 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName, candidatePaths << picsPath + "/downloadedPics/" + setName + "/" + nameVariant; } - // Non-set-folder export schemes (e.g., Name_Set_Collector) write straight into - // downloadedPics/; check there as a fallback so local overrides round-trip. - candidatePaths << picsPath + "/downloadedPics/" + nameVariant; - for (const QString &path : candidatePaths) { QFileInfo fileInfo(path); QDir dir = fileInfo.dir(); @@ -109,8 +105,7 @@ QImage CardPictureLoaderLocal::tryLoadCardImageFromDisk(const QString &setName, QStringList files = dir.entryList(QDir::Files); for (const QString &file : files) { - QFileInfo fi(file); - if (fi.completeBaseName() != baseName) { + if (!file.startsWith(baseName)) { continue; } diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp index d8779863e..2925b3af9 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.cpp @@ -107,15 +107,15 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture // Check for cached redirects QUrl cachedRedirect = getCachedRedirect(url); if (!cachedRedirect.isEmpty()) { + emit imageRequestSucceeded(url); // The redirect target is a different host, which may itself be in 429 backoff; hand the // entry back to its worker so it waits the backoff out instead of dispatching straight // onto the backed-off host. if (CardPictureLoaderWorkerWork::rateLimiter().isRateLimited(cachedRedirect.host(), QDateTime::currentDateTime())) { - worker->scheduleDeferredRetry(cachedRedirect.host()); + worker->scheduleDeferredRetry(); return nullptr; } - emit imageRequestSucceeded(url); return makeRequest(cachedRedirect, worker); } @@ -144,6 +144,11 @@ QNetworkReply *CardPictureLoaderWorker::makeRequest(const QUrl &url, CardPicture void CardPictureLoaderWorker::resetRequestQuota() { + // Allowances are seeded lazily per host in processSingleRequest() when a request is first + // looked at in a new second, so a host that enters the queue mid-second now gets its reduced + // per-host allowance instead of falling through to the full per-second default. + hostQuotaRemaining.clear(); + QDateTime now = QDateTime::currentDateTime(); for (auto it = hostRequestQuota.begin(); it != hostRequestQuota.end(); ++it) { if (!hostLast429.contains(it.key()) || now.msecsTo(hostLast429.value(it.key())) < -QUOTA_RECOVER_MS) { @@ -151,30 +156,35 @@ void CardPictureLoaderWorker::resetRequestQuota() } } - // Forget the per-second allowances; each host's allowance is re-seeded lazily from its - // reduced sustained quota the first time it is dispatched in the new second, so a host that - // enters the queue mid-second no longer falls through to a fresh full quota. - hostQuotaRemaining.clear(); - - updateTimerState(); + processQueuedRequests(); } void CardPictureLoaderWorker::processQueuedRequests() { - // QTimer must be started from the thread it lives in; if this public slot is ever reached from - // another thread, replay it on the worker's event loop instead of letting start() fail silently. - if (thread() != QThread::currentThread()) { - QMetaObject::invokeMethod(this, &CardPictureLoaderWorker::processQueuedRequests, Qt::QueuedConnection); + Q_ASSERT(thread() == QThread::currentThread()); + + if (requestLoadQueue.isEmpty()) { + dispatchTimer.stop(); + requestTimer.stop(); return; } - updateTimerState(); + // Start lazily from the worker's own thread: QTimer must be started in the thread it lives in. + if (!requestTimer.isActive()) { + requestTimer.start(); + } + // Restarting an active timer would reset the pacing countdown, so a burst of enqueues could + // keep starving the dispatcher; only start it when it has actually stopped. + if (!dispatchTimer.isActive()) { + dispatchTimer.start(); + } } void CardPictureLoaderWorker::dispatchQueuedRequest() { if (requestLoadQueue.isEmpty()) { - // All queued requests have been dispatched; stop the pacing timers. - updateTimerState(); + // All queued requests have been dispatched; stop the pacing and quota-reset timers. + dispatchTimer.stop(); + requestTimer.stop(); return; } @@ -184,40 +194,6 @@ void CardPictureLoaderWorker::dispatchQueuedRequest() } } -void CardPictureLoaderWorker::updateTimerState() -{ - // Never restart an active timer: that would reset the pacing countdown and a burst of enqueues - // could keep starving the dispatcher, so only (re)start a timer that has actually stopped. - if (requestLoadQueue.isEmpty()) { - dispatchTimer.stop(); - // Forget per-second allowances once nothing is pending: a stale zero would otherwise delay - // the next single request by a full quota-reset interval. - hostQuotaRemaining.clear(); - } else if (!dispatchTimer.isActive()) { - dispatchTimer.start(); - } - - // The quota timer resets allowances every second and is also the only thing that heals a host - // after a 429 (see resetRequestQuota). It must keep ticking while work is queued or a host is - // still recovering below the ceiling, and only winds down once no host needs recovery anymore. - // Keeping it alive during such idle periods lets reduced quotas recover as intended. - bool hostRecovering = false; - for (auto it = hostRequestQuota.cbegin(); it != hostRequestQuota.cend(); ++it) { - if (it.value() < MAX_REQUESTS_PER_SEC) { - hostRecovering = true; - break; - } - } - - if (!requestLoadQueue.isEmpty() || hostRecovering) { - if (!requestTimer.isActive()) { - requestTimer.start(); - } - } else if (requestTimer.isActive()) { - requestTimer.stop(); - } -} - bool CardPictureLoaderWorker::processSingleRequest() { QDateTime now = QDateTime::currentDateTime(); @@ -228,8 +204,8 @@ bool CardPictureLoaderWorker::processSingleRequest() // back to its worker so it can wait the backoff out or fall through to another source, // instead of leaving it parked in the queue with no reply pending. if (CardPictureLoaderWorkerWork::rateLimiter().isRateLimited(host, now)) { - auto entry = requestLoadQueue.takeAt(i); - entry.second->startNextPicDownload(); + requestLoadQueue.removeAt(i); + request.second->startNextPicDownload(); return true; } // Seed the allowance now so a host that was rate limited gets its reduced @@ -240,8 +216,8 @@ bool CardPictureLoaderWorker::processSingleRequest() int allowance = hostQuotaRemaining.value(host); if (allowance > 0) { hostQuotaRemaining.insert(host, allowance - 1); - auto entry = requestLoadQueue.takeAt(i); - makeRequest(entry.first, entry.second); + makeRequest(request.first, request.second); + requestLoadQueue.removeAt(i); return true; } } diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.h b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.h index 2a13e847c..f0fe17976 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.h +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker.h @@ -86,7 +86,7 @@ public slots: */ QNetworkReply *makeRequest(const QUrl &url, CardPictureLoaderWorkerWork *workThread); - /** @brief Ensures the pacing and quota-reset timers reflect the current queue and recovery state. */ + /** @brief Starts the pacing timers if there is queued work, stops them when the queue is empty. */ void processQueuedRequests(); /** @brief Chooses a request from the queue and starts it, respecting the quota and pacing. */ @@ -142,9 +142,6 @@ private: /** @brief Removes stale redirect entries older than TTL. */ void cleanStaleEntries(); - /** @brief Starts or stops the pacing and quota-reset timers to match the queue and recovery state. */ - void updateTimerState(); - private slots: /** @brief Resets the request quota for rate-limiting. */ void resetRequestQuota(); diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp index b70207ff4..e4a36ab8c 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.cpp @@ -173,7 +173,7 @@ void CardPictureLoaderWorkerWork::handleFailedReply(const QNetworkReply *reply) << "PictureLoader: [card: " << cardToDownload.getCard().getName() << " set: " << cardToDownload.getSetName() << "]: Too many requests from " << host << ", backing off until " << backoffUntil.toString(Qt::ISODate) << ", retrying the same url"; - scheduleDeferredRetry(host); + scheduleDeferredRetry(); } else { qCWarning(CardPictureLoaderWorkerWorkLog).nospace() << "PictureLoader: [card: " << cardToDownload.getCard().getName() @@ -278,16 +278,14 @@ QImage CardPictureLoaderWorkerWork::tryLoadImageFromReply(QNetworkReply *reply) return imgReader.read(); } -void CardPictureLoaderWorkerWork::scheduleDeferredRetry(const QString &preferredHost) +void CardPictureLoaderWorkerWork::scheduleDeferredRetry() { QDateTime now = QDateTime::currentDateTime(); - // Prefer waiting on the server that is actually blocking the request: callers hand in the - // rate-limited host when it differs from the current URL (e.g. a cached redirect target still - // in backoff), otherwise fall back to the current URL's server so we retry the same source. - QString waitHost = preferredHost.isEmpty() ? QUrl(cardToDownload.getCurrentUrl()).host() : preferredHost; - QDateTime backoffUntil = s_rateLimiter.deadline(waitHost); - if (!s_rateLimiter.isRateLimited(waitHost, now)) { + // Prefer waiting on the current URL's server so we retry the same source. + QString currentHost = QUrl(cardToDownload.getCurrentUrl()).host(); + QDateTime backoffUntil = s_rateLimiter.deadline(currentHost); + if (!s_rateLimiter.isRateLimited(currentHost, now)) { backoffUntil = s_rateLimiter.earliestDeadline(now); } diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.h b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.h index c5aa07d10..f05d727de 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.h +++ b/cockatrice/src/interface/card_picture_loader/card_picture_loader_worker_work.h @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -59,14 +58,13 @@ public: /** * @brief Schedules a deferred retry after the relevant server backoff expires. - * @param preferredHost The server that is actually blocking the request, or an empty - * string to use the current URL's server * - * Waits on the blocking server's backoff deadline, otherwise on the earliest active - * backoff. If no servers are in backoff, concludes with failure. Otherwise resets the - * CardPictureToLoad indices and retries after the backoff period. + * Waits on the current URL's server when it is the reason we are blocked, + * otherwise on the earliest active backoff. If no servers are in backoff, + * concludes with failure. Otherwise resets the CardPictureToLoad indices and + * retries after the backoff period. */ - void scheduleDeferredRetry(const QString &preferredHost = {}); + void scheduleDeferredRetry(); public slots: /** diff --git a/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp b/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp index 7cb502e92..5f4ff0bbd 100644 --- a/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp +++ b/cockatrice/src/interface/card_picture_loader/card_picture_to_load.cpp @@ -94,8 +94,7 @@ void CardPictureToLoad::populateSetUrls() } } - const QStringList orderedTemplates = urlTemplates; - for (const QString &urlTemplate : orderedTemplates) { + for (const QString &urlTemplate : urlTemplates) { QString transformedUrl = transformUrl(urlTemplate); if (!transformedUrl.isEmpty()) { @@ -283,15 +282,8 @@ QString CardPictureToLoad::transformUrl(const QString &urlTemplate) const } // language setting - const QString cardLang = SettingsCache::instance().cardsDisplay().getCardLang(); - transformMap["!sflang!"] = cardLang; - - // The localized printing's own id is unknown, so Scryfall must resolve it by - // its translated name (see populateSetUrls); expose that name for the - // `/cards/named` template. - if (cardLang != "en") { - transformMap["!localizedName!"] = card.getInfo().getLocalizedName(cardLang); - } + transformMap["!sflang!"] = QString(QCoreApplication::translate( + "PictureLoader", "en", "code for scryfall's language property, not available for all languages")); QString transformedUrl = urlTemplate; for (const QString &prop : transformMap.keys()) { diff --git a/cockatrice/src/interface/intents/contexts/context_open_deck.h b/cockatrice/src/interface/intents/contexts/context_open_deck.h deleted file mode 100644 index 03dca088e..000000000 --- a/cockatrice/src/interface/intents/contexts/context_open_deck.h +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef COCKATRICE_CONTEXT_OPEN_DECK_H -#define COCKATRICE_CONTEXT_OPEN_DECK_H - -#include "context_connect_to_server.h" - -#include - -struct ContextOpenDeck -{ - ContextConnectToServer serverContext; - QString shareToken; -}; - -#endif // COCKATRICE_CONTEXT_OPEN_DECK_H diff --git a/cockatrice/src/interface/intents/intent.cpp b/cockatrice/src/interface/intents/intent.cpp index db0d13b2c..c02a89f35 100644 --- a/cockatrice/src/interface/intents/intent.cpp +++ b/cockatrice/src/interface/intents/intent.cpp @@ -2,11 +2,10 @@ Intent::Intent(QObject *parent) : QObject(parent) { - // An intent is done as soon as it reports success, failure, or cancellation. - // Deleting it also tears down its dependency chain and disconnects any signal wiring. + // An intent is done as soon as it reports success or failure. Deleting it + // also tears down its dependency chain and disconnects any signal wiring. connect(this, &Intent::finished, this, &QObject::deleteLater); connect(this, &Intent::failed, this, &QObject::deleteLater); - connect(this, &Intent::cancelled, this, &QObject::deleteLater); } Intent::~Intent() = default; @@ -28,7 +27,6 @@ void Intent::runDependency(Intent *dependency) this->execute(); }); connect(dependency, &Intent::failed, this, &Intent::failed); - connect(dependency, &Intent::cancelled, this, &Intent::cancelled); dependency->execute(); } @@ -48,11 +46,3 @@ void Intent::emitFailed(const QString &reason) emit failed(reason); } } - -void Intent::emitCancelled() -{ - if (!completed) { - completed = true; - emit cancelled(); - } -} diff --git a/cockatrice/src/interface/intents/intent.h b/cockatrice/src/interface/intents/intent.h index 5d9fdd3d6..125900ecd 100644 --- a/cockatrice/src/interface/intents/intent.h +++ b/cockatrice/src/interface/intents/intent.h @@ -16,7 +16,6 @@ public: signals: void finished(); void failed(QString reason); - void cancelled(); protected: // --- Subclasses must implement these --- @@ -30,7 +29,6 @@ protected: // Emit the outcome exactly once; ignore late signals after the intent is done. void emitFinished(); void emitFailed(const QString &reason); - void emitCancelled(); private: bool completed = false; diff --git a/cockatrice/src/interface/intents/intent_join_server_game.cpp b/cockatrice/src/interface/intents/intent_join_server_game.cpp index b22b200ed..205c4dc70 100644 --- a/cockatrice/src/interface/intents/intent_join_server_game.cpp +++ b/cockatrice/src/interface/intents/intent_join_server_game.cpp @@ -19,15 +19,13 @@ bool IntentJoinServerGame::checkPrecondition() const if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) { return false; } - // serverName()/serverPort() reflect the server the client was configured - // to connect to, which may differ from the actual TCP peer (e.g. when - // connecting through a proxy), so compare those configured values. A link - // naming the same host on another port is a different server and must not - // reuse the session there. - if (remoteClient->serverName().compare(context->roomContext.serverContext.hostname, Qt::CaseInsensitive) != 0) { + // peerPort() reflects the actual TCP peer, which may differ from the + // configured server port (e.g. when connecting through a proxy), so only + // the hostname is compared here. + if (remoteClient->peerName() != context->roomContext.serverContext.hostname) { return false; } - if (QString::number(remoteClient->serverPort()) != context->roomContext.serverContext.port) { + if (QString::number(remoteClient->peerPort()) != context->roomContext.serverContext.port) { return false; } diff --git a/cockatrice/src/interface/intents/intent_login.cpp b/cockatrice/src/interface/intents/intent_login.cpp index 7beb63e1d..ff871fd03 100644 --- a/cockatrice/src/interface/intents/intent_login.cpp +++ b/cockatrice/src/interface/intents/intent_login.cpp @@ -1,14 +1,9 @@ #include "intent_login.h" #include "../../client/settings/cache_settings.h" -#include "../widgets/dialogs/dlg_login_prompt.h" #include "libcockatrice/settings/servers_settings.h" -#include - -IntentGetLoginCredentials::IntentGetLoginCredentials(ContextConnectToServer *_context, - bool _promptForMissingCredentials) - : Intent(), context(_context), promptForMissingCredentials(_promptForMissingCredentials) +IntentGetLoginCredentials::IntentGetLoginCredentials(ContextConnectToServer *_context) : Intent(), context(_context) { } @@ -34,46 +29,5 @@ void IntentGetLoginCredentials::onPreconditionSatisfied() void IntentGetLoginCredentials::onPreconditionNotSatisfied() { - // MainWindow::applyStartupDestination runs this intent on every launch for - // users whose startup tab is Server / Server Room; keep that path quiet, as - // it was before the link-driven sign-in dialog existed. - if (!promptForMissingCredentials) { - emitFailed(tr("No saved credentials for this server")); - return; - } - - // No credentials saved for the target server: ask the user for them. They - // opt into saving them so later links to the same server connect directly. - const QString serverText = context->hostname + ":" + context->port; - DlgLoginPrompt dialog(serverText); - // ApplicationModal: the dialog has no parent (the intent is not a widget), - // so WindowModal would not actually block any other window. - dialog.setWindowModality(Qt::ApplicationModal); - - if (dialog.exec() != QDialog::Accepted) { - emitCancelled(); - return; - } - - context->username = dialog.username(); - context->password = dialog.password(); - - if (dialog.savePassword() && !context->username.isEmpty()) { - ServersSettings &servers = SettingsCache::instance().servers(); - // The host may already be saved under a friendly name (e.g. a public-server - // list entry) with no credentials; reuse that name instead of overwriting - // it with the raw hostname when addNewServer updates the entry in place. - QString saveName = context->hostname; - const int existingIndex = servers.findServerIndex(context->hostname, context->port); - if (existingIndex >= 0) { - saveName = - servers.getValue(QString("saveName%1").arg(existingIndex), "server", "server_details").toString(); - if (saveName.isEmpty()) { - saveName = context->hostname; - } - } - servers.addNewServer(saveName, context->hostname, context->port, context->username, context->password, true); - } - - emitFinished(); + emitFailed(tr("No saved credentials for this server")); } diff --git a/cockatrice/src/interface/intents/intent_login.h b/cockatrice/src/interface/intents/intent_login.h index 8ffd91a0a..c7fec92b7 100644 --- a/cockatrice/src/interface/intents/intent_login.h +++ b/cockatrice/src/interface/intents/intent_login.h @@ -9,10 +9,7 @@ class IntentGetLoginCredentials : public Intent Q_OBJECT public: - // When promptForMissingCredentials is false (the default) a server without - // saved credentials fails silently; only intent chains from cockatrice:// - // links opt into the interactive sign-in dialog. - explicit IntentGetLoginCredentials(ContextConnectToServer *_context, bool _promptForMissingCredentials = false); + IntentGetLoginCredentials(ContextConnectToServer *_context); protected: bool checkPrecondition() const override; @@ -21,7 +18,6 @@ protected: private: ContextConnectToServer *context; - bool promptForMissingCredentials; }; #endif // COCKATRICE_INTENT_LOGIN_H diff --git a/cockatrice/src/interface/intents/intent_open_shared_deck.cpp b/cockatrice/src/interface/intents/intent_open_shared_deck.cpp deleted file mode 100644 index 016de63e6..000000000 --- a/cockatrice/src/interface/intents/intent_open_shared_deck.cpp +++ /dev/null @@ -1,208 +0,0 @@ -#include "intent_open_shared_deck.h" - -#include "../deck_loader/deck_loader.h" -#include "../widgets/dialogs/dlg_shared_decks_preview.h" -#include "../widgets/tabs/tab_supervisor.h" -#include "intent_connect_to_server.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -IntentOpenSharedDeck::IntentOpenSharedDeck(TabSupervisor *_tabSupervisor, - RemoteClient *_remoteClient, - const CardDatabaseQuerier *_querier, - std::unique_ptr _context) - : Intent(), tabSupervisor(_tabSupervisor), remoteClient(_remoteClient), querier(_querier), - context(_context.release()) -{ - downloadTimer = new QTimer(this); - downloadTimer->setSingleShot(true); - downloadTimer->setInterval(15000); - connect(downloadTimer, &QTimer::timeout, this, &IntentOpenSharedDeck::onDownloadTimeout); -} - -bool IntentOpenSharedDeck::checkPrecondition() const -{ - if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) { - return false; - } - // serverName()/serverPort() reflect the server the client was configured - // to connect to, which may differ from the actual TCP peer (e.g. when - // connecting through a proxy), so compare those configured values. The - // share token must be resolved against the host the link named — a link to - // the same host on another port is a different server. - if (remoteClient->serverName().compare(context->serverContext.hostname, Qt::CaseInsensitive) != 0) { - return false; - } - return QString::number(remoteClient->serverPort()) == context->serverContext.port; -} - -void IntentOpenSharedDeck::onPreconditionSatisfied() -{ - // Resolve the share token to its items first; a share can contain more than - // one deck, and each item is downloaded by id. Time the round trip like the - // downloads, so a silent server cannot hang the chain forever. - listPhase = true; - downloadTimer->start(); - - Command_DeckShareList cmd; - cmd.set_token(context->shareToken.toStdString()); - - PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, &IntentOpenSharedDeck::listShareFinished); - remoteClient->sendCommand(pend); -} - -void IntentOpenSharedDeck::onPreconditionNotSatisfied() -{ - runDependency(new IntentConnectToServer(remoteClient, &context->serverContext)); -} - -void IntentOpenSharedDeck::listShareFinished(const Response &response, const CommandContainer & /* commandContainer */) -{ - downloadTimer->stop(); - listPhase = false; - - if (response.response_code() != Response::RespOk) { - emitFailed(tr("The shared deck could not be found or has expired")); - return; - } - - const Response_DeckShareList &resp = response.GetExtension(Response_DeckShareList::ext); - if (resp.items_size() == 0) { - emitFailed(tr("The shared deck is empty")); - return; - } - - QList items; - items.reserve(resp.items_size()); - for (const ServerInfo_DeckShareItem &item : resp.items()) { - items.append(item); - itemNames.insert(item.id(), QString::fromStdString(item.name())); - } - - const QString serverText = context->serverContext.hostname + ":" + context->serverContext.port; - - // Ask the user which decks to open before downloading anything. - previewDialog = new DlgSharedDecksPreview(tabSupervisor, querier, QString::fromStdString(resp.name()), - resp.expires_at(), serverText, items); - connect(previewDialog, &DlgSharedDecksPreview::openRequested, this, &IntentOpenSharedDeck::startDownloads); - connect(previewDialog, &DlgSharedDecksPreview::cancelled, this, &IntentOpenSharedDeck::emitCancelled); - connect(previewDialog, &DlgSharedDecksPreview::cancelled, previewDialog, &QWidget::deleteLater); - previewDialog->show(); - previewDialog->raise(); - previewDialog->activateWindow(); -} - -void IntentOpenSharedDeck::startDownloads(const QList &itemIds) -{ - pendingItemIds = itemIds; - totalItems = itemIds.size(); - completedItems = 0; - loadedDecks.clear(); - downloadNextItem(); -} - -void IntentOpenSharedDeck::downloadNextItem() -{ - if (pendingItemIds.isEmpty()) { - finishAll(); - return; - } - - currentItemId = pendingItemIds.takeFirst(); - downloadTimer->start(); - - Command_DeckShareDownload cmd; - cmd.set_token(context->shareToken.toStdString()); - cmd.set_item_id(currentItemId); - - PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, &IntentOpenSharedDeck::downloadShareFinished); - remoteClient->sendCommand(pend); -} - -void IntentOpenSharedDeck::downloadShareFinished(const Response &response, - const CommandContainer & /* commandContainer */) -{ - downloadTimer->stop(); - - QString failureReason; - if (response.response_code() != Response::RespOk) { - failureReason = tr("Failed to download the shared deck"); - } else { - const Response_DeckShareDownload &resp = response.GetExtension(Response_DeckShareDownload::ext); - const QString deckString = QString::fromStdString(resp.deck()); - if (deckString.isEmpty()) { - failureReason = tr("The shared deck is empty"); - } else { - std::optional deckOpt = - DeckLoader::loadFromRemote(deckString, LoadedDeck::LoadInfo::NON_REMOTE_ID); - if (!deckOpt) { - failureReason = tr("The shared deck could not be loaded"); - } else { - loadedDecks.append(deckOpt.value()); - ++completedItems; - previewDialog->setDownloadProgress(completedItems, totalItems, - itemNames.value(currentItemId, tr("Unknown deck"))); - downloadNextItem(); - return; - } - } - } - - onItemFailure(failureReason); -} - -void IntentOpenSharedDeck::onItemFailure(const QString &reason) -{ - downloadTimer->stop(); - - if (loadedDecks.isEmpty()) { - previewDialog->deleteLater(); - emitFailed(reason); - return; - } - - const int downloadedCount = loadedDecks.size(); - const QMessageBox::StandardButton answer = QMessageBox::question( - previewDialog, tr("Open shared decks"), - tr("Could not download the deck \"%1\".\n\n%n deck(s) were already downloaded. Open them?", "", downloadedCount) - .arg(itemNames.value(currentItemId, tr("Unknown deck"))), - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - - if (answer == QMessageBox::Yes) { - finishAll(); - } else { - previewDialog->deleteLater(); - emitCancelled(); - } -} - -void IntentOpenSharedDeck::onDownloadTimeout() -{ - // The list phase has no preview dialog yet to report progress into; fail the - // whole intent instead of letting the shared deck hang in limbo. - if (listPhase) { - emitFailed(tr("Timed out while loading the shared deck")); - return; - } - onItemFailure(tr("Timed out while downloading the shared deck")); -} - -void IntentOpenSharedDeck::finishAll() -{ - previewDialog->deleteLater(); - for (const LoadedDeck &deck : loadedDecks) { - tabSupervisor->openDeckInNewTab(deck); - } - emitFinished(); -} diff --git a/cockatrice/src/interface/intents/intent_open_shared_deck.h b/cockatrice/src/interface/intents/intent_open_shared_deck.h deleted file mode 100644 index 87812bdea..000000000 --- a/cockatrice/src/interface/intents/intent_open_shared_deck.h +++ /dev/null @@ -1,60 +0,0 @@ -#ifndef COCKATRICE_INTENT_OPEN_SHARED_DECK_H -#define COCKATRICE_INTENT_OPEN_SHARED_DECK_H - -#include "contexts/context_open_deck.h" -#include "intent.h" -#include "remote_client.h" - -#include -#include -#include -#include - -class TabSupervisor; -struct LoadedDeck; -class CardDatabaseQuerier; -class DlgSharedDecksPreview; -class QTimer; - -class IntentOpenSharedDeck : public Intent -{ - Q_OBJECT - -public: - IntentOpenSharedDeck(TabSupervisor *_tabSupervisor, - RemoteClient *_remoteClient, - const CardDatabaseQuerier *_querier, - std::unique_ptr _context); - -protected: - bool checkPrecondition() const override; - void onPreconditionSatisfied() override; - void onPreconditionNotSatisfied() override; - -private slots: - void listShareFinished(const Response &response, const CommandContainer &commandContainer); - void downloadShareFinished(const Response &response, const CommandContainer &commandContainer); - void onDownloadTimeout(); - -private: - void startDownloads(const QList &itemIds); - void downloadNextItem(); - void onItemFailure(const QString &reason); - void finishAll(); - - TabSupervisor *tabSupervisor; - RemoteClient *remoteClient; - const CardDatabaseQuerier *querier; - QScopedPointer context; - DlgSharedDecksPreview *previewDialog = nullptr; - QTimer *downloadTimer; - QMap itemNames; - QList pendingItemIds; - QList loadedDecks; - bool listPhase = true; - int currentItemId = 0; - int totalItems = 0; - int completedItems = 0; -}; - -#endif // COCKATRICE_INTENT_OPEN_SHARED_DECK_H diff --git a/cockatrice/src/interface/intents/url_parser.cpp b/cockatrice/src/interface/intents/url_parser.cpp index 707863354..509390611 100644 --- a/cockatrice/src/interface/intents/url_parser.cpp +++ b/cockatrice/src/interface/intents/url_parser.cpp @@ -1,28 +1,19 @@ #include "url_parser.h" -#include "../../client/settings/cache_settings.h" #include "../widgets/tabs/tab_room.h" #include "../widgets/tabs/tab_supervisor.h" #include "../window_main.h" #include "contexts/context_join_game.h" -#include "contexts/context_open_deck.h" -#include "intent.h" #include "intent_join_server_game.h" #include "intent_login.h" -#include "intent_open_shared_deck.h" #include -#include #include #include #include -#include #include -#include #include -inline Q_LOGGING_CATEGORY(UrlParserLog, "url_parser"); - IntentUrlParser::IntentUrlParser(QObject *parent, MainWindow *_mainWindow) : QObject(parent), mainWindow(_mainWindow) { } @@ -38,33 +29,16 @@ void IntentUrlParser::handle(const QString &urlStr) const QString action = url.host(); QUrlQuery query(url); - qCDebug(UrlParserLog) << "Parsing intent URL, action:" << action; - - PendingIntentChain chain; - Intent *firstIntent = nullptr; if (action == "joingame") { - firstIntent = createJoinGameIntent(query, chain); + handleJoinGame(query); } else if (action == "opendeck") { - firstIntent = createOpenDeckIntent(query, chain); + // handleOpenDeck(query); } else { qWarning() << "Unknown intent:" << action; } - - if (firstIntent == nullptr) { - // The link was invalid or the user declined the confirm: nothing runs. - // Report the idle state when no other chain is queued so that a startup - // launch (which skipped its own connection for this URL) falls back to it. - if (!chainRunning && pendingChains.isEmpty()) { - emit urlChainFinished(mainWindow->getRemoteClient()->getStatus() == StatusLoggedIn); - } - return; - } - - pendingChains.append(chain); - startNextChain(); } -Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, PendingIntentChain &chain) +void IntentUrlParser::handleJoinGame(const QUrlQuery &query) { auto showError = [this](const QString &message) { QMessageBox::warning(mainWindow, tr("Open game"), message); }; @@ -75,21 +49,21 @@ Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, PendingInt if (ctx->roomContext.serverContext.hostname.isEmpty()) { showError(tr("Missing or empty hostname in the game link")); - return nullptr; + return; } bool ok = false; ctx->roomContext.serverContext.port.toUShort(&ok); if (!ok) { showError(tr("Invalid or missing port in the game link")); - return nullptr; + return; } ctx->roomContext.roomId = query.queryItemValue("roomid").toInt(&ok); if (!ok) { showError(tr("Invalid or missing room id in the game link")); - return nullptr; + return; } ok = false; @@ -97,7 +71,7 @@ Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, PendingInt if (!ok) { showError(tr("Invalid or missing game id in the game link")); - return nullptr; + return; } const QString gameDescription = query.queryItemValue("game", QUrl::FullyDecoded); @@ -106,33 +80,24 @@ Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, PendingInt const QMessageBox::StandardButton answer = QMessageBox::question( mainWindow, tr("Join game"), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); if (answer != QMessageBox::Yes) { - return nullptr; + return; } - RemoteClient *client = mainWindow->getRemoteClient(); - ContextConnectToServer *serverContext = &ctx->roomContext.serverContext; - // The join game intent owns the context and the credential lookup; once the // chain finishes (or fails) it deletes the whole tree. - auto joinGameIntent = new IntentJoinServerGame(mainWindow->getTabSupervisor(), client, std::move(ctx)); + ContextConnectToServer *serverContext = &ctx->roomContext.serverContext; + auto joinGameIntent = + new IntentJoinServerGame(mainWindow->getTabSupervisor(), mainWindow->getRemoteClient(), std::move(ctx)); joinGameIntent->setParent(this); - chain.intents.append(joinGameIntent); + + auto getLoginCredentialsIntent = new IntentGetLoginCredentials(serverContext); + getLoginCredentialsIntent->setParent(joinGameIntent); + + connect(getLoginCredentialsIntent, &Intent::finished, joinGameIntent, &Intent::execute); + connect(getLoginCredentialsIntent, &Intent::failed, joinGameIntent, &Intent::failed); connect(joinGameIntent, &Intent::failed, this, [showError](const QString &reason) { showError(reason); }); - Intent *firstIntent = joinGameIntent; - if (!isConnectedTo(serverContext->hostname, serverContext->port)) { - auto getLoginCredentialsIntent = - new IntentGetLoginCredentials(serverContext, /*promptForMissingCredentials=*/true); - getLoginCredentialsIntent->setParent(joinGameIntent); - chain.intents.insert(0, getLoginCredentialsIntent); - - connect(getLoginCredentialsIntent, &Intent::finished, joinGameIntent, &Intent::execute); - connect(getLoginCredentialsIntent, &Intent::failed, joinGameIntent, &Intent::failed); - connect(getLoginCredentialsIntent, &Intent::cancelled, joinGameIntent, &Intent::cancelled); - firstIntent = getLoginCredentialsIntent; - } - - return firstIntent; + getLoginCredentialsIntent->execute(); } QString IntentUrlParser::generateJoinGameMessage(const ContextJoinGame &context, const QString &gameDescription) @@ -169,270 +134,3 @@ QString IntentUrlParser::generateJoinGameMessage(const ContextJoinGame &context, .arg(gameDescription, gameIdStr, roomTab->getRoomName(), server) : tr("Join game \"%1\" (#%2) on %3?").arg(gameDescription, gameIdStr, server); } - -Intent *IntentUrlParser::createOpenDeckIntent(const QUrlQuery &query, PendingIntentChain &chain) -{ - auto showError = [this](const QString &message) { - QMessageBox::warning(mainWindow, tr("Open shared deck"), message); - }; - - auto ctx = std::make_unique(); - - ctx->serverContext.hostname = query.queryItemValue("hostname"); - ctx->serverContext.port = query.queryItemValue("port"); - ctx->shareToken = query.queryItemValue("share"); - - qCDebug(UrlParserLog) << "Open-deck intent: host" << ctx->serverContext.hostname << "port" - << ctx->serverContext.port << "token length" << ctx->shareToken.length(); - - if (ctx->serverContext.hostname.isEmpty()) { - showError(tr("Missing or empty hostname in the share link")); - return nullptr; - } - - bool ok = false; - const quint16 port = ctx->serverContext.port.toUShort(&ok); - if (!ok || port == 0) { - showError(tr("Invalid or missing port in the share link")); - return nullptr; - } - - if (ctx->shareToken.isEmpty()) { - showError(tr("Missing or empty share value in the share link")); - return nullptr; - } - - RemoteClient *client = mainWindow->getRemoteClient(); - - // The open deck download needs a connection to the link's server. Ask before - // taking the session anywhere it isn't already, naming the host we would - // connect to. Remember the link's target when it moves us away from a live - // session so a failed or cancelled chain can restore the session it left. - // The hostname is link-supplied and percent-decoded, so escape it: QMessageBox - // renders AutoText, and markup in a hostname would otherwise flip the whole - // prompt to rich text and let a link pad the message the user is shown. - const bool alreadyConnected = isConnectedTo(ctx->serverContext.hostname, ctx->serverContext.port); - if (!alreadyConnected) { - const QString target = - QStringLiteral("%1:%2").arg(ctx->serverContext.hostname.toHtmlEscaped(), ctx->serverContext.port); - - if (client->getStatus() == StatusLoggedIn) { - const QString current = - QStringLiteral("%1:%2").arg(client->serverName(), QString::number(client->serverPort())); - const QMessageBox::StandardButton answer = QMessageBox::question( - mainWindow, tr("Open shared deck"), - tr("Opening this share link connects you to %1 instead of %2.\n\nContinue?").arg(target, current), - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - if (answer != QMessageBox::Yes) { - return nullptr; - } - chain.migrationTargetHost = ctx->serverContext.hostname; - chain.migrationTargetPort = ctx->serverContext.port; - chain.pendingRestore = true; - } else { - // Fresh connection is harmless to wander away from, but a server the - // client has never been configured for deserves a harder warning (no - // by default) so a stray link cannot silently steer the client there. - const bool knownHost = SettingsCache::instance().servers().findHostIndex(ctx->serverContext.hostname) >= 0; - const QMessageBox::StandardButton answer = - knownHost - ? QMessageBox::question(mainWindow, tr("Open shared deck"), - tr("Opening this share link connects you to %1.\n\nContinue?").arg(target)) - : QMessageBox::warning(mainWindow, tr("Open shared deck"), - tr("Opening this share link connects you to %1, a server you have " - "never connected to before.\n\nContinue?") - .arg(target), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - if (answer != QMessageBox::Yes) { - return nullptr; - } - } - } - - ContextConnectToServer *serverContext = &ctx->serverContext; - - // The open deck intent owns the context and the credential lookup; once - // the chain finishes (or fails) it deletes the whole tree. - auto openDeckIntent = - new IntentOpenSharedDeck(mainWindow->getTabSupervisor(), client, CardDatabaseManager::query(), std::move(ctx)); - openDeckIntent->setParent(this); - chain.intents.append(openDeckIntent); - connect(openDeckIntent, &Intent::failed, this, [showError](const QString &reason) { showError(reason); }); - - Intent *firstIntent = openDeckIntent; - if (!isConnectedTo(serverContext->hostname, serverContext->port)) { - auto getLoginCredentialsIntent = - new IntentGetLoginCredentials(serverContext, /*promptForMissingCredentials=*/true); - getLoginCredentialsIntent->setParent(openDeckIntent); - chain.intents.insert(0, getLoginCredentialsIntent); - - connect(getLoginCredentialsIntent, &Intent::finished, openDeckIntent, &Intent::execute); - connect(getLoginCredentialsIntent, &Intent::failed, openDeckIntent, &Intent::failed); - connect(getLoginCredentialsIntent, &Intent::cancelled, openDeckIntent, &Intent::cancelled); - firstIntent = getLoginCredentialsIntent; - } - - return firstIntent; -} - -bool IntentUrlParser::isConnectedTo(const QString &hostname, const QString &port) const -{ - // serverName() reflects the server the client was configured to connect to, - // which may differ from the actual TCP peer (e.g. when connecting through a - // proxy), so compare the configured host and port — exactly what a link - // names. A link to the same host on another port is a different server and - // must not silently reuse an existing session there. - RemoteClient *client = mainWindow->getRemoteClient(); - return client->getStatus() == StatusLoggedIn && client->serverName().compare(hostname, Qt::CaseInsensitive) == 0 && - QString::number(client->serverPort()) == port; -} - -void IntentUrlParser::startNextChain() -{ - if (chainRunning || pendingChains.isEmpty()) { - return; - } - chainRunning = true; - - PendingIntentChain &chain = pendingChains.first(); - if (chain.intents.isEmpty()) { - pendingChains.removeFirst(); - chainRunning = false; - startNextChain(); - return; - } - - // Snapshot the session this chain moves away from now that it actually - // runs. Chains are parsed while earlier ones are still queued, so a capture - // at parse time would follow whichever server the chain before it settled - // on, not the one the user is really on when this link is handled. - if (chain.pendingRestore) { - RemoteClient *client = mainWindow->getRemoteClient(); - chain.previousServerHost = client->serverName(); - chain.previousServerPort = QString::number(client->serverPort()); - } - - // Only the last intent completes the chain; its terminal signal ends the - // whole run. Cancellation of an intermediate intent (e.g. declined login - // prompt) is forwarded onto the last intent in the chain builders above. - Intent *finalIntent = chain.intents.last(); - connect(finalIntent, &Intent::finished, this, [this]() { chainEnded(true); }); - connect(finalIntent, &Intent::failed, this, [this]() { chainEnded(false); }); - connect(finalIntent, &Intent::cancelled, this, [this]() { chainEnded(false); }); - // Backstop: if the final intent is destroyed without emitting a terminal - // signal (e.g. a network error dropped it while running), end the chain so - // later links are not queued and dropped for the rest of the session. - chainBackstopConnection = connect(finalIntent, &QObject::destroyed, this, &IntentUrlParser::onChainIntentDestroyed); - - chain.intents.first()->execute(); -} - -void IntentUrlParser::chainEnded(bool chainSucceeded) -{ - chainRunning = false; - QObject::disconnect(chainBackstopConnection); - - const PendingIntentChain chain = pendingChains.takeFirst(); - - // Only a failed or cancelled chain restores the session the link migrated - // away from; a successful one leaves the user where they are. - if (chain.pendingRestore && !chainSucceeded) { - restorePreviousServer(chain); - } - - startNextChain(); - - // Only report the terminal state once the queue has fully drained, so a - // queued follow-up link keeps the startup fallback out of the picture. - if (!chainRunning && pendingChains.isEmpty()) { - emit urlChainFinished(mainWindow->getRemoteClient()->getStatus() == StatusLoggedIn); - } -} - -void IntentUrlParser::onChainIntentDestroyed() -{ - if (!chainRunning) { - return; - } - qCWarning(UrlParserLog) << "Share-link intent destroyed without a terminal signal; ending its chain"; - chainEnded(false); -} - -void IntentUrlParser::restorePreviousServer(const PendingIntentChain &chain) -{ - if (chain.previousServerHost.isEmpty()) { - return; - } - - RemoteClient *client = mainWindow->getRemoteClient(); - const ClientStatus status = client->getStatus(); - - // A failed/cancelled chain can fire while the client is still settling the - // in-flight connection attempt (wrong password, connect timeout). Only - // decide once the client has settled into logged-in or disconnected; - // deciding mid-connect would strand the user offline from their previous - // server. - if (status == StatusDisconnected || status == StatusLoggedIn) { - restoreToPreviousServer(chain); - return; - } - auto waitConnection = std::make_shared(); - *waitConnection = connect(client, &RemoteClient::statusChanged, this, [this, chain, client, waitConnection]() { - const ClientStatus settled = client->getStatus(); - if (settled == StatusDisconnected || settled == StatusLoggedIn) { - QObject::disconnect(*waitConnection); - restoreToPreviousServer(chain); - } - }); -} - -void IntentUrlParser::restoreToPreviousServer(const PendingIntentChain &chain) -{ - RemoteClient *client = mainWindow->getRemoteClient(); - - // Back on the previous server already → nothing to undo. - if (client->serverName().compare(chain.previousServerHost, Qt::CaseInsensitive) == 0 && - QString::number(client->serverPort()) == chain.previousServerPort) { - return; - } - - // When logged in somewhere, only intervene if that somewhere is the server - // the link moved us to; if the user went elsewhere on their own, leave them. - if (client->getStatus() == StatusLoggedIn) { - const bool onMigrationTarget = - client->serverName().compare(chain.migrationTargetHost, Qt::CaseInsensitive) == 0 && - QString::number(client->serverPort()) == chain.migrationTargetPort; - if (!onMigrationTarget) { - return; - } - - ServersSettings &servers = SettingsCache::instance().servers(); - const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort); - if (index >= 0 && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) { - const QString username = - servers.getValue(QString("username%1").arg(index), "server", "server_details").toString(); - const QString password = - servers.getValue(QString("password%1").arg(index), "server", "server_details").toString(); - client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), username, password); - return; - } - client->disconnectFromServer(); - return; - } - - if (client->getStatus() != StatusDisconnected) { - return; - } - - // The link's connection attempt failed: reconnect to the previous server - // when credentials are saved, otherwise stay offline. - ServersSettings &servers = SettingsCache::instance().servers(); - const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort); - if (index >= 0 && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) { - const QString username = - servers.getValue(QString("username%1").arg(index), "server", "server_details").toString(); - const QString password = - servers.getValue(QString("password%1").arg(index), "server", "server_details").toString(); - client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), username, password); - } -} diff --git a/cockatrice/src/interface/intents/url_parser.h b/cockatrice/src/interface/intents/url_parser.h index ea29fed53..6d705e013 100644 --- a/cockatrice/src/interface/intents/url_parser.h +++ b/cockatrice/src/interface/intents/url_parser.h @@ -1,46 +1,10 @@ #ifndef COCKATRICE_URL_PARSER_H #define COCKATRICE_URL_PARSER_H - -#include #include #include -class Intent; class MainWindow; struct ContextJoinGame; - -/** - * @brief One queued intent chain with the session-migration bookkeeping for it. - * - * The restore fields are per-chain on purpose: chains are parsed while earlier - * ones are still queued, so parser-wide state would let one chain's failure - * consume the restore data another chain recorded. - */ -struct PendingIntentChain -{ - QList intents; - - // Snapshot of the session in place when this chain started running, so a - // queued chain follows whichever server the chain before it settled on. - QString previousServerHost; - QString previousServerPort; - - // Recorded at parse time when the user confirmed migrating away from a live - // session to the host/port named by the link. - QString migrationTargetHost; - QString migrationTargetPort; - bool pendingRestore = false; -}; - -/** - * @brief Parses cockatrice:// links and runs them as serialized intent chains. - * - * Links are parsed by action (joingame/opendeck) and translated into an intent - * chain. Chains are queued and run one at a time: a document can hand multiple - * links to the window while an earlier chain still connects, and running two - * connect chains concurrently tears the connection down. urlChainFinished is - * emitted once the queue has fully drained. - */ class IntentUrlParser : public QObject { Q_OBJECT @@ -48,28 +12,12 @@ class IntentUrlParser : public QObject public: IntentUrlParser(QObject *parent, MainWindow *mainWindow); void handle(const QString &urlStr); - -signals: - /** @brief Emitted when the last queued chain ended; carries whether the client is logged in. */ - void urlChainFinished(bool connected); + void handleJoinGame(const QUrlQuery &query); private: - Intent *createJoinGameIntent(const QUrlQuery &query, PendingIntentChain &chain); - Intent *createOpenDeckIntent(const QUrlQuery &query, PendingIntentChain &chain); QString generateJoinGameMessage(const ContextJoinGame &context, const QString &gameDescription); - [[nodiscard]] bool isConnectedTo(const QString &hostname, const QString &port) const; - void startNextChain(); - void chainEnded(bool chainSucceeded); - void onChainIntentDestroyed(); - void restorePreviousServer(const PendingIntentChain &chain); - void restoreToPreviousServer(const PendingIntentChain &chain); MainWindow *mainWindow; - QList pendingChains; - bool chainRunning = false; - // Disconnects the destroyed-signal backstop once a chain ends, so an old - // intent's deferred deletion cannot end the chain that runs after it. - QMetaObject::Connection chainBackstopConnection; }; #endif // COCKATRICE_URL_PARSER_H diff --git a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp index d5a168708..9cde72c01 100644 --- a/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp +++ b/cockatrice/src/interface/palette_editor/palette_editor_dialog.cpp @@ -1,5 +1,6 @@ #include "palette_editor_dialog.h" +#include "../../client/settings/cache_settings.h" #include "../theme_manager.h" #include "palette_generator.h" #include "palette_grid_widget.h" @@ -10,11 +11,31 @@ #include #include #include +#include #include +#include #include +#include #include #include +#include #include +#include + +// Probe whether a directory is truly writable by trying to create and remove a +// temporary file. QFileInfo::isWritable() on a directory is unreliable (notably +// on Windows where UAC VirtualStore can make a system dir appear writable). +static bool isDirReallyWritable(const QString &dirPath) +{ + const QString probe = QDir(dirPath).absoluteFilePath(".cockatrice_write_test"); + QFile f(probe); + if (!f.open(QIODevice::WriteOnly)) { + return false; + } + f.close(); + f.remove(); + return true; +} PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QString &_themeName, QWidget *parent) : QDialog(parent), themeDirPath(_themeDirPath), themeName(_themeName) @@ -25,7 +46,14 @@ PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QSt // Resolve a writable directory for saving. Built-in (Default / Fusion) and // other read-only theme directories must be customised in the user-writable // themes directory; otherwise the write would fail or be lost on upgrade. - saveDir = ThemeManager::writableThemeDir(themeName); + if (!themeDirPath.isEmpty() && isDirReallyWritable(themeDirPath)) { + saveDir = themeDirPath; + } else { + saveDir = QDir(SettingsCache::instance().paths().getThemesPath()).absoluteFilePath(themeName); + if (!QDir().mkpath(saveDir)) { + qWarning() << "Failed to create palette save directory:" << saveDir; + } + } // Load both scheme configs upfront so switching is instant loadSchemes(); @@ -186,7 +214,7 @@ void PaletteEditorDialog::retranslateUi() resetBtn->setToolTip(tr("Discard unsaved edits and restore the last saved palette")); saveBtn->setToolTip(tr("Write palette-%1.toml and reload the theme").arg(loadedScheme.toLower())); - if (saveDir.isEmpty() || !ThemeManager::isDirReallyWritable(saveDir)) { + if (saveDir.isEmpty() || !isDirReallyWritable(saveDir)) { saveBtn->setEnabled(false); saveBtn->setToolTip(tr("Cannot save: this theme has no writable directory")); } @@ -269,7 +297,7 @@ void PaletteEditorDialog::onSave() if (it.key() == loadedScheme) { continue; } - if (it.value() == savedConfig.value(it.key())) { + if (it.value().colors == savedConfig.value(it.key()).colors) { continue; } if (!ThemeManager::commitPalette(saveDir, it.key(), it.value())) { @@ -280,7 +308,7 @@ void PaletteEditorDialog::onSave() } // Commit the active scheme last so the global colour scheme matches. - if (workingConfig[loadedScheme] != savedConfig.value(loadedScheme)) { + if (workingConfig[loadedScheme].colors != savedConfig.value(loadedScheme).colors) { if (!ThemeManager::commitPalette(saveDir, loadedScheme, workingConfig[loadedScheme])) { QMessageBox::warning(this, tr("Save failed"), tr("Could not write %1 to:\n%2").arg(PaletteConfig::fileName(loadedScheme), saveDir)); diff --git a/cockatrice/src/interface/palette_editor/palette_generator.cpp b/cockatrice/src/interface/palette_editor/palette_generator.cpp index 822e57250..d30dd14f1 100644 --- a/cockatrice/src/interface/palette_editor/palette_generator.cpp +++ b/cockatrice/src/interface/palette_editor/palette_generator.cpp @@ -150,17 +150,6 @@ PaletteConfig fromAccent(const QColor &accent, int intensity, const QString &sch cfg.colors[CG::Disabled][CR::HighlightedText] = disText; cfg.colors[CG::Inactive][CR::HighlightedText] = dark ? Qt::white : Qt::black; - // Accent: same primary hue as Highlight, so palettes derived from a - // QuickSetup accent always carry a matching Accent role. -#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) - set3(CR::Accent, hl, disText, hl); -#endif - - // Application role colors: Strong tracks the primary accent, while Soft is - // the lightened, desaturated companion used for button-gradient highlights. - cfg.appColors[AppColor::AccentStrong] = hl; - cfg.appColors[AppColor::AccentSoft] = hsl(accent.lightness() + 60, qRound(accent.hslSaturation() * 70 / 100.0)); - // BrightText QColor bright; if (achromatic) { diff --git a/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp b/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp index 97d28b731..67294cd98 100644 --- a/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp +++ b/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp @@ -1,7 +1,5 @@ #include "palette_grid_widget.h" -#include "../theme_manager.h" - #include #include #include @@ -47,11 +45,6 @@ static const QMap ROLE_DESCRIPTIONS = { {QPalette::Shadow, QT_TR_NOOP("Very dark shadow colour")}, }; -static const QMap APP_ROLE_DESCRIPTIONS = { - {AppColor::AccentStrong, QT_TR_NOOP("Vivid primary accent (e.g. home-tab button gradient start)")}, - {AppColor::AccentSoft, QT_TR_NOOP("Lightened, desaturated accent (e.g. home-tab button gradient end)")}, -}; - PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent) { scroll = new QScrollArea(this); @@ -129,46 +122,6 @@ void PaletteGridWidget::buildGrid(QWidget *host) grid->addWidget(btn, row + 1, col + 1, Qt::AlignHCenter | Qt::AlignVCenter); } } - - // Application color section: one ColorButton per role below the role grid. - // These are not tied to a color group, so a single button spans the row. - QMetaEnum appEnum = QMetaEnum::fromType(); - - 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(appEnum.value(i)); - const int row = appHeaderRow + 1 + i; - - if (i % 2 == 0) { - for (int col = 0; col < 4; ++col) { - auto *shade = new QWidget(host); - shade->setAutoFillBackground(true); - grid->addWidget(shade, row, col); - rowShadeWidgets.push_back(shade); - } - } - - auto *label = new QLabel(QString(appEnum.valueToKey(role)), host); - label->setToolTip(APP_ROLE_DESCRIPTIONS.value(role, {})); - label->setContentsMargins(4, 2, 8, 2); - grid->addWidget(label, row, 0); - - auto *btn = new ColorButton(host); - connect(btn, &ColorButton::colorChanged, this, [this] { emit paletteChanged(); }); - appColorButtons[role] = btn; - grid->addWidget(btn, row, 1, Qt::AlignHCenter | Qt::AlignVCenter); - } } void PaletteGridWidget::changeEvent(QEvent *e) @@ -213,16 +166,6 @@ void PaletteGridWidget::loadPalette(const PaletteConfig &cfg) colorButtons[group][role]->setColor(color); } } - - QMetaEnum appEnum = QMetaEnum::fromType(); - for (int i = 0; i < appEnum.keyCount(); ++i) { - auto role = static_cast(appEnum.value(i)); - QColor color = cfg.appColors.value(role); - if (!color.isValid()) { - color = themeManager->appColor(role); - } - appColorButtons[role]->setColor(color); - } } PaletteConfig PaletteGridWidget::currentPaletteConfig() const @@ -233,12 +176,5 @@ PaletteConfig PaletteGridWidget::currentPaletteConfig() const cfg.colors[group][role] = colorButtons[group][role]->getColor(); } } - - QMetaEnum appEnum = QMetaEnum::fromType(); - for (int i = 0; i < appEnum.keyCount(); ++i) { - auto role = static_cast(appEnum.value(i)); - cfg.appColors[role] = appColorButtons[role]->getColor(); - } - return cfg; } \ No newline at end of file diff --git a/cockatrice/src/interface/palette_editor/palette_grid_widget.h b/cockatrice/src/interface/palette_editor/palette_grid_widget.h index 77cbf1c62..1a665971a 100644 --- a/cockatrice/src/interface/palette_editor/palette_grid_widget.h +++ b/cockatrice/src/interface/palette_editor/palette_grid_widget.h @@ -31,7 +31,6 @@ private: void refreshChromePalettes(); QMap> colorButtons; - QMap appColorButtons; QScrollArea *scroll; QWidget *gridHost; QVBoxLayout *layout; diff --git a/cockatrice/src/interface/pixel_map_generator.cpp b/cockatrice/src/interface/pixel_map_generator.cpp index b70dc576f..e74e86471 100644 --- a/cockatrice/src/interface/pixel_map_generator.cpp +++ b/cockatrice/src/interface/pixel_map_generator.cpp @@ -16,6 +16,7 @@ #define DEFAULT_COLOR_MODERATOR_LEFT "#ffffff"; #define DEFAULT_COLOR_MODERATOR_RIGHT "#000000"; #define DEFAULT_COLOR_ADMIN "#ff2701"; +#define DEFAULT_COLOR_DEVELOPER "#B8B8B8" /** * Clamps an svg render size so that rendering does not exceed a multiple of the requested size. @@ -361,10 +362,6 @@ static QString getIconType(const bool isBuddy, const UserLevelFlags &userLevelFl return "pawn_judge"; } - if (userLevelFlags.testFlag(ServerInfo_User::IsDeveloper)) { - return "pawn_dev"; - } - if (!privLevel.isEmpty() && privLevel.toLower() != "none") { return QString("pawn_%1").arg(privLevel.toLower()); } @@ -385,6 +382,8 @@ QIcon UserLevelPixmapGenerator::generateIconDefault(int height, if (userLevel.testFlag(ServerInfo_User::IsAdmin)) { colorLeft = DEFAULT_COLOR_ADMIN; + } else if (userLevel.testFlag(ServerInfo_User::IsDeveloper)) { + colorLeft = DEFAULT_COLOR_DEVELOPER; } else if (userLevel.testFlag(ServerInfo_User::IsModerator)) { colorLeft = DEFAULT_COLOR_MODERATOR_LEFT; colorRight = DEFAULT_COLOR_MODERATOR_RIGHT; diff --git a/cockatrice/src/interface/theme_config.cpp b/cockatrice/src/interface/theme_config.cpp index 8293a82cf..3c43c467d 100644 --- a/cockatrice/src/interface/theme_config.cpp +++ b/cockatrice/src/interface/theme_config.cpp @@ -16,7 +16,7 @@ QString ThemeConfig::toIni() const out += "[Appearance]\n"; out += QString("ColorScheme = %1\n").arg(colorScheme.isEmpty() ? "System" : colorScheme); out += "\n[Style]\n"; - out += QString("Name = %1\n").arg(styleName.isEmpty() ? "System" : styleName); + out += QString("Name = %1\n").arg(styleName.isEmpty() ? "Default" : styleName); return out; } @@ -96,7 +96,7 @@ bool ThemeConfig::save(const QString &themeDirPath) const bool PaletteConfig::hasPalette() const { - return !colors.isEmpty() || !appColors.isEmpty(); + return !colors.isEmpty(); } QString PaletteConfig::toToml() const @@ -133,24 +133,6 @@ QString PaletteConfig::toToml() const out += "\n"; } - if (!appColors.isEmpty()) { - QMetaEnum appEnum = QMetaEnum::fromType(); - - out += "[AppColors]\n"; - - for (auto it = appColors.cbegin(); it != appColors.cend(); ++it) { - const char *roleName = appEnum.valueToKey(it.key()); - - if (!roleName) { - continue; - } - - out += QString("%1 = %2\n").arg(QString(roleName), -20).arg(it.value().name(QColor::HexArgb)); - } - - out += "\n"; - } - return out; } @@ -170,7 +152,6 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath) } QMetaEnum roleEnum = QMetaEnum::fromType(); - QMetaEnum appEnum = QMetaEnum::fromType(); QString currentSection; QPalette::ColorGroup currentGroup = QPalette::Active; @@ -221,26 +202,6 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath) } } - QColor color(value); - - if (!color.isValid()) { - continue; - } - - if (currentSection.compare("AppColors", Qt::CaseInsensitive) == 0) { - if (key.startsWith("AppColor::")) { - key = key.mid(10); - } - - int appRoleInt = appEnum.keyToValue(key.toUtf8().constData()); - - if (appRoleInt >= 0) { - cfg.appColors[static_cast(appRoleInt)] = color; - } - - continue; - } - if (!currentSection.startsWith("Palette", Qt::CaseInsensitive)) { continue; } @@ -255,7 +216,11 @@ PaletteConfig PaletteConfig::fromFile(const QString &filePath) continue; } - cfg.colors[currentGroup][static_cast(roleInt)] = color; + QColor color(value); + + if (color.isValid()) { + cfg.colors[currentGroup][static_cast(roleInt)] = color; + } } return cfg; diff --git a/cockatrice/src/interface/theme_config.h b/cockatrice/src/interface/theme_config.h index 567aeccda..07bf55b7a 100644 --- a/cockatrice/src/interface/theme_config.h +++ b/cockatrice/src/interface/theme_config.h @@ -3,25 +3,9 @@ #include #include -#include #include #include -// Application-specific color roles, layered on top of the fixed QPalette role -// set. Stored in the same palette-.toml under an [AppColors] section -// and editable from the palette editor, so theme authors can control colors -// beyond what Qt's palette can express. -namespace AppColor -{ -Q_NAMESPACE -enum Role -{ - AccentStrong, - AccentSoft, -}; -Q_ENUM_NS(Role) -} // namespace AppColor - struct ThemeConfig { QString colorScheme; @@ -37,16 +21,7 @@ struct ThemeConfig struct PaletteConfig { QMap> colors; - QMap appColors; - bool operator==(const PaletteConfig &rhs) const - { - return colors == rhs.colors && appColors == rhs.appColors; - } - bool operator!=(const PaletteConfig &rhs) const - { - return !(*this == rhs); - } bool hasPalette() const; QString toToml() const; diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index d86ed77f9..12c8fad2c 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -22,7 +21,7 @@ #include #include -#define SYSTEM_THEME_NAME "System" +#define NONE_THEME_NAME "Default" #define FUSION_THEME_NAME "Fusion" #define STYLE_CSS_NAME "style.css" #define HANDZONE_BG_NAME "handzone" @@ -96,7 +95,7 @@ struct PaletteColorInfo static QString usableDefaultStyle(const QString &style) { // The Windows 11 native style is broken: when the OS default - // ("System" theme selection) would use it, fall back to the Vista style. + // ("Default" theme selection) would use it, fall back to the Vista style. // Explicitly choosing "windows11" in a theme is still honored. return style.compare("windows11", Qt::CaseInsensitive) == 0 ? QStringLiteral("windowsvista") : style; } @@ -119,16 +118,10 @@ ThemeManager::ThemeManager(QObject *parent) : QObject(parent) void ThemeManager::ensureThemeDirectoryExists() { - auto &settings = SettingsCache::instance(); - - // Migrate the old "Default" theme name to "System" - if (settings.getThemeName() == "Default") { - settings.setThemeName(SYSTEM_THEME_NAME); - } - - if (settings.getThemeName().isEmpty() || !getAvailableThemes().contains(settings.getThemeName())) { + if (SettingsCache::instance().getThemeName().isEmpty() || + !getAvailableThemes().contains(SettingsCache::instance().getThemeName())) { qCInfo(ThemeManagerLog) << "Theme name not set, setting default value"; - settings.setThemeName(FUSION_THEME_NAME); + SettingsCache::instance().setThemeName(NONE_THEME_NAME); } } @@ -191,32 +184,11 @@ QString ThemeManager::assetPath(QStringView prefix) const return resolvedPlain.isEmpty() ? prefix.toString() : resolvedPlain; } -// Probe whether a directory is truly writable by trying to create and remove a -// temporary file. QFileInfo::isWritable() on a directory is unreliable (notably -// on Windows where UAC VirtualStore can make a system dir appear writable). -bool ThemeManager::isDirReallyWritable(const QString &dirPath) +bool ThemeManager::isBuiltInTheme() { - const QString probe = QDir(dirPath).absoluteFilePath(".cockatrice_write_test"); - QFile f(probe); - if (!f.open(QIODevice::WriteOnly)) { - return false; - } - f.close(); - f.remove(); - return true; -} + const auto themeName = SettingsCache::instance().getThemeName(); -QString ThemeManager::writableThemeDir(const QString &themeName) -{ - // All theme writes go to the user themes directory regardless of whether - // the resolved (system) theme directory happens to be writable. Even when a - // write would succeed in-place, routing it to the user directory keeps the - // install intact and guarantees changes survive upgrades. - const QString dirPath = QDir(SettingsCache::instance().paths().getThemesPath()).absoluteFilePath(themeName); - if (!QDir().mkpath(dirPath)) { - qWarning() << "Failed to create theme save directory:" << dirPath; - } - return dirPath; + return themeName == NONE_THEME_NAME || themeName == FUSION_THEME_NAME; } // System (read-only) themes location, relative to the application binary. @@ -241,7 +213,9 @@ QStringMap &ThemeManager::getAvailableThemes() // load themes from user profile dir dir.setPath(SettingsCache::instance().paths().getThemesPath()); - availableThemes.insert(SYSTEM_THEME_NAME, dir.absoluteFilePath("System")); + // add default value + availableThemes.insert(NONE_THEME_NAME, dir.absoluteFilePath("Default")); + availableThemes.insert(FUSION_THEME_NAME, dir.absoluteFilePath("Fusion")); for (QString themeName : dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Name)) { @@ -357,7 +331,7 @@ bool ThemeManager::commitPalette(const QString &themeDirPath, const QString &col void ThemeManager::setColorScheme(const QString &scheme) { - const QString dirPath = writableThemeDir(SettingsCache::instance().getThemeName()); + const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName()); ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath); cfg.colorScheme = scheme; @@ -368,7 +342,7 @@ void ThemeManager::setColorScheme(const QString &scheme) void ThemeManager::setStyleName(const QString &styleName) { - const QString dirPath = writableThemeDir(SettingsCache::instance().getThemeName()); + const QString dirPath = getAvailableThemes().value(SettingsCache::instance().getThemeName()); ThemeConfig cfg = ThemeConfig::fromThemeDir(dirPath); cfg.styleName = styleName; @@ -399,7 +373,7 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName, Q_UNUSED(activeScheme) #endif QString styleName = themeCfg.styleName; - if (styleName.isEmpty() || styleName.compare("System", Qt::CaseInsensitive) == 0) { + if (styleName.isEmpty() || styleName.compare("Default", Qt::CaseInsensitive) == 0) { if (themeName == FUSION_THEME_NAME) { styleName = "Fusion"; } else { @@ -442,8 +416,6 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName, qApp->setPalette(base); qApp->setStyle(style); - currentAppColors = palCfg.appColors; - // Force every widget to re-polish and repaint immediately rather than // waiting for natural expose events, which produces a patchwork of old // and new colours during a live preview. @@ -456,35 +428,6 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName, style->polish(widget); widget->update(); } - - emit paletteChanged(); -} - -QColor ThemeManager::appColor(AppColor::Role role) const -{ - const auto it = currentAppColors.constFind(role); - if (it != currentAppColors.constEnd()) { - return it.value(); - } - - // QPalette::Accent was introduced in Qt 6.6 and several shipped palettes - // set it to a value barely distinguishable from Window, so it is not a - // reliable accent source. The selection highlight is the stable accent - // (Accent defaults to Highlight when unset), and deriving from it - // unconditionally keeps every Qt version rendering identically. - const QColor accent = qApp->palette().color(QPalette::Active, QPalette::Highlight); - - if (role == AppColor::AccentSoft) { - constexpr int SOFT_SATURATION_PERCENT = 70; - constexpr int SOFT_LIGHTNESS_OFFSET = 60; - - // Light end of the gradient: same hue, softened and lightened - return QColor::fromHsl(qMax(0, accent.hslHue()), - qBound(0, qRound(accent.hslSaturation() * SOFT_SATURATION_PERCENT / 100.0), 255), - qBound(0, accent.lightness() + SOFT_LIGHTNESS_OFFSET, 255)); - } - - return accent; } void ThemeManager::themeChangedSlot() @@ -521,19 +464,8 @@ void ThemeManager::themeChangedSlot() // ── Load palette: custom first, then theme default ──────────────────── PaletteConfig palette = PaletteConfig::fromScheme(dirPath, activeScheme); - const PaletteConfig themeDefault = ThemeManager::loadDefaultPaletteConfig(dirPath, themeName, activeScheme); - if (palette.hasPalette()) { - // A custom palette written before [AppColors] existed carries no app - // colors; merge the theme's shipped defaults so the identity colors - // survive (hasPalette() counts an app-colors-only file as a palette, - // so those are kept wholesale and never reach here empty). - for (auto it = themeDefault.appColors.cbegin(); it != themeDefault.appColors.cend(); ++it) { - if (!palette.appColors.contains(it.key())) { - palette.appColors.insert(it.key(), it.value()); - } - } - } else { - palette = themeDefault; + if (!palette.hasPalette()) { + palette = ThemeManager::loadDefaultPaletteConfig(dirPath, themeName, activeScheme); } applyStyleAndPalette(themeName, themeCfg, palette, activeScheme); diff --git a/cockatrice/src/interface/theme_manager.h b/cockatrice/src/interface/theme_manager.h index aadb38ee9..ac35042a0 100644 --- a/cockatrice/src/interface/theme_manager.h +++ b/cockatrice/src/interface/theme_manager.h @@ -50,7 +50,6 @@ private: QString currentThemePath; std::array brushes; QStringMap availableThemes; - QMap currentAppColors; /* Internal cache for multiple backgrounds */ @@ -66,16 +65,7 @@ protected: const QString &activeScheme); public: - // Resolves the directory to write theme changes to for the given theme - // name. The resolved theme dir (user or system) is used when writable; - // read-only system themes fall back to the user themes directory, creating - // it if needed, so customisations never get lost on upgrade. - static QString writableThemeDir(const QString &themeName); - // Probe whether a directory is truly writable by trying to create and remove - // a temporary file. QFileInfo::isWritable() on a directory is unreliable - // (notably on Windows where UAC VirtualStore can make a system dir appear - // writable). - static bool isDirReallyWritable(const QString &dirPath); + bool isBuiltInTheme(); // Explicit color scheme of the theme: theme.cfg's ColorScheme setting // (Dark/Light), falling back to the OS color scheme when it is "System". bool isDarkMode(const QString &themeDirPath) const; @@ -125,17 +115,12 @@ public: void reloadCurrentTheme(); void previewPalette(const PaletteConfig &cfg, const QString &scheme); - // Resolves an application color role: the theme's stored [AppColors] value - // when present, otherwise a palette-accent-derived fallback. - QColor appColor(AppColor::Role role) const; - QBrush &getBgBrush(Role zone); QBrush getExtraBgBrush(Role zone, int zoneId = 0); protected slots: void themeChangedSlot(); signals: void themeChanged(); - void paletteChanged(); }; extern ThemeManager *themeManager; diff --git a/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp b/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp index 2199faf30..1ea1bcb10 100644 --- a/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/additional_info/color_identity_widget.cpp @@ -85,7 +85,7 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event) } lastWidth = totalWidth; - const int totalHeight = qMax(0, totalWidth / 6); // Set height to 1/4 of the width + const int totalHeight = totalWidth / 6; // Set height to 1/4 of the width setFixedHeight(totalHeight); const int count = layout->count(); @@ -97,10 +97,6 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event) const int availableWidth = totalWidth - (spacing * (count - 1)); const int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height - if (iconSize <= 0) { - lastIconSize = iconSize; - return; - } if (iconSize == lastIconSize) { return; } diff --git a/cockatrice/src/interface/widgets/cards/additional_info/deck_color_identity.cpp b/cockatrice/src/interface/widgets/cards/additional_info/deck_color_identity.cpp deleted file mode 100644 index 62b01511e..000000000 --- a/cockatrice/src/interface/widgets/cards/additional_info/deck_color_identity.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include "deck_color_identity.h" - -#include -#include -#include -#include - -QString getDeckColorIdentity(const DeckList &deck, const CardDatabaseQuerier *db) -{ - const QStringList cardList = deck.getCardList({DECK_ZONE_MAIN, DECK_ZONE_SIDE}); - if (cardList.isEmpty()) { - return {}; - } - - QSet colorSet; // A set to collect unique color symbols (e.g., W, U, B, R, G) - - for (const QString &cardName : cardList) { - CardInfoPtr currentCard = db->getCardInfo(cardName); - if (currentCard) { - const QString colors = currentCard->getColors(); // returns something like "WUB" - for (const QChar &color : colors) { - colorSet.insert(color); - } - } - } - - // Ensure the color identity is in WUBRG order - QString colorIdentity; - const QString wubrgOrder = "WUBRG"; - for (const QChar &color : wubrgOrder) { - if (colorSet.contains(color)) { - colorIdentity.append(color); - } - } - - return colorIdentity; -} diff --git a/cockatrice/src/interface/widgets/cards/additional_info/deck_color_identity.h b/cockatrice/src/interface/widgets/cards/additional_info/deck_color_identity.h deleted file mode 100644 index 04294cb1c..000000000 --- a/cockatrice/src/interface/widgets/cards/additional_info/deck_color_identity.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef COCKATRICE_DECK_COLOR_IDENTITY_H -#define COCKATRICE_DECK_COLOR_IDENTITY_H - -#include - -class CardDatabaseQuerier; -class DeckList; - -/** - * @brief Computes the color identity of a deck (e.g. "WUBRG") from the color - * symbols of all cards in the main deck and sideboard, ordered WUBRG. - * - * Shared as a free function so the deck storage previews and the deck share - * dialog compute identities identically. - * - * @param db Card database used to look up card color symbols. - */ -QString getDeckColorIdentity(const DeckList &deck, const CardDatabaseQuerier *db); - -#endif // COCKATRICE_DECK_COLOR_IDENTITY_H diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp index 2dd21e78a..de622bdc8 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp @@ -74,8 +74,6 @@ CardInfoPictureWidget::CardInfoPictureWidget(QWidget *parent, const bool _hoverT update(); }); - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this, - &CardInfoPictureWidget::updatePixmap); } /** diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp index 000a88b2f..c5cb59b3b 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_with_text_overlay_widget.cpp @@ -133,14 +133,12 @@ void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event) path.addRoundedRect(glowRect, radius, radius); // Soft outer glow - QColor glowColor = palette().color(QPalette::Highlight); - glowColor.setAlpha(80); + QColor glowColor(0, 150, 255, 80); // subtle blu painter.setPen(QPen(glowColor, 6)); painter.drawPath(path); // Thin inner border for crispness - QColor borderColor = palette().color(QPalette::Highlight); - borderColor.setAlpha(200); + QColor borderColor(0, 150, 255, 200); painter.setPen(QPen(borderColor, 2)); painter.drawRoundedRect(pixmapRect, radius, radius); diff --git a/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp index e98c3c02a..c6af5320b 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_text_widget.cpp @@ -1,7 +1,6 @@ #include "card_info_text_widget.h" #include "../../../game_graphics/board/card_item.h" -#include "../../card_localization.h" #include #include @@ -11,7 +10,7 @@ #include #include -CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent) +CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent), info(nullptr) { propsLabel = new QLabel; propsLabel->setOpenExternalLinks(false); @@ -40,12 +39,6 @@ CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent) grid->setRowStretch(1, 1); retranslateUi(); - - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this, [this] { - if (currentCard) { - setCard(currentCard); - } - }); } void CardInfoTextWidget::setTexts(const QString &propsText, const QString &textText) @@ -67,7 +60,7 @@ void CardInfoTextWidget::setCard(const ExactCard &exactCard) QString text = ""; text += QString("") - .arg(tr("Name:"), CardLocalization::displayName(card).toHtmlEscaped()); + .arg(tr("Name:"), card->getName().toHtmlEscaped()); if (!exactCard.getPrinting().isEmpty()) { QString setShort = exactCard.getPrinting().getSet()->getShortName().toHtmlEscaped(); @@ -101,8 +94,7 @@ void CardInfoTextWidget::setCard(const ExactCard &exactCard) } text += "
%1%2
"; - setTexts(text, CardLocalization::displayText(card)); - currentCard = exactCard; + setTexts(text, card->getText()); } void CardInfoTextWidget::setInvalidCardName(const QString &cardName) diff --git a/cockatrice/src/interface/widgets/cards/card_info_text_widget.h b/cockatrice/src/interface/widgets/cards/card_info_text_widget.h index 683be5ab5..a9c29da37 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_text_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_info_text_widget.h @@ -23,7 +23,7 @@ private: QLabel *propsLabel; QScrollArea *propsScroll; QTextEdit *textLabel; - ExactCard currentCard; ///< Last card set, re-rendered when the card language changes. + CardInfoPtr info; void setTexts(const QString &propsText, const QString &textText); public: diff --git a/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.cpp b/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.cpp index 147143e7f..1614836fc 100644 --- a/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.cpp @@ -27,23 +27,18 @@ DeckPreviewCardPictureWidget::DeckPreviewCardPictureWidget(QWidget *parent, const QColor &textColor, const QColor &outlineColor, const int fontSize, - const Qt::Alignment alignment, - const bool _emitClickImmediately) + const Qt::Alignment alignment) : CardInfoPictureWithTextOverlayWidget(parent, hoverToZoomEnabled, raiseOnEnter, textColor, outlineColor, fontSize, - alignment), - emitClickImmediately(_emitClickImmediately) + alignment) { singleClickTimer = new QTimer(this); singleClickTimer->setSingleShot(true); - connect(singleClickTimer, &QTimer::timeout, this, [this]() { - emit imageClicked(lastMouseEvent, this); - emit imageSingleClicked(); - }); + connect(singleClickTimer, &QTimer::timeout, this, [this]() { emit imageClicked(lastMouseEvent, this); }); connect(&SettingsCache::instance().visualDeckStorage(), &VisualDeckStorageSettings::visualDeckStorageSelectionAnimationChanged, this, &CardInfoPictureWidget::setRaiseOnEnterEnabled); @@ -52,13 +47,8 @@ DeckPreviewCardPictureWidget::DeckPreviewCardPictureWidget(QWidget *parent, void DeckPreviewCardPictureWidget::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { - if (emitClickImmediately) { - emit imageClicked(event, this); - emit imageSingleClicked(); - } else { - lastMouseEvent = event; - singleClickTimer->start(QApplication::doubleClickInterval()); - } + lastMouseEvent = event; + singleClickTimer->start(QApplication::doubleClickInterval()); } else { emit imageClicked(event, this); event->accept(); @@ -68,14 +58,7 @@ void DeckPreviewCardPictureWidget::mousePressEvent(QMouseEvent *event) void DeckPreviewCardPictureWidget::mouseDoubleClickEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { - if (emitClickImmediately) { - // Do not report a second single click for the second press of the - // double-click; the consumer maps the double-click to select+open. - lastMouseEvent = event; - emit imageDoubleClicked(event, this); - } else { - singleClickTimer->stop(); // Prevent single-click logic - emit imageDoubleClicked(lastMouseEvent, this); - } + singleClickTimer->stop(); // Prevent single-click logic + emit imageDoubleClicked(lastMouseEvent, this); } } diff --git a/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.h b/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.h index 7571bc256..154e938aa 100644 --- a/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.h +++ b/cockatrice/src/interface/widgets/cards/deck_preview_card_picture_widget.h @@ -20,38 +20,21 @@ class DeckPreviewCardPictureWidget final : public CardInfoPictureWithTextOverlay Q_OBJECT public: - /** - * @brief Constructs a DeckPreviewCardPictureWidget. - * @param parent The parent widget. - * @param hoverToZoomEnabled If this widget will spawn a larger widget when hovered over. - * @param raiseOnEnter If the widget raises its border when the mouse enters. - * @param textColor The color of the overlay text. - * @param outlineColor The color of the outline around the text. - * @param fontSize The font size of the overlay text. - * @param alignment The alignment of the text within the overlay. - * @param emitClickImmediately If true, a left click is reported immediately on click - * instead of after the double-click interval. Use this for selection surfaces - * where reacting to a double-click (select-and-open) would needlessly delay the - * single-click feedback. The double-click signal is still emitted. - */ explicit DeckPreviewCardPictureWidget(QWidget *parent, bool hoverToZoomEnabled = false, bool raiseOnEnter = false, const QColor &textColor = Qt::white, const QColor &outlineColor = Qt::black, int fontSize = 12, - Qt::Alignment alignment = Qt::AlignCenter, - bool _emitClickImmediately = false); + Qt::Alignment alignment = Qt::AlignCenter); signals: void imageClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); - void imageSingleClicked(); void imageDoubleClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); private: QTimer *singleClickTimer; QMouseEvent *lastMouseEvent = nullptr; // Store the last mouse event - bool emitClickImmediately; protected: void mousePressEvent(QMouseEvent *event) override; diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp index 4b141e255..a3653f03e 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp @@ -345,9 +345,7 @@ ExactCard DeckEditorDeckDockWidget::getCurrentCard() if (!current.isValid()) { return {}; } - // The display role holds the localized card name; the edit role always carries the - // canonical English name needed to look the card up in the database. - const QString cardName = current.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); + const QString cardName = current.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); const QString cardProviderID = current.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString(); const QModelIndex gparent = current.parent().parent(); @@ -520,11 +518,8 @@ void DeckEditorDeckDockWidget::syncBannerCardComboBoxSelectionWithDeck() void DeckEditorDeckDockWidget::setSelectedIndex(const QModelIndex &newCardIndex, bool preserveWidgetFocus) { - const QModelIndex proxyIndex = proxy->mapFromSource(newCardIndex); - deckView->clearSelection(); - deckView->setCurrentIndex(proxyIndex); - deckView->scrollTo(proxyIndex); + deckView->setCurrentIndex(newCardIndex); recursiveExpand(newCardIndex); if (!preserveWidgetFocus) { diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp index cfe989e9f..e563729a4 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp @@ -1,20 +1,13 @@ #include "deck_state_manager.h" -#include "../../../client/settings/cache_settings.h" - #include #include #include -#include DeckStateManager::DeckStateManager(QObject *parent) : QObject(parent), deckList(QSharedPointer(new DeckList)), deckListModel(new DeckListModel(this, deckList)), historyManager(new DeckListHistoryManager(this)) { - deckListModel->setDisplayLanguage(SettingsCache::instance().cardsDisplay().getCardLang()); - connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, deckListModel, - [this](const QString &lang) { deckListModel->setDisplayLanguage(lang); }); - connect(historyManager, &DeckListHistoryManager::undoRedoStateChanged, this, [this] { setModified(true); emit historyChanged(); @@ -267,10 +260,7 @@ bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx) return false; } - // The display role holds the localized card name; the edit role always carries the - // canonical English name needed to look the card up in the database. - QString displayCardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); - QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString(); + QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); QString providerId = idx.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data().toString(); QModelIndex gparent = idx.parent().parent(); @@ -287,7 +277,7 @@ bool DeckStateManager::swapCardAtIndex(const QModelIndex &idx) QString reason = tr("Moved to %1 1 × \"%2\" (%3)") // .arg(otherZoneName) - .arg(displayCardName) + .arg(cardName) .arg(providerId); return modifyDeck(reason, [&idx, &cardName, &providerId, &otherZoneName](auto model) { @@ -301,8 +291,9 @@ bool DeckStateManager::removeCardAtIndex(const QModelIndex &idx) return false; } - QString reason = - tr("Removed \"%1\" (all copies)").arg(idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString()); + QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data().toString(); + + QString reason = tr("Removed \"%1\" (all copies)").arg(cardName); return modifyDeck(reason, [&idx](auto model) { return model->removeRow(idx.row(), idx.parent()); }); } diff --git a/cockatrice/src/interface/widgets/deck_share/deck_share_utils.cpp b/cockatrice/src/interface/widgets/deck_share/deck_share_utils.cpp deleted file mode 100644 index 4343f0aab..000000000 --- a/cockatrice/src/interface/widgets/deck_share/deck_share_utils.cpp +++ /dev/null @@ -1,59 +0,0 @@ -#include "deck_share_utils.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace DeckShareUtils -{ - -QString buildShareLink(const AbstractClient *client, const QString &token) -{ - QUrl url; - url.setScheme(QStringLiteral("cockatrice")); - url.setHost(QStringLiteral("opendeck")); - - QUrlQuery query; - query.addQueryItem(QStringLiteral("share"), token); - query.addQueryItem(QStringLiteral("hostname"), client->serverName()); - query.addQueryItem(QStringLiteral("port"), QString::number(client->serverPort())); - url.setQuery(query); - - return url.toString(QUrl::FullyEncoded); -} - -QString copyShareLinkToClipboard(const QString &link) -{ - QGuiApplication::clipboard()->setText(link); - return link; -} - -QString formatShareExpiry(const QDateTime &expiry) -{ - return QLocale().toString(expiry.toLocalTime(), QLocale::ShortFormat); -} - -ShareResponse handleShareResponse(const AbstractClient *client, const Response &response) -{ - const Response_DeckShareCreate &resp = response.GetExtension(Response_DeckShareCreate::ext); - const QString token = QString::fromStdString(resp.token()); - - const QString link = buildShareLink(client, token); - copyShareLinkToClipboard(link); - -#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) - const QDateTime expiry = QDateTime::fromSecsSinceEpoch(resp.expires_at(), QTimeZone::UTC); -#else - const QDateTime expiry = QDateTime::fromSecsSinceEpoch(resp.expires_at(), Qt::UTC); -#endif - - return {link, expiry}; -} - -} // namespace DeckShareUtils diff --git a/cockatrice/src/interface/widgets/deck_share/deck_share_utils.h b/cockatrice/src/interface/widgets/deck_share/deck_share_utils.h deleted file mode 100644 index c9f7afa8b..000000000 --- a/cockatrice/src/interface/widgets/deck_share/deck_share_utils.h +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @file deck_share_utils.h - * @ingroup DeckShareWidgets - */ -//! \todo Document this file. - -#ifndef DECK_SHARE_UTILS_H -#define DECK_SHARE_UTILS_H - -#include -#include - -class AbstractClient; -class Response; - -/** - * @brief Shared helpers for creating temporary deck shares. - */ -namespace DeckShareUtils -{ - -/** - * @brief The outcome of a successful share-create response. - */ -struct ShareResponse -{ - QString link; ///< The share link that was copied to the clipboard. - QDateTime expiry; ///< When the share expires (UTC). -}; - -/** - * @brief Builds the cockatrice:// link for a freshly created deck share. - * @param client Used to embed the target server's hostname and port. - * @param token The share token from Response_DeckShareCreate. - */ -QString buildShareLink(const AbstractClient *client, const QString &token); - -/** - * @brief Copies the share link to the clipboard. - * @return The link that was copied. - */ -QString copyShareLinkToClipboard(const QString &link); - -/** - * @brief Formats the expiration timestamp for a share. - */ -QString formatShareExpiry(const QDateTime &expiry); - -/** - * @brief Handles a successful Response_DeckShareCreate: builds the share link, - * copies it to the clipboard, and derives the share expiry. - * @param client Used to embed the target server's hostname and port. - * @param response The successful response carrying the share token and expiry. - */ -ShareResponse handleShareResponse(const AbstractClient *client, const Response &response); - -} // namespace DeckShareUtils - -#endif // DECK_SHARE_UTILS_H diff --git a/cockatrice/src/interface/widgets/deck_share/share_bar_widget.cpp b/cockatrice/src/interface/widgets/deck_share/share_bar_widget.cpp deleted file mode 100644 index 5aca343ca..000000000 --- a/cockatrice/src/interface/widgets/deck_share/share_bar_widget.cpp +++ /dev/null @@ -1,77 +0,0 @@ -#include "share_bar_widget.h" - -#include -#include -#include -#include - -ShareBarWidget::ShareBarWidget(QWidget *parent) : QWidget(parent) -{ - auto *layout = new QHBoxLayout(this); - layout->setContentsMargins(12, 10, 12, 10); - layout->setSpacing(8); - - hintLabel = new QLabel(this); - hintLabel->setWordWrap(true); - - nameEdit = new QLineEdit(this); - nameEdit->setMaximumWidth(260); - - countLabel = new QLabel(this); - - cancelButton = new QPushButton(this); - connect(cancelButton, &QPushButton::clicked, this, &ShareBarWidget::cancelRequested); - - createButton = new QPushButton(this); - createButton->setDefault(true); - connect(createButton, &QPushButton::clicked, this, &ShareBarWidget::createRequested); - - layout->addWidget(hintLabel, 1); - layout->addWidget(nameEdit); - layout->addWidget(countLabel); - layout->addStretch(); - layout->addWidget(cancelButton); - layout->addWidget(createButton); - - setLayout(layout); - - retranslateUi(); -} - -void ShareBarWidget::retranslateUi() -{ - nameEdit->setPlaceholderText(tr("Share name")); - cancelButton->setText(tr("Cancel")); - createButton->setText(tr("Create share link")); -} - -QString ShareBarWidget::name() const -{ - return nameEdit->text().trimmed(); -} - -void ShareBarWidget::setName(const QString &value) -{ - nameEdit->setText(value); -} - -void ShareBarWidget::setCountText(const QString &text) -{ - countLabel->setText(text); -} - -void ShareBarWidget::setHintText(const QString &text, bool visible) -{ - hintLabel->setText(text); - hintLabel->setVisible(visible); -} - -void ShareBarWidget::setCreateEnabled(bool enabled) -{ - createButton->setEnabled(enabled); -} - -void ShareBarWidget::focusName() -{ - nameEdit->setFocus(); -} diff --git a/cockatrice/src/interface/widgets/deck_share/share_bar_widget.h b/cockatrice/src/interface/widgets/deck_share/share_bar_widget.h deleted file mode 100644 index ede9fb41b..000000000 --- a/cockatrice/src/interface/widgets/deck_share/share_bar_widget.h +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @file share_bar_widget.h - * @ingroup DeckShareWidgets - */ -//! \todo Document this file. - -#ifndef SHARE_BAR_WIDGET_H -#define SHARE_BAR_WIDGET_H - -#include - -class QLabel; -class QLineEdit; -class QPushButton; - -/** - * @brief The activated toolbar used to create a temporary deck share. - * - * A single reusable component shared by the local visual deck storage and the - * remote server deck storage tabs, so the share workflow renders identically in - * both places. It owns its own widgets, strings, and layout; the owning tab only - * sets the count/hint text and reacts to the create/cancel signals. - */ -class ShareBarWidget final : public QWidget -{ - Q_OBJECT - -public: - explicit ShareBarWidget(QWidget *parent = nullptr); - - void retranslateUi(); - - /** @return The trimmed name entered by the user. */ - [[nodiscard]] QString name() const; - - /** @brief Resets the name field to the given default. */ - void setName(const QString &name); - - /** @brief Sets the selected-count summary label text. */ - void setCountText(const QString &text); - - /** @brief Sets the explainer hint text, showing it when @p visible is true. */ - void setHintText(const QString &text, bool visible); - - /** @brief Enables or disables the create-share-link button (guards double submission). */ - void setCreateEnabled(bool enabled); - - /** @brief Moves keyboard focus to the name field. */ - void focusName(); - -signals: - void createRequested(); - void cancelRequested(); - -private: - QLabel *hintLabel; - QLineEdit *nameEdit; - QLabel *countLabel; - QPushButton *cancelButton; - QPushButton *createButton; -}; - -#endif // SHARE_BAR_WIDGET_H diff --git a/cockatrice/src/interface/widgets/deck_share/shared_deck_preview_widget.cpp b/cockatrice/src/interface/widgets/deck_share/shared_deck_preview_widget.cpp deleted file mode 100644 index 21ec80e08..000000000 --- a/cockatrice/src/interface/widgets/deck_share/shared_deck_preview_widget.cpp +++ /dev/null @@ -1,140 +0,0 @@ -#include "shared_deck_preview_widget.h" - -#include "../cards/additional_info/color_identity_widget.h" -#include "../cards/deck_preview_card_picture_widget.h" - -#include -#include -#include -#include -#include -#include -#include - -SharedDeckPreviewWidget::SharedDeckPreviewWidget(QWidget *parent, - const CardDatabaseQuerier *querier, - const QString &deckName, - const QString &bannerCardName, - const QString &colorIdentity, - const QString &gameFormat, - const QString &deckToolTip) - : QWidget(parent) -{ - bannerCardDisplayWidget = - new DeckPreviewCardPictureWidget(this, false, false, Qt::white, Qt::black, 12, Qt::AlignCenter, true); - bannerCardDisplayWidget->setScaleFactor(100); - const ExactCard bannerCard = bannerCardName.isEmpty() ? ExactCard() : querier->getCard(CardRef{bannerCardName, {}}); - bannerCardDisplayWidget->setCard(bannerCard); - bannerCardDisplayWidget->setOverlayText(deckName); - setToolTip(deckToolTip.isEmpty() ? deckName : deckToolTip); - setFocusPolicy(Qt::StrongFocus); - setBaseAccessibleName(deckName); - - colorIdentityWidget = new ColorIdentityWidget(this, colorIdentity); - colorIdentityWidget->setVisible(!colorIdentity.isEmpty()); - - // gameFormat is server-supplied and the QLabel renders AutoText, so escape it. - gameFormatLabel = new QLabel(gameFormat.toHtmlEscaped(), this); - gameFormatLabel->setAlignment(Qt::AlignCenter); - gameFormatLabel->setVisible(!gameFormat.isEmpty()); - - selectionCheckBox = new QCheckBox(this); - selectionCheckBox->setToolTip(tr("Select this deck")); - // The tile itself is focusable (Space/Enter toggles); keep the checkbox - // from creating a second tab stop per tile. - selectionCheckBox->setFocusPolicy(Qt::NoFocus); - - // Selection frame reused from the deck-preview selection covenant: a - // palette(highlight) border around the banner card, shown while selected. - selectionFrame = new QFrame(bannerCardDisplayWidget); - selectionFrame->setAttribute(Qt::WA_TransparentForMouseEvents); - selectionFrame->setStyleSheet(QStringLiteral( - "QFrame { border: 2px solid palette(highlight); border-radius: 4px; background: transparent; }")); - selectionFrame->setVisible(false); - - auto *selectionRow = new QHBoxLayout; - selectionRow->addWidget(selectionCheckBox); - selectionRow->addStretch(1); - - auto *layout = new QVBoxLayout(this); - layout->setContentsMargins(0, 0, 0, 0); - layout->addLayout(selectionRow); - layout->addWidget(bannerCardDisplayWidget, 0, Qt::AlignHCenter); - layout->addWidget(colorIdentityWidget, 0, Qt::AlignHCenter); - layout->addWidget(gameFormatLabel, 0, Qt::AlignHCenter); - setLayout(layout); - - connect(selectionCheckBox, &QCheckBox::toggled, this, [this](bool checked) { - updateSelectionVisual(checked); - emit selectionToggled(checked); - }); - connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageClicked, this, - &SharedDeckPreviewWidget::toggleSelection); - connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this, - &SharedDeckPreviewWidget::activate); -} - -bool SharedDeckPreviewWidget::isSelected() const -{ - return selectionCheckBox->isChecked(); -} - -void SharedDeckPreviewWidget::setSelected(bool selected) -{ - if (isSelected() == selected) { - return; - } - selectionCheckBox->setChecked(selected); -} - -void SharedDeckPreviewWidget::updateSelectionVisual(bool selected) -{ - selectionFrame->setVisible(selected); - selectionFrame->raise(); - if (selected) { - setAccessibleName(baseAccessibleName + tr(" (selected)")); - } else { - setAccessibleName(baseAccessibleName); - } -} - -void SharedDeckPreviewWidget::setBaseAccessibleName(const QString &name) -{ - baseAccessibleName = name; - setAccessibleName(name); -} - -void SharedDeckPreviewWidget::toggleSelection() -{ - setSelected(!isSelected()); -} - -void SharedDeckPreviewWidget::activate() -{ - setSelected(true); - emit activated(); -} - -void SharedDeckPreviewWidget::resizeEvent(QResizeEvent *event) -{ - QWidget::resizeEvent(event); - updateSelectionFrameGeometry(); -} - -void SharedDeckPreviewWidget::updateSelectionFrameGeometry() -{ - if (selectionFrame == nullptr || bannerCardDisplayWidget == nullptr) { - return; - } - selectionFrame->setGeometry(bannerCardDisplayWidget->rect().adjusted(1, 1, -1, -1)); -} - -void SharedDeckPreviewWidget::keyPressEvent(QKeyEvent *event) -{ - if (event->key() == Qt::Key_Space || event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { - toggleSelection(); - event->accept(); - return; - } - QWidget::keyPressEvent(event); -} diff --git a/cockatrice/src/interface/widgets/deck_share/shared_deck_preview_widget.h b/cockatrice/src/interface/widgets/deck_share/shared_deck_preview_widget.h deleted file mode 100644 index bd4c4ee5f..000000000 --- a/cockatrice/src/interface/widgets/deck_share/shared_deck_preview_widget.h +++ /dev/null @@ -1,77 +0,0 @@ -/** - * @file shared_deck_preview_widget.h - * @ingroup DeckShareWidgets - */ -//! \todo Document this file. - -#ifndef SHARED_DECK_PREVIEW_WIDGET_H -#define SHARED_DECK_PREVIEW_WIDGET_H - -#include - -class ColorIdentityWidget; -class DeckPreviewCardPictureWidget; -class QCheckBox; -class QFrame; -class QKeyEvent; -class QLabel; -class QResizeEvent; -class CardDatabaseQuerier; - -/** - * @brief A selectable preview tile for a deck that has no local file. - * - * Renders a banner card picture (looked up by name in the card database), the - * deck name, color identity and game format. Used to preview decks shared via a - * cockatrice:// link (metadata from Command_DeckShareList) and the deck - * currently open in the deck editor. - * - * Selection follows the deck-preview covenant: the tile reports its click - * immediately (no double-click interval delay), a palette(highlight) frame - * marks the selected tile, and Space/Enter toggles selection from the keyboard. - * A double click selects the tile and emits activated() so the caller can open - * just that deck. - */ -class SharedDeckPreviewWidget : public QWidget -{ - Q_OBJECT - -public: - explicit SharedDeckPreviewWidget(QWidget *parent, - const CardDatabaseQuerier *querier, - const QString &deckName, - const QString &bannerCardName, - const QString &colorIdentity, - const QString &gameFormat = QString(), - const QString &deckToolTip = QString()); - - [[nodiscard]] bool isSelected() const; - void setSelected(bool selected); - - void setBaseAccessibleName(const QString &name); - -signals: - void selectionToggled(bool selected); - void activated(); - -protected: - void resizeEvent(QResizeEvent *event) override; - void keyPressEvent(QKeyEvent *event) override; - -private slots: - void toggleSelection(); - void activate(); - -private: - void updateSelectionVisual(bool selected); - void updateSelectionFrameGeometry(); - - DeckPreviewCardPictureWidget *bannerCardDisplayWidget; - ColorIdentityWidget *colorIdentityWidget; - QLabel *gameFormatLabel; - QCheckBox *selectionCheckBox; - QFrame *selectionFrame; - QString baseAccessibleName; -}; - -#endif // SHARED_DECK_PREVIEW_WIDGET_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_login_prompt.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_login_prompt.cpp deleted file mode 100644 index 86647ef26..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_login_prompt.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include "dlg_login_prompt.h" - -#include -#include -#include -#include -#include -#include - -DlgLoginPrompt::DlgLoginPrompt(const QString &serverText, QWidget *parent) : QDialog(parent) -{ - setWindowTitle(tr("Sign in")); - - auto *mainLayout = new QVBoxLayout(this); - mainLayout->addWidget( - new QLabel(tr("This link requires you to be signed in.\nSign in to %1:").arg(serverText), this)); - - auto *formLayout = new QFormLayout; - usernameEdit = new QLineEdit(this); - passwordEdit = new QLineEdit(this); - passwordEdit->setEchoMode(QLineEdit::Password); - formLayout->addRow(tr("Username:"), usernameEdit); - formLayout->addRow(tr("Password:"), passwordEdit); - mainLayout->addLayout(formLayout); - - savePasswordCheckBox = new QCheckBox(tr("Save password for this server"), this); - mainLayout->addWidget(savePasswordCheckBox); - - auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); - connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); - connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); - mainLayout->addWidget(buttonBox); - - usernameEdit->setFocus(); -} - -QString DlgLoginPrompt::username() const -{ - return usernameEdit->text().trimmed(); -} - -QString DlgLoginPrompt::password() const -{ - return passwordEdit->text(); -} - -bool DlgLoginPrompt::savePassword() const -{ - return savePasswordCheckBox->isChecked(); -} diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_login_prompt.h b/cockatrice/src/interface/widgets/dialogs/dlg_login_prompt.h deleted file mode 100644 index e47924058..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_login_prompt.h +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @file dlg_login_prompt.h - * @ingroup ConnectionDialogs - */ -//! \todo Document this file. - -#ifndef DLG_LOGIN_PROMPT_H -#define DLG_LOGIN_PROMPT_H - -#include - -class QCheckBox; -class QLineEdit; - -/** - * @brief Small sign-in dialog used when a cockatrice:// link needs credentials - * that are not saved for the target server. - * - * The entered name and password are handed to the intent chain; when the user - * opts to save them, they are stored in the server settings so that later links - * to the same server connect seamlessly. - */ -class DlgLoginPrompt : public QDialog -{ - Q_OBJECT - -public: - explicit DlgLoginPrompt(const QString &serverText, QWidget *parent = nullptr); - - [[nodiscard]] QString username() const; - [[nodiscard]] QString password() const; - [[nodiscard]] bool savePassword() const; - -private: - QLineEdit *usernameEdit; - QLineEdit *passwordEdit; - QCheckBox *savePasswordCheckBox; -}; - -#endif // DLG_LOGIN_PROMPT_H diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp index 581920dbc..fb559fc4b 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp @@ -416,11 +416,6 @@ void DlgSettings::setTab(int index) } } -AbstractSettingsPage *DlgSettings::page(SettingsPage which) const -{ - return pages.value(static_cast(which)); -} - void DlgSettings::updateLanguage() { qApp->removeTranslator(translator); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast) diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h index 845c0b4d6..b700f7af9 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h @@ -54,7 +54,6 @@ public: explicit DlgSettings(QWidget *parent = nullptr); void setTab(int index); - AbstractSettingsPage *page(SettingsPage which) const; private slots: void onTabClicked(int index); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_share_deck.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_share_deck.cpp deleted file mode 100644 index 897d32062..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_share_deck.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "dlg_share_deck.h" - -#include "../../../client/settings/cache_settings.h" -#include "../cards/additional_info/deck_color_identity.h" -#include "../deck_share/deck_share_utils.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -DlgShareDeck::DlgShareDeck(AbstractClient *_client, const QSharedPointer &_deck, QWidget *_parent) - : QDialog(_parent), client(_client), deck(_deck), shareTimeoutTimer(new QTimer(this)) -{ - setWindowTitle(tr("Share deck")); - - auto *layout = new QVBoxLayout(this); - - nameEdit = new QLineEdit(this); - nameEdit->setText(tr("Shared deck")); - - auto *form = new QFormLayout; - form->addRow(tr("Share name:"), nameEdit); - layout->addLayout(form); - - auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); - buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Create share link")); - buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); - connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgShareDeck::actShare); - connect(buttonBox, &QDialogButtonBox::rejected, this, &DlgShareDeck::reject); - this->buttonBox = buttonBox; - layout->addWidget(buttonBox); - - shareTimeoutTimer->setSingleShot(true); - shareTimeoutTimer->setInterval( - static_cast((static_cast(SettingsCache::instance().network().getTimeOut()) + 1) * - SettingsCache::instance().network().getKeepAlive() * 1000)); - connect(shareTimeoutTimer, &QTimer::timeout, this, &DlgShareDeck::onShareTimeout); -} - -void DlgShareDeck::actShare() -{ - buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - - Command_DeckShareCreate cmd; - cmd.set_name(nameEdit->text().trimmed().toStdString()); - if (cmd.name().empty()) { - cmd.set_name(tr("Shared deck").toStdString()); - } - - DeckShareItem *item = cmd.add_items(); - item->set_deck_list(deck->writeToString_Native().toStdString()); - item->set_color_identity(getDeckColorIdentity(*deck, CardDatabaseManager::query()).toStdString()); - - PendingCommand *pend = client->prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, &DlgShareDeck::shareFinished); - client->sendCommand(pend); - shareTimeoutTimer->start(); -} - -void DlgShareDeck::shareFinished(const Response &response, const CommandContainer & /*commandContainer*/) -{ - shareTimeoutTimer->stop(); - if (response.response_code() != Response::RespOk) { - buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); - QMessageBox::critical(this, tr("Share deck"), - tr("Failed to create the share link (server response code %1).") - .arg(QString::number(static_cast(response.response_code())))); - return; - } - - const DeckShareUtils::ShareResponse share = DeckShareUtils::handleShareResponse(client, response); - - QMessageBox::information(this, tr("Share deck"), - tr("Share link created and copied to the clipboard:\n\n%1\n\n" - "The share expires on %2.") - .arg(share.link, DeckShareUtils::formatShareExpiry(share.expiry))); - accept(); -} - -void DlgShareDeck::onShareTimeout() -{ - buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); - QMessageBox::warning(this, tr("Share deck"), tr("The server did not respond in time. Try again.")); -} diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_share_deck.h b/cockatrice/src/interface/widgets/dialogs/dlg_share_deck.h deleted file mode 100644 index 162fa3677..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_share_deck.h +++ /dev/null @@ -1,46 +0,0 @@ -/** - * @file dlg_share_deck.h - * @ingroup Dialogs - */ -//! \todo Document this file. - -#ifndef DLG_SHARE_DECK_H -#define DLG_SHARE_DECK_H - -#include -#include - -class AbstractClient; -class CommandContainer; -class DeckList; -class QDialogButtonBox; -class QLineEdit; -class QTimer; -class Response; - -/** - * @brief Slim dialog to create a temporary share for the deck open in the editor. - * - * Asks for a share name, sends Command_DeckShareCreate for the single inline - * deck, and copies the resulting link to the clipboard. - */ -class DlgShareDeck : public QDialog -{ - Q_OBJECT -public: - DlgShareDeck(AbstractClient *_client, const QSharedPointer &_deck, QWidget *parent = nullptr); - -private slots: - void actShare(); - void shareFinished(const Response &response, const CommandContainer &commandContainer); - void onShareTimeout(); - -private: - AbstractClient *client; - QSharedPointer deck; - QLineEdit *nameEdit; - QDialogButtonBox *buttonBox; - QTimer *shareTimeoutTimer; -}; - -#endif // DLG_SHARE_DECK_H diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_shared_decks_preview.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_shared_decks_preview.cpp deleted file mode 100644 index cd7ecdcdd..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_shared_decks_preview.cpp +++ /dev/null @@ -1,181 +0,0 @@ -#include "dlg_shared_decks_preview.h" - -#include "../deck_share/shared_deck_preview_widget.h" -#include "../general/layout_containers/flow_widget.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -DlgSharedDecksPreview::DlgSharedDecksPreview(QWidget *parent, - const CardDatabaseQuerier *querier, - const QString &shareName, - qint64 expiresAt, - const QString &serverText, - const QList &items) - : QDialog(parent) -{ - setWindowTitle(tr("Open shared decks")); - resize(700, 500); - - auto *mainLayout = new QVBoxLayout(this); - - // shareName and serverText come from the share server, so escape them: the - // QLabels render AutoText and markup would otherwise be shown as rich text. - auto *titleLabel = - new QLabel(tr("Share: %1").arg((shareName.isEmpty() ? tr("Untitled") : shareName).toHtmlEscaped()), this); - QFont titleFont = titleLabel->font(); - titleFont.setBold(true); - titleFont.setPointSize(titleFont.pointSize() + 2); - titleLabel->setFont(titleFont); - mainLayout->addWidget(titleLabel); - - if (!serverText.isEmpty()) { - mainLayout->addWidget(new QLabel(tr("From %1").arg(serverText.toHtmlEscaped()), this)); - } - - if (expiresAt > 0) { - const QString expiryText = QDateTime::fromSecsSinceEpoch(expiresAt).toLocalTime().toString(Qt::TextDate); - mainLayout->addWidget(new QLabel(tr("This share link expires on %1").arg(expiryText), this)); - } - - downloadStatusLabel = new QLabel(this); - downloadStatusLabel->setVisible(false); - mainLayout->addWidget(downloadStatusLabel); - - flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); - mainLayout->addWidget(flowWidget, 1); - - for (const ServerInfo_DeckShareItem &item : items) { - QStringList tags; - for (const auto &tag : item.tags()) { - tags.append(QString::fromStdString(tag)); - } - - auto *tile = new SharedDeckPreviewWidget( - this, querier, QString::fromStdString(item.name()), QString::fromStdString(item.banner_card()), - QString::fromStdString(item.color_identity()), QString::fromStdString(item.game_format()), tags.join(", ")); - flowWidget->addNavigableWidget(tile); - tiles.append(tile); - itemIds.append(item.id()); - } - - if (tiles.size() == 1) { - tiles.first()->setSelected(true); - } - - auto *buttonBox = new QDialogButtonBox(this); - openSelectedButton = buttonBox->addButton(tr("Open selected"), QDialogButtonBox::AcceptRole); - openAllButton = buttonBox->addButton(tr("Open all"), QDialogButtonBox::ActionRole); - buttonBox->addButton(tr("Cancel"), QDialogButtonBox::RejectRole); - mainLayout->addWidget(buttonBox); - - connect(buttonBox, &QDialogButtonBox::rejected, this, [this]() { - onCancel(); - close(); - }); - - // Esc calls QDialog::reject() directly (which hides the dialog without a - // close event), so route it through the same guarded cancel as the button. - connect(this, &QDialog::rejected, this, [this]() { - onCancel(); - close(); - }); - - connect(openSelectedButton, &QPushButton::clicked, this, &DlgSharedDecksPreview::openSelected); - connect(buttonBox, &QDialogButtonBox::clicked, this, [this, buttonBox](QAbstractButton *button) { - if (buttonBox->buttonRole(button) == QDialogButtonBox::ActionRole) { - openAll(); - } - }); - - for (SharedDeckPreviewWidget *tile : tiles) { - connect(tile, &SharedDeckPreviewWidget::selectionToggled, this, - &DlgSharedDecksPreview::updateOpenSelectedEnabled); - } - for (int i = 0; i < tiles.size(); ++i) { - const int itemId = itemIds.at(i); - // Double-clicking a tile selects it and opens just that deck. - connect(tiles.at(i), &SharedDeckPreviewWidget::activated, this, [this, itemId]() { - resultEmitted = true; - setDownloading(true); - emit openRequested(QList{itemId}); - }); - } - updateOpenSelectedEnabled(); -} - -QList DlgSharedDecksPreview::selectedItemIds() const -{ - QList selectedIds; - for (int i = 0; i < tiles.size(); ++i) { - if (tiles.at(i)->isSelected()) { - selectedIds.append(itemIds.at(i)); - } - } - return selectedIds; -} - -void DlgSharedDecksPreview::openSelected() -{ - const QList selectedIds = selectedItemIds(); - if (selectedIds.isEmpty()) { - return; - } - resultEmitted = true; - setDownloading(true); - emit openRequested(selectedIds); -} - -void DlgSharedDecksPreview::openAll() -{ - resultEmitted = true; - setDownloading(true); - emit openRequested(itemIds); -} - -void DlgSharedDecksPreview::setDownloading(bool downloading) -{ - if (downloadInProgress == downloading) { - return; - } - downloadInProgress = downloading; - downloadStatusLabel->setVisible(downloading); - for (SharedDeckPreviewWidget *tile : tiles) { - tile->setEnabled(!downloading); - } - openSelectedButton->setEnabled(!downloading); - openAllButton->setEnabled(!downloading); -} - -void DlgSharedDecksPreview::setDownloadProgress(int done, int total, const QString ¤tDeckName) -{ - if (!downloadInProgress) { - return; - } - downloadStatusLabel->setText(tr("Downloading deck %1 of %2: %3").arg(done).arg(total).arg(currentDeckName)); -} - -void DlgSharedDecksPreview::updateOpenSelectedEnabled() -{ - openSelectedButton->setEnabled(!selectedItemIds().isEmpty()); -} - -void DlgSharedDecksPreview::onCancel() -{ - if (!resultEmitted || downloadInProgress) { - resultEmitted = true; - emit cancelled(); - } -} - -void DlgSharedDecksPreview::closeEvent(QCloseEvent *event) -{ - onCancel(); - QDialog::closeEvent(event); -} diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_shared_decks_preview.h b/cockatrice/src/interface/widgets/dialogs/dlg_shared_decks_preview.h deleted file mode 100644 index 31820e3a0..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_shared_decks_preview.h +++ /dev/null @@ -1,68 +0,0 @@ -#ifndef COCKATRICE_DLG_SHARED_DECKS_PREVIEW_H -#define COCKATRICE_DLG_SHARED_DECKS_PREVIEW_H - -#include -#include - -class FlowWidget; -class QCloseEvent; -class QLabel; -class QPushButton; -class ServerInfo_DeckShareItem; -class SharedDeckPreviewWidget; -class CardDatabaseQuerier; - -/** - * @brief Non-modal preview of the decks contained in a shared-deck link. - * - * Lets the user pick which of the shared decks to open before anything is - * downloaded. Emits openRequested with the ids of the chosen decks, or - * cancelled when the user closes the dialog without choosing. Once the user - * picks, the dialog switches into a "downloading" state: the tiles and open - * buttons are disabled, a progress label shows the current download and Cancel - * stays functional so the download can be aborted. - */ -class DlgSharedDecksPreview : public QDialog -{ - Q_OBJECT - -public: - explicit DlgSharedDecksPreview(QWidget *parent, - const CardDatabaseQuerier *querier, - const QString &shareName, - qint64 expiresAt, - const QString &serverText, - const QList &items); - - void setDownloadProgress(int done, int total, const QString ¤tDeckName); - -public slots: - void setDownloading(bool downloading); - -signals: - void openRequested(const QList &itemIds); - void cancelled(); - -protected: - void closeEvent(QCloseEvent *event) override; - -private slots: - void openSelected(); - void openAll(); - void updateOpenSelectedEnabled(); - void onCancel(); - -private: - QList selectedItemIds() const; - - FlowWidget *flowWidget; - QList tiles; - QList itemIds; - QPushButton *openSelectedButton; - QPushButton *openAllButton; - QLabel *downloadStatusLabel; - bool resultEmitted = false; - bool downloadInProgress = false; -}; - -#endif // COCKATRICE_DLG_SHARED_DECKS_PREVIEW_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/general/home_tab_button_color.h b/cockatrice/src/interface/widgets/general/home_tab_button_color.h index b45bd7a92..1550b57e7 100644 --- a/cockatrice/src/interface/widgets/general/home_tab_button_color.h +++ b/cockatrice/src/interface/widgets/general/home_tab_button_color.h @@ -11,8 +11,8 @@ namespace HomeTabButtonColor */ enum Source { - FromThemeColors, ///< Use the theme's identity accent colors - FromBackground, ///< Extract colour from the background image + Automatic, ///< Extract color from background, or use theme color if no background + FromBackground, ///< Always extract color from background }; struct Entry @@ -23,7 +23,7 @@ struct Entry inline QList all() { - static QList entries = {{FromThemeColors, QT_TR_NOOP("From theme colors")}, + static QList entries = {{Automatic, QT_TR_NOOP("Automatic")}, {FromBackground, QT_TR_NOOP("Extract from background")}}; return entries; @@ -33,12 +33,12 @@ inline QList all() * Safely converts an int into the corresponding Source. * * @param value The int value - * @return The Source. Returns Source::FromThemeColors if the value is not within range + * @return The Source. Returns Source::Automatic if the value is not within range */ inline Source intToSource(int value) { if (value > FromBackground) { - return FromThemeColors; // default + return Automatic; // default } return static_cast(value); diff --git a/cockatrice/src/interface/widgets/general/home_widget.cpp b/cockatrice/src/interface/widgets/general/home_widget.cpp index 648d315f9..0d030b973 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.cpp +++ b/cockatrice/src/interface/widgets/general/home_widget.cpp @@ -11,8 +11,6 @@ #include "home_tab_button_color.h" #include -#include -#include #include #include #include @@ -23,7 +21,8 @@ #include HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) - : QWidget(parent), tabSupervisor(_tabSupervisor), background(themePixmap(QStringLiteral("backgrounds/home"))) + : QWidget(parent), tabSupervisor(_tabSupervisor), background(themePixmap(QStringLiteral("backgrounds/home"))), + overlay(themePixmap(QStringLiteral("cockatrice"))) { layout = new QGridLayout(this); @@ -55,8 +54,6 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) // Lambda is cleaner to read than overloading this connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabDisplayCardNameChanged, this, [this] { repaint(); }); - connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundDimChanged, this, - [this] { repaint(); }); connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, &HomeWidget::initializeBackgroundFromSource); connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this, @@ -64,18 +61,12 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) // Scheme flips (light/dark/system with an OS switch) fire on themeManager, // not on SettingsCache::themeChanged, so re-resolve the variant background. connect(themeManager, &ThemeManager::themeChanged, this, &HomeWidget::initializeBackgroundFromSource); - connect(themeManager, &ThemeManager::paletteChanged, this, &HomeWidget::updateButtonsToBackgroundColor); - connect(themeManager, &ThemeManager::paletteChanged, this, &HomeWidget::updateLogoOverlay); connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabButtonColorChanged, this, &HomeWidget::updateButtonsToBackgroundColor); } void HomeWidget::initializeBackgroundFromSource() { - // The featured logo is theme/scheme-derived too; reload it alongside the - // background so a theme or appearance switch doesn't leave it stale. - updateLogoOverlay(); - if (CardDatabaseManager::getInstance()->getLoadStatus() != LoadStatus::Ok) { connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this, &HomeWidget::initializeBackgroundFromSource); @@ -114,24 +105,32 @@ void HomeWidget::loadBackgroundSourceDeck() backgroundSourceDeck = deckOpt.has_value() ? deckOpt.value().deckList : DeckList(); } -static QPair paletteDerivedButtonColors() +static bool isDefaultBackgroundAndTheme() { - return {themeManager->appColor(AppColor::AccentStrong), themeManager->appColor(AppColor::AccentSoft)}; + QString sourceId = SettingsCache::instance().appearance().getHomeTabBackgroundSource(); + return themeManager->isBuiltInTheme() && BackgroundSources::fromId(sourceId) == BackgroundSources::Theme; } QPair HomeWidget::determineButtonColor() const { + static QPair defaultColor = {QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80)}; + auto colorSource = HomeTabButtonColor::intToSource(SettingsCache::instance().appearance().getHomeTabButtonColorSourceIndex()); switch (colorSource) { - case HomeTabButtonColor::FromThemeColors: - return paletteDerivedButtonColors(); + case HomeTabButtonColor::Automatic: { + if (isDefaultBackgroundAndTheme()) { + return defaultColor; + } else { + return extractDominantColors(background); + } + } case HomeTabButtonColor::FromBackground: return extractDominantColors(background); } - return paletteDerivedButtonColors(); + return defaultColor; } void HomeWidget::setRandomCard(ExactCard &newCard) @@ -235,10 +234,10 @@ QGroupBox *HomeWidget::createButtons() QVBoxLayout *boxLayout = new QVBoxLayout; boxLayout->setAlignment(Qt::AlignHCenter); - logoLabel = new QLabel; + QLabel *logoLabel = new QLabel; + logoLabel->setPixmap(overlay.scaledToWidth(200, Qt::SmoothTransformation)); logoLabel->setAlignment(Qt::AlignCenter); boxLayout->addWidget(logoLabel); - updateLogoOverlay(); boxLayout->addSpacing(25); connectButton = new HomeStyledButton("Connect/Play", gradientColors); @@ -366,15 +365,13 @@ void HomeWidget::paintEvent(QPaintEvent *event) painter.drawPixmap(topLeft, toDraw); } - if (SettingsCache::instance().appearance().getHomeTabBackgroundDim()) { - // Draw translucent black overlay with rounded corners - QRectF overlayRect(5, 5, width() - 10, height() - 10); - QPainterPath roundedRectPath; - roundedRectPath.addRoundedRect(overlayRect, 20, 20); + // Draw translucent black overlay with rounded corners + QRectF overlayRect(5, 5, width() - 10, height() - 10); + QPainterPath roundedRectPath; + roundedRectPath.addRoundedRect(overlayRect, 20, 20); - QColor semiTransparentBlack(0, 0, 0, static_cast(255 * 0.33)); - painter.fillPath(roundedRectPath, semiTransparentBlack); - } + QColor semiTransparentBlack(0, 0, 0, static_cast(255 * 0.33)); + painter.fillPath(roundedRectPath, semiTransparentBlack); // Card name overlay (above the attribution, bottom-right) QString cardName; @@ -439,56 +436,3 @@ void HomeWidget::paintEvent(QPaintEvent *event) QWidget::paintEvent(event); } - -void HomeWidget::updateLogoOverlay() -{ - // Emulate cockatrice.svg in Qt rather than rendering the baked-in SVG. - // The SVG has no separate plate: the gradient fills the bird's silhouette - // paths (light #c9fd62/AccentSoft at the top-left, dark #139740/AccentStrong - // toward the bottom-right — the SVG's linearGradient4265-7-8 stops along - // its userSpaceOnUse axis), and the white highlight path - // (cockatrice-logo-white) sits on top. So we paint that gradient clipped to - // the full logo silhouette (the full-color logo's alpha), then overlay the - // white mark. Colours stay fully theme-driven and independent of the static - // greens baked into the SVG. - const QColor strong = themeManager->appColor(AppColor::AccentStrong); - const QColor soft = themeManager->appColor(AppColor::AccentSoft); - - const QPixmap silhouette = themePixmap(QStringLiteral("cockatrice")).scaledToWidth(200, Qt::SmoothTransformation); - const QPixmap whiteMark = - themePixmap(QStringLiteral("cockatrice-logo-white")).scaledToWidth(200, Qt::SmoothTransformation); - if (silhouette.isNull() || whiteMark.isNull()) { - return; - } - - QPixmap composite(silhouette.size()); - composite.fill(Qt::transparent); - - { - QPainter painter(&composite); - painter.setRenderHint(QPainter::Antialiasing); - painter.setRenderHint(QPainter::SmoothPixmapTransform); - - // Recreate cockatrice.svg's own gradient geometry (linearGradient - // 4265-7-8, userSpaceOnUse): light AccentSoft at S=(-8.097,-97.746), - // dark AccentStrong at E=(162.455,295.208), on the SVG's 300x300 - // canvas. Scale those coordinates to this composite's size. - const qreal scale = composite.width() / 300.0; - QLinearGradient gradient(QPointF(-8.097, -97.746) * scale, QPointF(162.455, 295.208) * scale); - gradient.setColorAt(0.0, soft); - gradient.setColorAt(1.0, strong); - painter.fillRect(composite.rect(), gradient); - - // Clip the gradient to the full logo silhouette exactly as the SVG's - // gradient paths are confined to the bird. - painter.setCompositionMode(QPainter::CompositionMode_DestinationIn); - painter.drawPixmap(0, 0, silhouette); - - painter.setCompositionMode(QPainter::CompositionMode_SourceOver); - painter.drawPixmap(0, 0, whiteMark); - } - - if (logoLabel) { - logoLabel->setPixmap(composite); - } -} diff --git a/cockatrice/src/interface/widgets/general/home_widget.h b/cockatrice/src/interface/widgets/general/home_widget.h index 1cadc4a67..9df0d7b6a 100644 --- a/cockatrice/src/interface/widgets/general/home_widget.h +++ b/cockatrice/src/interface/widgets/general/home_widget.h @@ -15,9 +15,6 @@ #include #include -class QGridLayout; -class QLabel; - class HomeWidget : public QWidget { @@ -44,14 +41,13 @@ private: QPixmap background; CardInfoPictureArtCropWidget *backgroundSourceCard = nullptr; DeckList backgroundSourceDeck; - QLabel *logoLabel = nullptr; + QPixmap overlay; QPair gradientColors; HomeStyledButton *connectButton; void setRandomCard(ExactCard &newCard); void loadBackgroundSourceDeck(); QPair determineButtonColor() const; - void updateLogoOverlay(); }; #endif // HOME_WIDGET_H diff --git a/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.cpp b/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.cpp index 01c9ac34e..025f457bd 100644 --- a/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.cpp +++ b/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.cpp @@ -7,7 +7,6 @@ #include "flow_widget.h" #include -#include #include #include #include @@ -81,35 +80,13 @@ FlowWidget::FlowWidget(QWidget *parent, /** * @brief Adds a widget to the flow layout within the FlowWidget. * - * Plain widgets are not filtered for arrow keys: intercepting them would steal - * Up/Down/Left/Right from controls that use them (combo boxes, spin boxes - * etc.). Widgets that want keyboard navigation between flow items must be - * added via addNavigableWidget instead. - * * @param widget_to_add The widget to add to the flow layout. */ -void FlowWidget::addWidget(QWidget *widget_to_add) +void FlowWidget::addWidget(QWidget *widget_to_add) const { flowLayout->addWidget(widget_to_add); } -/** - * @brief Adds a widget and routes its arrow keys to FlowWidget focus navigation. - * - * The widget is filtered for arrow-key events so keyboard navigation between - * the flow items keeps working even when the flow sits inside a QScrollArea, - * which swallows arrow keys before they can reach FlowWidget::keyPressEvent. - * Only widgets added through this method are affected; anything that needs its - * own arrow keys should use plain addWidget. - * - * @param widget_to_add The widget to add to the flow layout. - */ -void FlowWidget::addNavigableWidget(QWidget *widget_to_add) -{ - widget_to_add->installEventFilter(this); - flowLayout->addWidget(widget_to_add); -} - void FlowWidget::insertWidgetAtIndex(QWidget *toInsert, int index) { flowLayout->insertWidgetAtIndex(toInsert, index); @@ -200,66 +177,6 @@ QLayoutItem *FlowWidget::itemAt(int index) const return flowLayout->itemAt(index); } -void FlowWidget::keyPressEvent(QKeyEvent *event) -{ - if (moveFocus(event)) { - event->accept(); - return; - } - QWidget::keyPressEvent(event); -} - -bool FlowWidget::eventFilter(QObject *watched, QEvent *event) -{ - if (event->type() == QEvent::KeyPress && moveFocus(static_cast(event))) { - return true; - } - return QWidget::eventFilter(watched, event); -} - -bool FlowWidget::moveFocus(QKeyEvent *event) -{ - // Keyboard navigation between the flow items: arrow keys move focus just - // like clicking the sibling tiles would. Only items that can take keyboard - // focus (e.g. the deck-preview tiles in shared-deck links) are visited. - const bool moveForward = event->key() == Qt::Key_Right || event->key() == Qt::Key_Down; - const bool moveBackward = event->key() == Qt::Key_Left || event->key() == Qt::Key_Up; - if (!moveForward && !moveBackward) { - return false; - } - - QList focusableItems; - for (int i = 0; i < flowLayout->count(); ++i) { - QWidget *item = flowLayout->itemAt(i)->widget(); - if (item != nullptr && (item->focusPolicy() & Qt::TabFocus)) { - focusableItems.append(item); - } - } - - if (focusableItems.isEmpty()) { - return false; - } - - int currentIndex = -1; - for (int i = 0; i < focusableItems.size(); ++i) { - if (focusableItems.at(i)->hasFocus()) { - currentIndex = i; - break; - } - } - - const int delta = moveForward ? 1 : -1; - int nextIndex; - if (currentIndex < 0) { - nextIndex = moveForward ? 0 : focusableItems.size() - 1; - } else { - nextIndex = (currentIndex + delta + focusableItems.size()) % focusableItems.size(); - } - focusableItems.value(nextIndex)->setFocus(); - event->accept(); - return true; -} - int FlowWidget::count() const { return flowLayout->count(); diff --git a/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.h b/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.h index 4d52db3f1..a232336d8 100644 --- a/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.h +++ b/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.h @@ -11,7 +11,6 @@ #include "../../../layouts/flow_layout.h" #include -#include #include #include #include @@ -29,8 +28,7 @@ public: Qt::ScrollBarPolicy horizontalPolicy, Qt::ScrollBarPolicy verticalPolicy); - void addWidget(QWidget *widget_to_add); - void addNavigableWidget(QWidget *widget_to_add); + void addWidget(QWidget *widget_to_add) const; void insertWidgetAtIndex(QWidget *toInsert, int index); void removeWidget(QWidget *widgetToRemove) const; void clearLayout(); @@ -45,15 +43,9 @@ public slots: void setSpacing(int hSpacing, int vSpacing); protected: - bool eventFilter(QObject *watched, QEvent *event) override; void resizeEvent(QResizeEvent *event) override; - void keyPressEvent(QKeyEvent *event) override; private: - /// @brief Moves keyboard focus to an adjacent flow item for an arrow-key event. - /// @return True when the event was an arrow key and was handled. - bool moveFocus(QKeyEvent *event); - Qt::Orientation flowDirection; QHBoxLayout *mainLayout; FlowLayout *flowLayout; diff --git a/cockatrice/src/interface/widgets/menus/deck_editor_menu.cpp b/cockatrice/src/interface/widgets/menus/deck_editor_menu.cpp index 6e27b4e42..f057680be 100644 --- a/cockatrice/src/interface/widgets/menus/deck_editor_menu.cpp +++ b/cockatrice/src/interface/widgets/menus/deck_editor_menu.cpp @@ -28,9 +28,6 @@ DeckEditorMenu::DeckEditorMenu(AbstractTabDeckEditor *parent) : QMenu(parent), d aSaveDeckAs = new QAction(QString(), this); connect(aSaveDeckAs, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actSaveDeckAs); - aShareDeck = new QAction(QString(), this); - connect(aShareDeck, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actShareDeck); - aLoadDeckFromClipboard = new QAction(QString(), this); connect(aLoadDeckFromClipboard, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actLoadDeckFromClipboard); @@ -99,7 +96,6 @@ DeckEditorMenu::DeckEditorMenu(AbstractTabDeckEditor *parent) : QMenu(parent), d addMenu(loadRecentDeckMenu); addAction(aSaveDeck); addAction(aSaveDeckAs); - addAction(aShareDeck); addSeparator(); addAction(aLoadDeckFromClipboard); addMenu(editDeckInClipboardMenu); @@ -124,7 +120,6 @@ void DeckEditorMenu::setSaveStatus(bool newStatus) { aSaveDeck->setEnabled(newStatus); aSaveDeckAs->setEnabled(newStatus); - aShareDeck->setEnabled(newStatus); aSaveDeckToClipboard->setEnabled(newStatus); aSaveDeckToClipboardNoSetInfo->setEnabled(newStatus); aSaveDeckToClipboardRaw->setEnabled(newStatus); @@ -162,7 +157,6 @@ void DeckEditorMenu::retranslateUi() aClearRecents->setText(tr("Clear")); aSaveDeck->setText(tr("&Save deck")); aSaveDeckAs->setText(tr("Save deck &as...")); - aShareDeck->setText(tr("Share deck...")); aLoadDeckFromClipboard->setText(tr("Load deck from cl&ipboard...")); diff --git a/cockatrice/src/interface/widgets/menus/deck_editor_menu.h b/cockatrice/src/interface/widgets/menus/deck_editor_menu.h index 08ce7bba1..eff9257bb 100644 --- a/cockatrice/src/interface/widgets/menus/deck_editor_menu.h +++ b/cockatrice/src/interface/widgets/menus/deck_editor_menu.h @@ -21,8 +21,7 @@ public: QAction *aNewDeck, *aLoadDeck, *aClearRecents, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard, *aEditDeckInClipboard, *aEditDeckInClipboardRaw, *aSaveDeckToClipboard, *aSaveDeckToClipboardNoSetInfo, *aSaveDeckToClipboardRaw, *aSaveDeckToClipboardRawNoSetInfo, *aPrintDeck, *aLoadDeckFromWebsite, - *aExportDeckDecklist, *aExportDeckDecklistXyz, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout, *aShareDeck, - *aClose; + *aExportDeckDecklist, *aExportDeckDecklistXyz, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout, *aClose; QMenu *loadRecentDeckMenu, *analyzeDeckMenu, *editDeckInClipboardMenu, *saveDeckToClipboardMenu; void setSaveStatus(bool newStatus); diff --git a/cockatrice/src/interface/widgets/onboarding/banner_shader_config.h b/cockatrice/src/interface/widgets/onboarding/banner_shader_config.h index 6008044ff..32f3e89c0 100644 --- a/cockatrice/src/interface/widgets/onboarding/banner_shader_config.h +++ b/cockatrice/src/interface/widgets/onboarding/banner_shader_config.h @@ -38,10 +38,6 @@ class BannerShaderConfig : public QObject Q_PROPERTY(QColor colorA READ colorA WRITE setColorA NOTIFY colorAChanged) Q_PROPERTY(QColor colorB READ colorB WRITE setColorB NOTIFY colorBChanged) Q_PROPERTY(QColor accent READ accent WRITE setAccent NOTIFY accentChanged) - Q_PROPERTY(QColor glowColor READ glowColor WRITE setGlowColor NOTIFY glowColorChanged) - Q_PROPERTY(QColor brandStrong READ brandStrong WRITE setBrandStrong NOTIFY brandStrongChanged) - Q_PROPERTY(QColor brandSoft READ brandSoft WRITE setBrandSoft NOTIFY brandSoftChanged) - Q_PROPERTY(qreal vignetteMin READ vignetteMin WRITE setVignetteMin NOTIFY vignetteMinChanged) Q_PROPERTY(bool logoVisible READ logoVisible WRITE setLogoVisible NOTIFY logoVisibleChanged) Q_PROPERTY(qreal logoGlow READ logoGlow WRITE setLogoGlow NOTIFY logoGlowChanged) @@ -189,54 +185,6 @@ public: } } - QColor glowColor() const - { - return m_glowColor; - } - void setGlowColor(const QColor &c) - { - if (c != m_glowColor) { - m_glowColor = c; - emit glowColorChanged(); - } - } - - QColor brandStrong() const - { - return m_brandStrong; - } - void setBrandStrong(const QColor &c) - { - if (c != m_brandStrong) { - m_brandStrong = c; - emit brandStrongChanged(); - } - } - - QColor brandSoft() const - { - return m_brandSoft; - } - void setBrandSoft(const QColor &c) - { - if (c != m_brandSoft) { - m_brandSoft = c; - emit brandSoftChanged(); - } - } - - qreal vignetteMin() const - { - return m_vignetteMin; - } - void setVignetteMin(qreal v) - { - if (v != m_vignetteMin) { - m_vignetteMin = v; - emit vignetteMinChanged(); - } - } - bool logoVisible() const { return m_logoVisible; @@ -274,10 +222,6 @@ signals: void colorAChanged(); void colorBChanged(); void accentChanged(); - void glowColorChanged(); - void brandStrongChanged(); - void brandSoftChanged(); - void vignetteMinChanged(); void logoVisibleChanged(); void logoGlowChanged(); @@ -295,16 +239,9 @@ private: bool m_frontIsA = true; - // Curated fallback seed values -- BannerHost overwrites these with - // palette-derived colours (see shader_banner_widget.cpp) before the first - // paint, so they only matter as a safe pre-first-apply default. QColor m_colorA{0x1A, 0x1A, 0x20}; QColor m_colorB{0x0E, 0x0E, 0x12}; QColor m_accent{0x8B, 0xDD, 0x6B}; - QColor m_glowColor{Qt::white}; - QColor m_brandStrong{0x13, 0x97, 0x40}; - QColor m_brandSoft{0xC9, 0xFD, 0x62}; - qreal m_vignetteMin = 0.62; bool m_logoVisible = false; qreal m_logoGlow = 1.0; diff --git a/cockatrice/src/interface/widgets/onboarding/brand_colors.h b/cockatrice/src/interface/widgets/onboarding/brand_colors.h deleted file mode 100644 index bf173270b..000000000 --- a/cockatrice/src/interface/widgets/onboarding/brand_colors.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef BRAND_COLORS_H -#define BRAND_COLORS_H - -#include - -/** @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 \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.cpp b/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.cpp index 797fe4425..3293b19ac 100644 --- a/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.cpp +++ b/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.cpp @@ -14,31 +14,10 @@ #include #include #include -#include #include #include #include -namespace -{ -/** @brief A theme's shipped identity accent, immune to any user- or auto- - * generated palette that may currently be masking appColor(). */ -QColor themeIdentityAccent(const QString &themeDirPath, const QString &themeName) -{ - for (const QString &scheme : {QStringLiteral("Light"), QStringLiteral("Dark")}) { - const PaletteConfig cfg = ThemeManager::loadDefaultPaletteConfig(themeDirPath, themeName, scheme); - if (cfg.appColors.contains(AppColor::AccentStrong)) { - return cfg.appColors.value(AppColor::AccentStrong); - } - const QColor highlight = cfg.colors.value(QPalette::Active).value(QPalette::Highlight); - if (highlight.isValid()) { - return highlight; - } - } - return {}; -} -} // namespace - ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent) { themeCombo = new QComboBox(this); @@ -51,15 +30,6 @@ ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent) quickSetupPanel = new QuickSetupPanel(this); - // Seed the picker from the current theme's own identity accent rather than - // a hardcoded brand green: Plasma seeds violet, Fusion green, etc., and it - // is immune to stale generated palettes that may mask appColor(). This was - // initially a brand-green workaround from before Fusion became the default. - // setAccentColor blocks signals, so this never triggers a generation. - lastSeededTheme = SettingsCache::instance().getThemeName(); - quickSetupPanel->setAccentColor( - themeIdentityAccent(themeManager->getAvailableThemes().value(lastSeededTheme), lastSeededTheme)); - connect(themeCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onThemeChanged); connect(schemeCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeSetupPage::onSchemeChanged); connect(quickSetupPanel, &QuickSetupPanel::valueChanged, this, &ThemeSetupPage::onGenerateFromAccent); @@ -77,8 +47,7 @@ ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent) // Mirrors AppearanceSettingsPage's identical listener for the combo-sync // half of this. connect(themeManager, &ThemeManager::themeChanged, this, [this] { - const QString newTheme = SettingsCache::instance().getThemeName(); - const QString newDir = themeManager->getAvailableThemes().value(newTheme); + const QString newDir = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName()); const ThemeConfig cfg = ThemeConfig::fromThemeDir(newDir); const QString current = cfg.colorScheme; @@ -87,14 +56,6 @@ ThemeSetupPage::ThemeSetupPage(QWidget *parent) : FirstRunWizardPage(parent) schemeCombo->setCurrentIndex(idx >= 0 ? idx : 0); schemeCombo->blockSignals(false); - // Keep the picker's accent in step with the theme's own identity; the - // swatch seeded at construction would otherwise stay stale (e.g. green - // from a previous theme) when the user toggles themes. - if (newTheme != lastSeededTheme) { - lastSeededTheme = newTheme; - quickSetupPanel->setAccentColor(themeIdentityAccent(newDir, newTheme)); - } - maybeAutoGeneratePalette(); }); @@ -197,14 +158,8 @@ void ThemeSetupPage::maybeAutoGeneratePalette() const QString dirPath = themeManager->getAvailableThemes().value(SettingsCache::instance().getThemeName()); const QString scheme = resolvedScheme(); - // The theme dir may resolve to the user profile even for built-in themes - // (getAvailableThemes gives the user copy precedence), so consult the - // shipped palette too -- both via loadDefaultPaletteConfig's system fallback. - // Without it, scheme flips regenerate a fresh palette from the picker accent - // and clobber the curated colours the theme explicitly ships. if (PaletteConfig::fromScheme(dirPath, scheme).hasPalette() || - ThemeManager::loadDefaultPaletteConfig(dirPath, SettingsCache::instance().getThemeName(), scheme) - .hasPalette()) { + PaletteConfig::fromDefault(dirPath, scheme).hasPalette()) { return; // theme already has something real to show -- leave it alone } diff --git a/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.h b/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.h index 16a3c9a5d..d1f84c1b9 100644 --- a/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.h +++ b/cockatrice/src/interface/widgets/onboarding/pages/theme_setup_page.h @@ -53,9 +53,6 @@ private: QComboBox *homeTabBackgroundCombo; bool paletteDirty = false; - - /// Theme whose identity accent currently seeds the picker. - QString lastSeededTheme; }; #endif // THEME_SETUP_PAGE_H diff --git a/cockatrice/src/interface/widgets/onboarding/qml/BrandBanner.qml b/cockatrice/src/interface/widgets/onboarding/qml/BrandBanner.qml index d94e15280..f1a385cad 100644 --- a/cockatrice/src/interface/widgets/onboarding/qml/BrandBanner.qml +++ b/cockatrice/src/interface/widgets/onboarding/qml/BrandBanner.qml @@ -16,8 +16,6 @@ Item { property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0) property vector4d uColorB: Qt.vector4d(bannerConfig.colorB.r, bannerConfig.colorB.g, bannerConfig.colorB.b, 1.0) property vector4d uAccent: Qt.vector4d(bannerConfig.accent.r, bannerConfig.accent.g, bannerConfig.accent.b, 1.0) - property vector4d uGlowColor: Qt.vector4d(bannerConfig.glowColor.r, bannerConfig.glowColor.g, bannerConfig.glowColor.b, 1.0) - property real uVignetteMin: bannerConfig.vignetteMin property real uLogoGlow: bannerConfig.logoGlow fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb" } @@ -35,62 +33,30 @@ Item { property vector4d uColorA: Qt.vector4d(bannerConfig.colorA.r, bannerConfig.colorA.g, bannerConfig.colorA.b, 1.0) property vector4d uColorB: Qt.vector4d(bannerConfig.colorB.r, bannerConfig.colorB.g, bannerConfig.colorB.b, 1.0) property vector4d uAccent: Qt.vector4d(bannerConfig.accent.r, bannerConfig.accent.g, bannerConfig.accent.b, 1.0) - property vector4d uGlowColor: Qt.vector4d(bannerConfig.glowColor.r, bannerConfig.glowColor.g, bannerConfig.glowColor.b, 1.0) - property real uVignetteMin: bannerConfig.vignetteMin property real uLogoGlow: bannerConfig.logoGlow fragmentShader: "qrc:/onboarding/shaders/brand_banner.frag.qsb" } - // The white logo sits at full opacity on top of the static gradient plate — - // no glow, no breathing. The plate matches home_widget's QPainter composite. - Item { - id: logoHost - visible: bannerConfig.logoVisible + // The hero logo itself — breathes cleanly over a 0.5–1.0 opacity range + Image { + id: logo anchors.centerIn: parent + visible: bannerConfig.logoVisible + source: "qrc:/resources/cockatrice-logo-white.svg" width: root.height * 0.6 - height: width + height: width * (sourceSize.height > 0 ? sourceSize.height / Math.max(sourceSize.width, 1) : 1) + fillMode: Image.PreserveAspectFit + smooth: true + opacity: 0.5 + 0.5 * bannerConfig.logoGlow + sourceSize: Qt.size(256, 256) - // The full-color logo renders beneath the white mark and is consumed as - // a texture (layer.enabled) by the plate shader's silhouette mask, so - // the gradient is clipped to the bird exactly as the SVG's gradient - // paths are. It is never drawn to the screen itself. - Image { - id: silhouetteMask - anchors.fill: parent - source: "qrc:/resources/cockatrice.svg" - sourceSize: Qt.size(256, 256) - fillMode: Image.PreserveAspectFit - smooth: true - visible: false - layer.enabled: true - layer.smooth: true - } + Behavior on opacity { NumberAnimation { duration: 300; easing.type: Easing.InOutSine } } - // The logo's gradient plate, drawn behind the mark: a linear - // AccentSoft (light) -> AccentStrong (dark) sheet along the same - // top-left -> bottom-right userSpaceOnUse axis the baked-in SVG used, - // clipped to the bird silhouette via uSilhouette. The white highlight - // path above is theme independent. Sized to the logo itself — no - // rounded badge, matching home_widget's QPainter composite. Static. - // Small ShaderEffect, Qt 6.4-safe. - ShaderEffect { - id: brandPlate - anchors.fill: parent - property vector4d uStrong: Qt.vector4d(bannerConfig.brandStrong.r, bannerConfig.brandStrong.g, - bannerConfig.brandStrong.b, 1.0) - property vector4d uSoft: Qt.vector4d(bannerConfig.brandSoft.r, bannerConfig.brandSoft.g, - bannerConfig.brandSoft.b, 1.0) - property var uSilhouette: silhouetteMask - fragmentShader: "qrc:/onboarding/shaders/brand_plate.frag.qsb" - } - - Image { - id: logoImage - anchors.fill: parent - source: "qrc:/resources/cockatrice-logo-white.svg" - sourceSize: Qt.size(256, 256) - fillMode: Image.PreserveAspectFit - smooth: true + transform: Scale { + origin.x: logo.width / 2 + origin.y: logo.height / 2 + xScale: 0.94 + 0.06 * bannerConfig.logoGlow + yScale: 0.94 + 0.06 * bannerConfig.logoGlow } } -} \ No newline at end of file +} diff --git a/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp b/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp index 0b9f65783..fd1fb2a98 100644 --- a/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp +++ b/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.cpp @@ -1,10 +1,7 @@ #include "shader_banner_widget.h" -#include "../../theme_manager.h" #include "banner_shader_config.h" -#include "brand_colors.h" -#include #include #include #include @@ -14,98 +11,11 @@ namespace { -// Curated near-black stage -- used only when the active palette resolves no -// usable window colour. Matches the banner's original design (dark and quiet -// so the accent stands out) and satisfies design-plans §2.1's "identity -// survives a bare palette". -constexpr QRgb kFallbackColorA = 0x1A1A20; -constexpr QRgb kFallbackColorB = 0x0E0E12; - -struct SuggestedColors -{ - QColor colorA; - QColor colorB; - QColor accent; - QColor glowColor; - QColor brandStrong; - QColor brandSoft; - qreal vignetteMin = 0.62; -}; - -SuggestedColors suggestedBannerColors() -{ - const QPalette &pal = qApp->palette(); - const QColor window = pal.color(QPalette::Active, QPalette::Window); - // Identity accent: the theme's [AppColors] AccentStrong, which appColor() - // resolves to QPalette::Highlight when a theme doesn't pin AccentStrong. - // Reading bare Highlight ignored curated accent tokens (Plasma's violet - // vs Default's green) whenever a palette didn't set the role itself. - const QColor accentStrong = themeManager->appColor(AppColor::AccentStrong); - if (!window.isValid() || !accentStrong.isValid()) { - return {QColor(kFallbackColorA), - QColor(kFallbackColorB), - kCockatriceBrandGreen, - QColor(Qt::white), - kCockatriceBrandGreen, - QColor(0xC9, 0xFD, 0x62), - 0.62}; - } - - // The theme's brand pair: AccentStrong is the deep green, AccentSoft the - // lime. These two appColors form the logo's "surrounding gradient" (deep - // core grading out to the soft, brand-toned glow) on both the banner and - // the home screen. - const QColor brandStrong = accentStrong; - const QColor brandSoft = themeManager->appColor(AppColor::AccentSoft); - - // Dress the stage for the scheme so the banner never fights the - // surrounding window in either mode. Dark palettes keep the original - // quiet near-black stage (lightness 29 → 16) with the theme's window - // hue; light palettes get a pastel "frosted accent" treatment built from - // the accent hue instead of a plain near-white copy: a coloured wash that - // clearly belongs to the theme. - const qreal luma = 0.299 * window.red() + 0.587 * window.green() + 0.114 * window.blue(); - const bool lightStage = luma > 115.0; - if (lightStage) { - const int hue = accentStrong.hslHue(); - // Achromatic accents (grey) get a neutral near-white stage instead. - const int stageSat = hue < 0 ? 0 : 64; - const int hueSafe = hue < 0 ? 0 : hue; - // Depth is what stops a light stage reading as a washed-out near-white - // copy of the page behind the banner: deepen the lower pastel band and - // raise saturation so the hue is clearly present while staying frosted. - auto pastel = [hueSafe, stageSat](int lightness) { return QColor::fromHsl(hueSafe, stageSat, lightness); }; - auto pastelLower = [hueSafe](int lightness) { return QColor::fromHsl(hueSafe, 76, lightness); }; - // Brightness-lifted accent for additive glows: the raw accent on a - // light stage must be mid-bright to read instead of washing out, so - // lift lightness and saturation together. - const int accentLightness = qBound(158, accentStrong.lightness() + 82, 198); - const int accentSaturation = hue < 0 ? 0 : qMax(accentStrong.hslSaturation(), 180); - const QColor liftedAccent = - hue < 0 ? accentStrong : QColor::fromHsl(hueSafe, accentSaturation, accentLightness); - // The centre glow (and logo tint in QML) uses the deep accent itself: - // a coloured halo/fill behind the logo instead of a white or black one. - return {pastel(214), pastelLower(186), liftedAccent, accentStrong, brandStrong, brandSoft, 0.80}; - } - - // Dark stage: force the window hue down to the banner's curated darkness, - // scaling saturation away so chromatic palettes tint it without going - // muddy. The accent is the bright, brand-driven tone (hue from the accent - // itself, never the -- often grey -- window), and it drives both the - // embers/fog and the logo glow so the mark tints like the light stage. - auto stage = [&window](int lightness) { - const int hue = window.hslHue(); - const int saturation = hue < 0 ? 0 : qBound(0, qRound(window.hslSaturation() * (lightness / 40.0)), 255); - return QColor::fromHsl(hue, saturation, lightness); - }; - const int accentHue = accentStrong.hslHue(); - const int accentHueSafe = accentHue < 0 ? 0 : accentHue; - const int accentLightness = qBound(150, accentStrong.lightness() + 70, 185); - const int accentSaturation = accentHue < 0 ? 0 : qMax(accentStrong.hslSaturation(), 160); - const QColor accent = - accentHue < 0 ? accentStrong : QColor::fromHsl(accentHueSafe, accentSaturation, accentLightness); - return {stage(29), stage(16), accent, accent, brandStrong, brandSoft, 0.62}; -} +// Near-black base palette -- the background is dark and quiet so the green +// accent stands out. +constexpr QRgb kColorA = 0x1A1A20; +constexpr QRgb kColorB = 0x0E0E12; +constexpr QRgb kAccent = 0x8BDD6B; } // namespace class GradientFallbackWidget : public QWidget @@ -113,27 +23,15 @@ class GradientFallbackWidget : public QWidget public: using QWidget::QWidget; - void setColors(const QColor &a, const QColor &b) - { - if (a != colorA || b != colorB) { - colorA = a; - colorB = b; - } - } - protected: void paintEvent(QPaintEvent *) override { QPainter painter(this); QLinearGradient gradient(0, 0, width(), height()); - gradient.setColorAt(0.0, colorA); - gradient.setColorAt(1.0, colorB); + gradient.setColorAt(0.0, QColor(kColorA)); + gradient.setColorAt(1.0, QColor(kColorB)); painter.fillRect(rect(), gradient); } - -private: - QColor colorA{QColor(kFallbackColorA)}; - QColor colorB{QColor(kFallbackColorB)}; }; BannerHost::BannerHost(QWidget *parent) : QWidget(parent) @@ -164,9 +62,6 @@ BannerHost::BannerHost(QWidget *parent) : QWidget(parent) connect(&clock, &QTimer::timeout, this, &BannerHost::tick); clock.setInterval(16); // ~60fps; the shader itself is cheap, this is just a wall clock - connect(themeManager, &ThemeManager::themeChanged, this, &BannerHost::applyThemeColors); - applyThemeColors(); - applyMotifPreset(currentMotif); updateAspect(); } @@ -228,9 +123,9 @@ void BannerHost::applyMotifPreset(Motif motif) const Preset p = presetFor(motif); - config->setColorA(bannerColorA); - config->setColorB(bannerColorB); - config->setAccent(bannerAccent); + config->setColorA(QColor(kColorA)); + config->setColorB(QColor(kColorB)); + config->setAccent(QColor(kAccent)); config->setLogoVisible(motif == Motif::Welcome); if (isFirstApply) { @@ -288,34 +183,8 @@ void BannerHost::hideEvent(QHideEvent *event) clock.stop(); } -void BannerHost::applyThemeColors() -{ - const SuggestedColors colors = suggestedBannerColors(); - bannerColorA = colors.colorA; - bannerColorB = colors.colorB; - bannerAccent = colors.accent; - - if (usingFallback) { - fallback->setColors(bannerColorA, bannerColorB); - fallback->update(); - } else if (config) { - config->setColorA(bannerColorA); - config->setColorB(bannerColorB); - config->setAccent(bannerAccent); - config->setGlowColor(colors.glowColor); - config->setBrandStrong(colors.brandStrong); - config->setBrandSoft(colors.brandSoft); - config->setVignetteMin(colors.vignetteMin); - } -} - void BannerHost::tick() { - // Palette previews (e.g. accent drags in the wizard's QuickSetupPanel) - // apply qApp->palette() without firing themeChanged, so re-derive here; - // BannerShaderConfig's setters are equality-guarded, so this is a no-op - // unless the colours actually changed. - applyThemeColors(); if (config) { qreal t = elapsed.elapsed() / 1000.0; config->setTime(t); diff --git a/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.h b/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.h index ac47b1141..2e230ad7f 100644 --- a/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.h +++ b/cockatrice/src/interface/widgets/onboarding/shader_banner_widget.h @@ -1,7 +1,6 @@ #ifndef SHADER_BANNER_WIDGET_H #define SHADER_BANNER_WIDGET_H -#include #include #include #include @@ -54,7 +53,6 @@ protected: private slots: void tick(); void onSceneGraphFailed(); - void applyThemeColors(); private: struct Preset @@ -75,12 +73,6 @@ private: BannerShaderConfig *config = nullptr; GradientFallbackWidget *fallback = nullptr; - // Palette-derived banner colours -- the theme's window hue forced down to - // the banner's curated darkness, plus the theme's Highlight as accent. - QColor bannerColorA; - QColor bannerColorB; - QColor bannerAccent; - QTimer clock; QElapsedTimer elapsed; Motif currentMotif = Motif::Welcome; diff --git a/cockatrice/src/interface/widgets/onboarding/shaders/brand_banner.frag b/cockatrice/src/interface/widgets/onboarding/shaders/brand_banner.frag index 2bf6d0abf..508bd4bc4 100644 --- a/cockatrice/src/interface/widgets/onboarding/shaders/brand_banner.frag +++ b/cockatrice/src/interface/widgets/onboarding/shaders/brand_banner.frag @@ -28,8 +28,6 @@ layout(std140, binding = 0) uniform buf vec4 uColorA; vec4 uColorB; vec4 uAccent; - vec4 uGlowColor; - float uVignetteMin; float uLogoGlow; }; @@ -121,7 +119,7 @@ vec3 backgroundField(vec2 uv, float time) // Accent-coloured fog layer: flowNoise peaks above 0.6 contribute accent float fog = flowNoise(uv * 0.8 + vec2(100.0, 50.0), time * 0.02); - col += uAccent.rgb * max(fog - 0.6, 0.0) * 0.14; + col += uAccent.rgb * max(fog - 0.6, 0.0) * 0.10; return col; } @@ -138,17 +136,14 @@ vec3 motifWelcome(vec2 uv, vec3 bg, float t) vec2 center = vec2(asp * 0.5, 0.5); float cDist = length(ac - center); - // Centre bloom at logo position; intensity scales with uLogoGlow. The - // QML brandGlow halo now supplies the primary logo surround (the two - // brand appColors), so this shader bloom is deliberately kept as a subtle - // ambience rather than a competing glow. + // Centre bloom at logo position; intensity scales with uLogoGlow float centreLight = bloom(cDist, 0.08 * asp, 0.40 * asp); - col += uGlowColor.rgb * centreLight * 0.20 * uLogoGlow; + col += centreLight * 0.20 * uLogoGlow; // Flow-noise shimmer gated by Gaussian mask at centre float shimmer = flowNoise(ac * 0.8 + vec2(55.0, 33.0), t * 0.05) * 0.5 + 0.5; float shimmerMask = exp(-(cDist * cDist) / (0.18 * asp * 0.18 * asp)); - col += uGlowColor.rgb * shimmer * shimmerMask * 0.04 * uLogoGlow; + col += shimmer * shimmerMask * 0.04 * uLogoGlow; // 48 ember particles: hash-seeded position, speed, size, brightness. // Embers within a distance threshold of centre are deflected into an @@ -167,8 +162,8 @@ vec3 motifWelcome(vec2 uv, vec3 bg, float t) float pX = baseX * asp + sin(t * driftFreq + fi * 1.7) * driftAmp * asp; float pY = fract(baseY + t * riseSpeed); - float size = 0.010 + hash21(vec2(fi * 2.9, uSeed * 4.7)) * 0.014; - float bright = 0.18 + hash21(vec2(fi * 6.1, uSeed * 0.9)) * 0.32; + float size = 0.006 + hash21(vec2(fi * 2.9, uSeed * 4.7)) * 0.012; + float bright = 0.15 + hash21(vec2(fi * 6.1, uSeed * 0.9)) * 0.30; // Fade out near top/bottom edges float edgeFade = smoothstep(0.0, 0.12, pY) * smoothstep(1.0, 0.88, pY); @@ -237,7 +232,7 @@ vec3 motifCardDatabase(vec2 uv, vec3 bg, float t) // Semi-transparent dark fill float fill = smoothstep(0.015, -0.005, d); - col = mix(col, uColorB.rgb * 0.60, fill * 0.62); + col = mix(col, uColorB.rgb * 0.55, fill * 0.50); // Accent outline float edge = smoothstep(0.035, 0.0, abs(d)); @@ -314,7 +309,7 @@ vec3 motifAccount(vec2 uv, vec3 bg, float t) // Node glow via bloom; intensity modulated by pulse float dist = length(ac - pos); - col += uAccent.rgb * bloom(dist, 0.018, 0.08) * mix(0.30, 0.55, pulse); + col += uAccent.rgb * bloom(dist, 0.018, 0.08) * mix(0.20, 0.45, pulse); } // Edges: connect nodes within a radius threshold @@ -328,19 +323,19 @@ vec3 motifAccount(vec2 uv, vec3 bg, float t) vec2 ba = nodePos[j] - nodePos[i]; float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); float lineDist = length(pa - ba * h); - col += uAccent.rgb * smoothstep(0.010, 0.0, lineDist) * strength * 0.14; + col += uAccent.rgb * smoothstep(0.010, 0.0, lineDist) * strength * 0.10; } } } // Central bloom at banner centre float cDist = length(ac - center); - col += uAccent.rgb * bloom(cDist, 0.04, 0.25) * 0.20; + col += uAccent.rgb * bloom(cDist, 0.04, 0.25) * 0.12; // Periodic expanding ring from centre float ripplePhase = t * 0.4; float rippleDist = abs(cDist - fract(ripplePhase) * asp * 0.7); - col += uAccent.rgb * smoothstep(0.02, 0.0, rippleDist) * 0.14; + col += uAccent.rgb * smoothstep(0.02, 0.0, rippleDist) * 0.10; return col; } @@ -461,8 +456,6 @@ void main() else if (uMode < 4.5) col = motifPreferences(uv, bg, t); else col = motifFinish(uv, bg, t); - // Corner vignette; uVignetteMin is scheme-driven (0.62 on dark stages, - // gentler on light ones so near-white corners don't go muddy grey). - col *= mix(uVignetteMin, 1.0, vignette(uv)); + col *= mix(0.62, 1.0, vignette(uv)); fragColor = vec4(col, 1.0) * qt_Opacity; } diff --git a/cockatrice/src/interface/widgets/onboarding/shaders/brand_plate.frag b/cockatrice/src/interface/widgets/onboarding/shaders/brand_plate.frag deleted file mode 100644 index 43b798c9f..000000000 --- a/cockatrice/src/interface/widgets/onboarding/shaders/brand_plate.frag +++ /dev/null @@ -1,42 +0,0 @@ -#version 440 - -// The logo's gradient plate, drawn by us rather than the baked-in SVG: a -// linear blend between the two brand appColors (light AccentSoft at the -// top-left grading to dark AccentStrong at the bottom-right, mirroring -// cockatrice.svg's linearGradient4265-7-8 userSpaceOnUse axis), clipped to -// the bird's full silhouette via the full-color logo's alpha (uSilhouette). -// The white highlight path (cockatrice-logo-white) is overlaid in QML on top, -// exactly as the SVG stacks its white path over the gradient paths. Fully -// static: no glow, no breathing — the plate just sits there like the home -// widget's QPainter composite. - -layout(location = 0) in vec2 qt_TexCoord0; -layout(location = 0) out vec4 fragColor; - -layout(std140, binding = 0) uniform buf -{ - mat4 qt_Matrix; - float qt_Opacity; - vec4 uStrong; - vec4 uSoft; -}; - -// The full-color logo's alpha channel acts as the silhouette mask: the -// gradient only appears inside the bird, exactly like the SVG's gradient paths. -layout(binding = 1) uniform sampler2D uSilhouette; - -void main() -{ - // Recreate cockatrice.svg's own gradient geometry (linearGradient4265-7-8, - // userSpaceOnUse): light AccentSoft at the start point S=(-8.097,-97.746), - // dark AccentStrong at the end E=(162.455,295.208), on the SVG's 300x300 - // canvas. Normalized to UV space, V=E-S=(0.5685,1.3098), so - // t = dot(uv - S_norm, V)/|V|^2 with S_norm=(-0.0270,-0.3258). - float t = clamp(dot(qt_TexCoord0 - vec2(-0.02699, -0.32582), vec2(0.56851, 1.30985)) / 2.03891, 0.0, 1.0); - vec3 color = mix(uSoft.rgb, uStrong.rgb, t); - - // Anti-aliased silhouette clip from the full-color logo's alpha. - float alpha = texture(uSilhouette, qt_TexCoord0).a; - - fragColor = vec4(color * alpha, alpha) * qt_Opacity; -} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp index e8816847e..0b77ca185 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.cpp @@ -1,19 +1,13 @@ #include "printing_selector_card_overlay_widget.h" #include "../../../client/settings/cache_settings.h" -#include "../../card_picture_loader/card_picture_loader.h" #include "../cards/card_info_picture_widget.h" #include "printing_selector_card_display_widget.h" -#include -#include #include #include #include -#include #include -#include -#include #include #include #include @@ -21,49 +15,6 @@ #include #include -namespace -{ -/** - * @brief Places the preview beside the highlighted action inside the given screen. - * - * Side-aware: hugs the side of the action that has room, aligned with its row, then - * clamps every edge so the preview always lands fully on-screen on first show. - */ -QPoint -previewPositionNear(const QRect &actionRect, const QSize &labelSize, const QRect &screenGeometry, int previewOffset) -{ - const bool rightFits = actionRect.right() + previewOffset + labelSize.width() <= screenGeometry.right(); - const bool leftFits = actionRect.left() - previewOffset - labelSize.width() >= screenGeometry.left(); - - int x; - if (rightFits) { - x = actionRect.right() + previewOffset; - } else if (leftFits) { - x = actionRect.left() - previewOffset - labelSize.width(); - } else { - x = actionRect.left(); - } - x = qMax(screenGeometry.left(), x); - x = qMin(screenGeometry.right() - labelSize.width() + 1, x); - - const bool belowFits = actionRect.bottom() + previewOffset + labelSize.height() <= screenGeometry.bottom(); - const bool aboveFits = actionRect.top() - previewOffset - labelSize.height() >= screenGeometry.top(); - - int y; - if (belowFits) { - y = actionRect.bottom() + previewOffset; - } else if (aboveFits) { - y = actionRect.top() - previewOffset - labelSize.height(); - } else { - y = actionRect.top(); - } - y = qMax(screenGeometry.top(), y); - y = qMin(screenGeometry.bottom() - labelSize.height() + 1, y); - - return {x, y}; -} -} // namespace - /** * @brief Constructs a PrintingSelectorCardOverlayWidget for displaying a card overlay. * @@ -99,31 +50,6 @@ PrintingSelectorCardOverlayWidget::PrintingSelectorCardOverlayWidget(QWidget *pa initializePinBadge(); - // Parent the preview to this overlay so it is destroyed with it (Qt::ToolTip keeps - // it a frameless, non-activating top-level window despite the parent). - cardOverridePreviewLabel = new QLabel(this, Qt::ToolTip); - cardOverridePreviewLabel->setWindowFlag(Qt::FramelessWindowHint); - cardOverridePreviewLabel->setAttribute(Qt::WA_ShowWithoutActivating); - cardOverridePreviewLabel->setScaledContents(true); - cardOverridePreviewLabel->hide(); - - // While the preview is visible, keep it honest: when the hovered printing's art - // resolves (all alternate printings share the root card's CardInfo), redraw it in place. - if (rootCard.getCardPtr()) { - connect(rootCard.getCardPtr().data(), &CardInfo::pixmapUpdated, this, [this] { - if (cardOverridePreviewLabel->isVisible()) { - refreshPreview(); - } - }); - } - - // Alt-Tab / app-inactive must not strand the floating preview. - connect(qApp, &QGuiApplication::applicationStateChanged, this, [this](Qt::ApplicationState state) { - if (state != Qt::ApplicationActive) { - hidePreview(); - } - }); - // Update when this overlay emits cardPreferenceChanged or when size/scale changes connect(this, &PrintingSelectorCardOverlayWidget::cardPreferenceChanged, this, &PrintingSelectorCardOverlayWidget::updatePinBadgeVisibility); @@ -259,23 +185,19 @@ void PrintingSelectorCardOverlayWidget::leaveEvent(QEvent *event) } /** - * @brief Creates and shows the card-overlay context menu. + * @brief Creates and shows a custom context menu when the right mouse button is clicked. * - * The menu includes the card art preference (Pin/Unpin Printing), the Image Overrides - * submenu (Load Custom Image, Clear Custom Image, and one entry per alternate printing with a - * live preview), and the Show Related cards submenu. + * The context menu includes an option to show related cards, which displays a submenu with actions + * for each related card. When an action is triggered, the card information is updated, and the + * printing selector is shown. * - * @param point The local position the menu should pop at. + * @param point The position of the mouse when the right-click occurred. */ void PrintingSelectorCardOverlayWidget::customMenu(QPoint point) { QMenu menu; - hidePreview(); // Clear any preview state left over from a previous menu run. - - // Submenus are owned by the stack-allocated top-level menu (addMenu() does not - // transfer ownership). - auto *preferenceMenu = new QMenu(tr("Preference"), &menu); + auto *preferenceMenu = new QMenu(tr("Preference")); menu.addMenu(preferenceMenu); const auto &preferredProviderId = @@ -297,66 +219,8 @@ void PrintingSelectorCardOverlayWidget::customMenu(QPoint point) }); } - menu.addSeparator(); - - auto *overrideMenu = new QMenu(tr("Image Overrides"), &menu); - - auto *loadCustomAction = overrideMenu->addAction(tr("Load Custom Image...")); - auto *clearOverrideAction = overrideMenu->addAction(tr("Clear Custom Image")); - - // Nothing to clear on a card that has no local override yet. - clearOverrideAction->setEnabled(CardPictureLoader::hasLocalOverrides(rootCard)); - - overrideMenu->addSeparator(); - - const auto &allSets = rootCard.getInfo().getSets(); - - for (const auto &set : allSets) { - for (const auto &printing : set) { - if (printing == rootCard.getPrinting()) { - continue; - } - - // The submenu is already scoped to this card, so the rows lead with set + - // collector; only printings with a distinct display name add their own name. - const CardSetPtr cardSet = printing.getSet(); - if (!cardSet) { - continue; - } - - QString label = tr("%1 %2").arg(cardSet->getCorrectedShortName(), printing.getProperty("num")); - - auto *action = overrideMenu->addAction(label); - - ExactCard overrideCard(rootCard.getCardPtr(), printing); - action->setData(QVariant::fromValue(overrideCard)); - - connect(action, &QAction::triggered, this, [this, overrideCard]() { - CardPictureLoader::getInstance().installPrintingOverride(rootCard, overrideCard); - QPixmapCache::clear(); - rootCard.emitPixmapUpdated(); // refresh the overlay art in place, like the other paths - }); - } - } - - connect(clearOverrideAction, &QAction::triggered, this, [this]() { - CardPictureLoader::deleteAllLocalOverrides(rootCard); - QPixmapCache::clear(); - rootCard.emitPixmapUpdated(); // force UI refresh - }); - - connect(loadCustomAction, &QAction::triggered, this, &PrintingSelectorCardOverlayWidget::loadCustomImage); - - connect(overrideMenu, &QMenu::hovered, this, &PrintingSelectorCardOverlayWidget::showPreviewForAction); - connect(overrideMenu, &QMenu::aboutToHide, this, &PrintingSelectorCardOverlayWidget::hidePreview); - connect(overrideMenu, &QMenu::triggered, this, &PrintingSelectorCardOverlayWidget::hidePreview); - - menu.addMenu(overrideMenu); - - menu.addSeparator(); - // filling out the related cards submenu - auto *relatedMenu = new QMenu(tr("Show Related cards"), &menu); + auto *relatedMenu = new QMenu(tr("Show Related cards")); menu.addMenu(relatedMenu); auto relatedCards = rootCard.getInfo().getAllRelatedCards(); if (relatedCards.isEmpty()) { @@ -371,11 +235,7 @@ void PrintingSelectorCardOverlayWidget::customMenu(QPoint point) }); } } - // The preview anchors itself to this popup's global geometry while it is open, so the - // pointer must stay valid for the whole exec() and be dropped before the stack unwinds. - previewSourceMenu = overrideMenu; menu.exec(this->mapToGlobal(point)); - previewSourceMenu = nullptr; } /** @@ -431,136 +291,3 @@ void PrintingSelectorCardOverlayWidget::initializePinBadge() pinBadge->setVisible(false); pinBadge->raise(); } - -/** - * @brief Asks for an image file and installs it as the card's custom art. - * - * Unreadable files answer with a visible warning instead of a silent no-op. - */ -void PrintingSelectorCardOverlayWidget::loadCustomImage() -{ - QString filePath = QFileDialog::getOpenFileName(this, tr("Select Card Image"), QString(), - tr("Images (*.png *.jpg *.jpeg *.webp)")); - - if (filePath.isEmpty()) { - return; - } - - QPixmap pixmap(filePath); - if (pixmap.isNull()) { - // No silent paths: a file that cannot be read answers visibly instead of a no-op. - QMessageBox::warning(this, tr("Load Custom Image"), tr("The selected file could not be read as an image.")); - return; - } - - CardPictureLoader::getInstance().saveCardImageToLocalStorage(rootCard, pixmap, true); - - QPixmapCache::clear(); - rootCard.emitPixmapUpdated(); -} - -/** - * @brief Shows the hover preview for a highlighted printing entry in the Image Overrides submenu. - * - * QMenu::hovered fires on keyboard highlight too, so the preview appears when arrows walk - * onto a printing entry, not only under the mouse. - * - * Non-printing entries (e.g., Load Custom Image, Clear Custom Image) hide the preview. - * - * @param action The action that was highlighted. - */ -void PrintingSelectorCardOverlayWidget::showPreviewForAction(QAction *action) -{ - if (!action) { - hidePreview(); - return; - } - - const QVariant data = action->data(); - - if (!data.canConvert()) { - hidePreview(); - return; - } - - const ExactCard previewCard = qvariant_cast(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(); - } -} diff --git a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h index fbcfa9230..228393c9c 100644 --- a/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h +++ b/cockatrice/src/interface/widgets/printing_selector/printing_selector_card_overlay_widget.h @@ -13,9 +13,6 @@ #include -class QAction; -class QMenu; - class PrintingSelectorCardOverlayWidget : public QWidget { Q_OBJECT @@ -46,19 +43,11 @@ private slots: private: void initializePinBadge(); - void loadCustomImage(); - void showPreviewForAction(QAction *action); - void refreshPreview(); - void hidePreview(); CardInfoPictureWidget *cardInfoPicture; AllZonesCardAmountWidget *allZonesCardAmountWidget; QLabel *pinBadge = nullptr; AbstractTabDeckEditor *deckEditor; ExactCard rootCard; - QLabel *cardOverridePreviewLabel = nullptr; - ExactCard hoveredOverrideCard; - QMenu *previewSourceMenu = nullptr; - QAction *hoveredOverrideAction = nullptr; }; #endif // PRINTING_SELECTOR_CARD_OVERLAY_WIDGET_H diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp index 659325987..a8bf54e91 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.cpp +++ b/cockatrice/src/interface/widgets/server/game_selector.cpp @@ -369,7 +369,7 @@ void GameSelector::joinGame(const ServerInfo_Game &game, const bool asSpectator, return; } - bool overrideRestrictions = tabSupervisor->canOverrideGameRestrictions(); + bool overrideRestrictions = !tabSupervisor->getAdminLocked(); // Joining a full game without override privileges silently becomes a // spectator join, so ask first instead of surprising the player. @@ -462,7 +462,7 @@ void GameSelector::enableButtonsForIndex(const QModelIndex ¤t) } const ServerInfo_Game &game = gameListModel->getGame(current.data(Qt::UserRole).toInt()); - bool overrideRestrictions = tabSupervisor->canOverrideGameRestrictions(); + bool overrideRestrictions = !tabSupervisor->getAdminLocked(); spectateButton->setEnabled(game.spectators_allowed() || overrideRestrictions); joinButton->setEnabled(game.player_count() < game.max_players() || overrideRestrictions); diff --git a/cockatrice/src/interface/widgets/server/remote/remote_decklist_tree_widget.cpp b/cockatrice/src/interface/widgets/server/remote/remote_decklist_tree_widget.cpp index df9dfbf53..a6add3fca 100644 --- a/cockatrice/src/interface/widgets/server/remote/remote_decklist_tree_widget.cpp +++ b/cockatrice/src/interface/widgets/server/remote/remote_decklist_tree_widget.cpp @@ -113,7 +113,7 @@ int RemoteDeckList_TreeModel::rowCount(const QModelIndex &parent) const int RemoteDeckList_TreeModel::columnCount(const QModelIndex & /*parent*/) const { - return 4; + return 3; } QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) const @@ -121,7 +121,7 @@ QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) cons if (!index.isValid()) { return QVariant(); } - if (index.column() >= 4) { + if (index.column() >= 3) { return QVariant(); } @@ -134,29 +134,12 @@ QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) cons switch (index.column()) { case 0: return node->getName(); - case 3: - // Report the node's own bit, not the inherited effective - // state, so it stays in step with what publishing toggles. - if (node->isPublic()) { - return tr("Public"); - } - return isEffectivelyPublic(node) ? tr("Public (inherited)") : tr("Private"); default: return QVariant(); } } case Qt::DecorationRole: return index.column() == 0 ? dirIcon : QVariant(); - case Qt::ToolTipRole: - if (index.column() == 3) { - if (node->isPublic()) { - return tr("This folder is visible to other users"); - } - return isEffectivelyPublic(node) - ? tr("This folder is private, but a parent folder is public (inherited).") - : tr("This folder is only visible to you"); - } - return QVariant(); default: return QVariant(); } @@ -170,13 +153,6 @@ QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) cons return file->getId(); case 2: return file->getUploadTime(); - case 3: - // Report the node's own bit, not the inherited effective - // state, so it stays in step with what publishing toggles. - if (file->isPublic()) { - return tr("Public"); - } - return isEffectivelyPublic(file) ? tr("Public (inherited)") : tr("Private"); default: return QVariant(); } @@ -185,16 +161,6 @@ QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) cons return index.column() == 0 ? fileIcon : QVariant(); case Qt::TextAlignmentRole: return index.column() == 1 ? Qt::AlignRight : Qt::AlignLeft; - case Qt::ToolTipRole: - if (index.column() == 3) { - if (file->isPublic()) { - return tr("This deck is visible to other users"); - } - return isEffectivelyPublic(file) - ? tr("This deck is private, but a parent folder is public (inherited).") - : tr("This deck is only visible to you"); - } - return QVariant(); default: return QVariant(); } @@ -217,8 +183,6 @@ QVariant RemoteDeckList_TreeModel::headerData(int section, Qt::Orientation orien return tr("ID"); case 2: return tr("Upload time"); - case 3: - return tr("Visibility"); default: return QVariant(); } @@ -275,14 +239,13 @@ void RemoteDeckList_TreeModel::addFileToTree(const ServerInfo_DeckStorage_TreeIt time.setSecsSinceEpoch(fileInfo.creation_time()); beginInsertRows(nodeToIndex(parent), parent->size(), parent->size()); - parent->append(new FileNode(QString::fromStdString(file.name()), file.id(), time, parent, fileInfo.is_public())); + parent->append(new FileNode(QString::fromStdString(file.name()), file.id(), time, parent)); endInsertRows(); } void RemoteDeckList_TreeModel::addFolderToTree(const ServerInfo_DeckStorage_TreeItem &folder, DirectoryNode *parent) { DirectoryNode *newItem = addNamedFolderToTree(QString::fromStdString(folder.name()), parent); - newItem->setIsPublic(folder.folder().is_public()); const ServerInfo_DeckStorage_Folder &folderInfo = folder.folder(); const int folderItemsSize = folderInfo.items_size(); for (int i = 0; i < folderItemsSize; ++i) { @@ -322,21 +285,6 @@ void RemoteDeckList_TreeModel::refreshTree() client->sendCommand(pend); } -bool RemoteDeckList_TreeModel::isEffectivelyPublic(const Node *node) const -{ - if (node == nullptr || node == root) { - return false; - } - const Node *current = node; - while (current != nullptr) { - if (current->isPublic()) { - return true; - } - current = current->getParent(); - } - return false; -} - void RemoteDeckList_TreeModel::clearTree() { beginResetModel(); diff --git a/cockatrice/src/interface/widgets/server/remote/remote_decklist_tree_widget.h b/cockatrice/src/interface/widgets/server/remote/remote_decklist_tree_widget.h index 2cf09aff7..3dd91d7a4 100644 --- a/cockatrice/src/interface/widgets/server/remote/remote_decklist_tree_widget.h +++ b/cockatrice/src/interface/widgets/server/remote/remote_decklist_tree_widget.h @@ -27,11 +27,9 @@ public: protected: DirectoryNode *parent; QString name; - bool publicFlag; public: - explicit Node(const QString &_name, DirectoryNode *_parent = nullptr) - : parent(_parent), name(_name), publicFlag(false) + explicit Node(const QString &_name, DirectoryNode *_parent = nullptr) : parent(_parent), name(_name) { } virtual ~Node() = default; @@ -43,14 +41,6 @@ public: { return name; } - [[nodiscard]] bool isPublic() const - { - return publicFlag; - } - void setIsPublic(bool _public) - { - publicFlag = _public; - } }; class DirectoryNode : public Node, public QList { @@ -69,14 +59,9 @@ public: QDateTime uploadTime; public: - FileNode(const QString &_name, - int _id, - const QDateTime &_uploadTime, - DirectoryNode *_parent = nullptr, - bool _isPublic = false) + FileNode(const QString &_name, int _id, const QDateTime &_uploadTime, DirectoryNode *_parent = nullptr) : Node(_name, _parent), id(_id), uploadTime(_uploadTime) { - setIsPublic(_isPublic); } [[nodiscard]] int getId() const { @@ -124,11 +109,6 @@ public: { return root; } - /** - * @brief Whether a node is visible to other users (own flag or inherited - * from any ancestor folder). - */ - [[nodiscard]] bool isEffectivelyPublic(const Node *node) const; void addFileToTree(const ServerInfo_DeckStorage_TreeItem &file, DirectoryNode *parent); void addFolderToTree(const ServerInfo_DeckStorage_TreeItem &folder, DirectoryNode *parent); DirectoryNode *addNamedFolderToTree(const QString &name, DirectoryNode *parent); diff --git a/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp index f95ad88e4..0d2267a63 100644 --- a/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_context_menu.cpp @@ -37,7 +37,6 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent, aDetails = new QAction(QString(), this); aChat = new QAction(QString(), this); aShowGames = new QAction(QString(), this); - aViewPublicDecks = new QAction(QString(), this); aAddToBuddyList = new QAction(QString(), this); aRemoveFromBuddyList = new QAction(QString(), this); aAddToIgnoreList = new QAction(QString(), this); @@ -65,7 +64,6 @@ void UserContextMenu::retranslateUi() aDetails->setText(tr("User &details")); aChat->setText(tr("Private &chat")); aShowGames->setText(tr("Show this user's &games")); - aViewPublicDecks->setText(tr("View this user's &public decks")); aAddToBuddyList->setText(tr("Add to &buddy list")); aRemoveFromBuddyList->setText(tr("Remove from &buddy list")); aAddToIgnoreList->setText(tr("Add to &ignore list")); @@ -378,9 +376,6 @@ void UserContextMenu::showContextMenu(const QPoint &pos, } menu->addAction(aDetails); menu->addAction(aShowGames); - if (userLevel.testFlag(ServerInfo_User::IsRegistered)) { - menu->addAction(aViewPublicDecks); - } menu->addAction(aChat); const QList inviteOptions = inviteOptionsForUser(userName); if (!inviteOptions.isEmpty()) { @@ -460,7 +455,6 @@ void UserContextMenu::showContextMenu(const QPoint &pos, aChat->setEnabled(anotherUser && online && !userListProxy->isUserIgnored(userName)); aShowGames->setEnabled(online); aReport->setEnabled(anotherUser); - aViewPublicDecks->setEnabled(anotherUser); aAddToBuddyList->setEnabled(anotherUser); aRemoveFromBuddyList->setEnabled(anotherUser); aAddToIgnoreList->setEnabled(anotherUser); @@ -487,8 +481,6 @@ void UserContextMenu::showContextMenu(const QPoint &pos, execChat(userName); } else if (actionClicked == aShowGames) { execShowGames(userName); - } else if (actionClicked == aViewPublicDecks) { - execViewPublicDecks(userName); } else if (actionClicked == aAddToBuddyList) { execAddToBuddy(userName); } else if (actionClicked == aRemoveFromBuddyList) { @@ -612,11 +604,6 @@ void UserContextMenu::execShowGames(const QString &userName) client->sendCommand(pend); } -void UserContextMenu::execViewPublicDecks(const QString &userName) -{ - tabSupervisor->openTabPublicDecks(userName); -} - void UserContextMenu::execAddToBuddy(const QString &userName) { Command_AddToList cmd; diff --git a/cockatrice/src/interface/widgets/server/user/user_context_menu.h b/cockatrice/src/interface/widgets/server/user/user_context_menu.h index 0922eae94..6abbc057a 100644 --- a/cockatrice/src/interface/widgets/server/user/user_context_menu.h +++ b/cockatrice/src/interface/widgets/server/user/user_context_menu.h @@ -37,7 +37,6 @@ private: QAction *aUserName; QAction *aDetails; QAction *aShowGames; - QAction *aViewPublicDecks; QAction *aChat; QAction *aAddToBuddyList, *aRemoveFromBuddyList; QAction *aAddToIgnoreList, *aRemoveFromIgnoreList; @@ -112,7 +111,6 @@ public: void execInvite(const QString &userName); void execDetails(const QString &userName); void execShowGames(const QString &userName); - void execViewPublicDecks(const QString &userName); void execAddToBuddy(const QString &userName); void execRemoveFromBuddy(const QString &userName); void execAddToIgnore(const QString &userName); diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp index f161f0f19..c8494f095 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.cpp @@ -59,8 +59,8 @@ AppearanceSettingsPage::AppearanceSettingsPage() connect(&schemeCombo, &QComboBox::currentIndexChanged, this, [this] { themeManager->setColorScheme(schemeCombo.currentData().toString()); }); - // Qt widget style; "System" lets the application decide - styleCombo.addItem(tr("System"), QStringLiteral("System")); + // Qt widget style; "Default" lets the application decide + styleCombo.addItem(tr("Default"), QStringLiteral("Default")); for (const QString &key : QStyleFactory::keys()) { styleCombo.addItem(key, key); } @@ -132,10 +132,6 @@ AppearanceSettingsPage::AppearanceSettingsPage() connect(&homeTabDisplayCardNameCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(), &AppearanceSettings::setHomeTabDisplayCardName); - homeTabBackgroundDimCheckBox.setChecked(settings.appearance().getHomeTabBackgroundDim()); - connect(&homeTabBackgroundDimCheckBox, &QCheckBox::QT_STATE_CHANGED, &settings.appearance(), - &AppearanceSettings::setHomeTabBackgroundDim); - for (const auto &entry : HomeTabButtonColor::all()) { homeTabButtonColorSourceBox.addItem(QObject::tr(entry.trKey)); } @@ -154,7 +150,6 @@ AppearanceSettingsPage::AppearanceSettingsPage() homeTabGrid->addWidget(&homeTabDisplayCardNameCheckBox, 2, 0, 1, 2); homeTabGrid->addWidget(&homeTabButtonColorSourceLabel, 3, 0); homeTabGrid->addWidget(&homeTabButtonColorSourceBox, 3, 1); - homeTabGrid->addWidget(&homeTabBackgroundDimCheckBox, 4, 0, 1, 2); homeTabGroupBox = new QGroupBox; homeTabGroupBox->setLayout(homeTabGrid); @@ -513,12 +508,9 @@ void AppearanceSettingsPage::retranslateUi() homeTabBackgroundShuffleFrequencyLabel.setText(tr("Home tab background shuffle frequency:")); homeTabBackgroundShuffleFrequencySpinBox.setSpecialValueText(tr("Disabled")); homeTabDisplayCardNameCheckBox.setText(tr("Display card name of background in bottom right")); - homeTabBackgroundDimCheckBox.setText(tr("Dim the home tab background")); - homeTabBackgroundDimCheckBox.setToolTip( - tr("Draw a translucent overlay over the home tab background so buttons and text stand out")); homeTabButtonColorSourceLabel.setText(tr("Home tab button color:")); homeTabButtonColorSourceBox.setToolTip( - tr("Use the theme's identity accent colors, or extract colors from the background image")); + tr("Automatic: extract from background if present, otherwise use theme default")); playmatGroupBox->setTitle(tr("Playmat settings")); playmatVisibilityLabel.setText(tr("Playmat visibility:")); diff --git a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h index bec4cd72f..8db71ff8f 100644 --- a/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/appearance_settings_page.h @@ -41,7 +41,6 @@ private: QLabel homeTabBackgroundShuffleFrequencyLabel; QSpinBox homeTabBackgroundShuffleFrequencySpinBox; QCheckBox homeTabDisplayCardNameCheckBox; - QCheckBox homeTabBackgroundDimCheckBox; QLabel homeTabButtonColorSourceLabel; QComboBox homeTabButtonColorSourceBox; diff --git a/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp index 36436f8a3..62b06fb60 100644 --- a/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp @@ -1,21 +1,15 @@ #include "general_settings_page.h" #include "../../../client/settings/cache_settings.h" -#include "../interface/card_picture_loader/card_picture_loader.h" #include "../main.h" #include "../server/user/user_info_connection.h" #include "update/client/release_channel.h" #include -#include #include #include #include -#include #include -#include -#include -#include #include #include #include @@ -52,27 +46,10 @@ GeneralSettingsPage::GeneralSettingsPage() connect(&languageBox, qOverload(&QComboBox::currentIndexChanged), this, &GeneralSettingsPage::languageBoxChanged); - // card text & images language, independent of the UI language - cardLanguageBox.addItem(tr("English"), "en"); - for (const QString &code : CardLocalization::supportedLanguages()) { - cardLanguageBox.addItem(CardLocalization::languageDisplayName(code), code); - } - const int cardLangIndex = cardLanguageBox.findData(SettingsCache::instance().cardsDisplay().getCardLang()); - cardLanguageBox.setCurrentIndex(cardLangIndex < 0 ? 0 : cardLangIndex); - - connect(&cardLanguageBox, qOverload(&QComboBox::currentIndexChanged), this, - &GeneralSettingsPage::cardLanguageBoxChanged); - auto *languageGrid = new QGridLayout; languageGrid->addWidget(&languageLabel, 0, 0); languageGrid->addWidget(&languageBox, 0, 1); - languageGrid->addWidget(&cardLanguageLabel, 1, 0); - languageGrid->addWidget(&cardLanguageBox, 1, 1); - languageGrid->addWidget(&cardLanguageNoteLabel, 2, 1); - languageGrid->addWidget(&advertiseTranslationPageLabel, 3, 1, Qt::AlignRight); - - cardLanguageNoteLabel.setWordWrap(true); - cardLanguageNoteLabel.setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + languageGrid->addWidget(&advertiseTranslationPageLabel, 1, 1, Qt::AlignRight); languageGroupBox = new QGroupBox; languageGroupBox->setLayout(languageGrid); @@ -435,52 +412,6 @@ void GeneralSettingsPage::languageBoxChanged(int index) SettingsCache::instance().personal().setLang(languageBox.itemData(index).toString()); } -void GeneralSettingsPage::cardLanguageBoxChanged(int index) -{ - const QString lang = cardLanguageBox.itemData(index).toString(); - SettingsCache::instance().cardsDisplay().setCardLang(lang); - - // Switching to a non-default language only takes effect after the card - // database is re-imported with that language selected; English data is always - // present, so switching back to English needs no prompt. - if (lang == "en") { - return; - } - - // The binary cache does not track the language its entries were imported in, - // and the downloaded pictures were fetched with English art names, so both are - // stale until Oracle re-imports the database in the new language: drop them. - QFile::remove(SettingsCache::instance().getCardDatabasePath() + ".cache"); - CardPictureLoader::clearNetworkCache(); - CardPictureLoader::clearPixmapCache(); - - // Art is resolved by the translated card name for non-English languages, so the - // matching Scryfall URL is added to the top of the download list. It stays - // visible in the deck editor settings, where it can be removed or reordered. - const bool localizedUrlAdded = SettingsCache::instance().downloads().addLocalizedScryfallUrl(); - - QString message = tr("

The card database only contains English card data. To see cards in %1, " - "Oracle must run once with this language selected and re-import the card " - "database.

" - "

The cached database and the downloaded card pictures have been cleared, so a " - "re-import is picked up without stale entries.

") - .arg(cardLanguageBox.itemText(index)); - if (localizedUrlAdded) { - message += tr("

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.

"); - } - message += tr("

Run Oracle now?

"); - - const QMessageBox::StandardButton answer = QMessageBox::question( - this, tr("Card text & images language changed"), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - - // The answer only controls whether Oracle starts right away; the caches stay - // cleared so the next import or launch rebuilds them in the new language. - if (answer == QMessageBox::Yes) { - emit cardDatabaseUpdateRequested(); - } -} - void GeneralSettingsPage::updateStartupServerControlsVisibility() { const int index = startupTabSelector.currentIndex(); @@ -498,10 +429,6 @@ void GeneralSettingsPage::retranslateUi() languageGroupBox->setTitle(tr("Language settings")); languageLabel.setText(tr("Language:")); - cardLanguageBox.setItemText(0, tr("English")); - cardLanguageLabel.setText(tr("Card text & images language:")); - cardLanguageNoteLabel.setText( - tr("Foreign card names, text and art apply after you update the card database (Oracle).")); advertiseTranslationPageLabel.setText( QString("%2").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations"))); diff --git a/cockatrice/src/interface/widgets/settings_page/general_settings_page.h b/cockatrice/src/interface/widgets/settings_page/general_settings_page.h index 7afba158f..e0c1a47bf 100644 --- a/cockatrice/src/interface/widgets/settings_page/general_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/general_settings_page.h @@ -23,10 +23,6 @@ public: static QStringList findQmFiles(); static QString languageName(const QString &lang); -signals: - /// Request to re-import the card database with the newly selected card language - void cardDatabaseUpdateRequested(); - private slots: void deckPathButtonClicked(); void filtersPathButtonClicked(); @@ -37,7 +33,6 @@ private slots: void tokenDatabasePathButtonClicked(); void resetAllPathsClicked(); void languageBoxChanged(int index); - void cardLanguageBoxChanged(int index); void updateStartupServerControlsVisibility(); private: @@ -51,10 +46,6 @@ private: QComboBox languageBox; QLabel advertiseTranslationPageLabel; - QLabel cardLanguageLabel; - QComboBox cardLanguageBox; - QLabel cardLanguageNoteLabel; - QLabel updateReleaseChannelLabel; QComboBox updateReleaseChannelBox; QCheckBox startupUpdateCheckCheckBox; diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp index 6423c581b..f80649eba 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -11,7 +11,6 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" -#include "../cards/additional_info/deck_color_identity.h" #include "../client/network/interfaces/deck_stats_interface.h" #include "../client/network/interfaces/tapped_out_interface.h" #include "../deck_editor/deck_state_manager.h" @@ -20,7 +19,6 @@ #include "../interface/widgets/dialogs/dlg_load_deck.h" #include "../interface/widgets/dialogs/dlg_load_deck_from_clipboard.h" #include "../interface/widgets/dialogs/dlg_load_deck_from_website.h" -#include "../interface/widgets/dialogs/dlg_share_deck.h" #include "../utility/visibility_change_listener.h" #include "tab_supervisor.h" @@ -325,7 +323,6 @@ bool AbstractTabDeckEditor::actSaveDeck() Command_DeckUpload cmd; cmd.set_deck_id(static_cast(loadedDeck.lastLoadInfo.remoteDeckId)); cmd.set_deck_list(deckString.toStdString()); - cmd.set_color_identity(getDeckColorIdentity(loadedDeck.deckList, CardDatabaseManager::query()).toStdString()); PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd); connect(pend, &PendingCommand::finished, this, &AbstractTabDeckEditor::saveDeckRemoteFinished); @@ -385,27 +382,6 @@ bool AbstractTabDeckEditor::actSaveDeckAs() return true; } -/** - * @brief Opens the deck share dialog with the current deck preselected. - */ -void AbstractTabDeckEditor::actShareDeck() -{ - AbstractClient *client = tabSupervisor->getServerClient(); - if (client->getStatus() != StatusLoggedIn) { - QMessageBox::information(this, tr("Share deck"), tr("You must be connected to the server to share a deck.")); - return; - } - - const QSharedPointer deck = deckStateManager->getDeckListShared(); - if (deck->isBlankDeck()) { - QMessageBox::information(this, tr("Share deck"), tr("The deck is empty. Add cards before sharing it.")); - return; - } - - DlgShareDeck shareDialog(client, deck, this); - shareDialog.exec(); -} - /** * @brief Callback for remote deck save completion. * @param response Server response. diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h index dae8f5b0a..a3cda2bfc 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.h @@ -214,9 +214,6 @@ protected slots: /** @brief Saves the current deck under a new name. */ virtual bool actSaveDeckAs(); - /** @brief Opens the deck share dialog for the current deck. */ - void actShareDeck(); - /** @brief Loads a deck from the clipboard. */ virtual void actLoadDeckFromClipboard(); diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp index 657ef3dbe..66b68d823 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp @@ -1,6 +1,5 @@ #include "archidekt_api_response_deck_display_widget.h" -#include "../../../../../../client/settings/cache_settings.h" #include "../../../../../deck_loader/card_node_function.h" #include "../../../../../deck_loader/deck_loader.h" #include "../../../../cards/card_size_widget.h" @@ -11,7 +10,6 @@ #include #include -#include ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWidget *parent, ArchidektApiResponseDeck _response, @@ -122,9 +120,6 @@ ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWi } 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); auto decklist = QSharedPointer(new DeckList); diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp index 62769d0e3..f3535d850 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp @@ -3,54 +3,35 @@ #include "../../../client/settings/cache_settings.h" #include "../../deck_loader/deck_loader.h" #include "../../pixel_map_generator.h" -#include "../cards/additional_info/deck_color_identity.h" -#include "../deck_share/deck_share_utils.h" -#include "../deck_share/share_bar_widget.h" #include "../interface/widgets/server/remote/remote_decklist_tree_widget.h" #include "../interface/widgets/utility/get_text_with_max.h" #include #include -#include #include #include #include #include -#include #include #include -#include #include -#include #include #include #include #include -#include #include #include #include #include #include -#include -#include #include #include #include -#include #include #include -#include #include #include -namespace -{ -// How long to wait after the last visibility change before reading back the -// Public/Private column, in milliseconds. -constexpr int VISIBILITY_REFRESH_DELAY = 500; -} // namespace - TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User *currentUserInfo) @@ -111,34 +92,8 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, serverDirView = new RemoteDeckList_TreeWidget(client); connect(serverDirView, &QTreeView::doubleClicked, this, &TabDeckStorage::actRemoteDoubleClick); - connect(serverDirView->selectionModel(), &QItemSelectionModel::selectionChanged, this, - [this] { onServerSelectionChanged(); }); - - // Share bar for creating a share link from the selected server decks/folders. - shareBar = new ShareBarWidget(this); - connect(shareBar, &ShareBarWidget::createRequested, this, &TabDeckStorage::actShareSelection); - connect(shareBar, &ShareBarWidget::cancelRequested, this, &TabDeckStorage::cancelShareDecks); - shareBar->setVisible(false); - - shareTimeoutTimer = new QTimer(this); - shareTimeoutTimer->setSingleShot(true); - shareTimeoutTimer->setInterval( - static_cast((static_cast(SettingsCache::instance().network().getTimeOut()) + 1) * - SettingsCache::instance().network().getKeepAlive() * 1000)); - connect(shareTimeoutTimer, &QTimer::timeout, this, &TabDeckStorage::onShareFromTreeTimeout); - - // Restartable single-shot refresh for the Public/Private column. It is - // armed with the full network timeout when a publish is sent (so a dropped - // reply still drains once) and re-armed with the short delay every time a - // reply lands, so the drain cannot fire while a slow round trip is still in - // flight. Either way the tree is re-read once things quiet down. - visibilityRefreshTimer = new QTimer(this); - visibilityRefreshTimer->setSingleShot(true); - visibilityRefreshTimer->setInterval(VISIBILITY_REFRESH_DELAY); - connect(visibilityRefreshTimer, &QTimer::timeout, this, &TabDeckStorage::onVisibilityRefreshTimeout); QVBoxLayout *rightVbox = new QVBoxLayout; - rightVbox->addWidget(shareBar); rightVbox->addWidget(serverDirView); rightVbox->addLayout(rightToolBarLayout); rightGroupBox = new QGroupBox; @@ -184,14 +139,6 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, aDeleteRemoteDeck->setIcon(themePixmap(QStringLiteral("icons/remove_row"))); connect(aDeleteRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteRemoteDeck); - aShareDecks = new QAction(this); - aShareDecks->setIcon(themePixmap(QStringLiteral("icons/share"))); - connect(aShareDecks, &QAction::triggered, this, &TabDeckStorage::actShareDecks); - - aPublishDeck = new QAction(this); - aPublishDeck->setIcon(QPixmap("theme:icons/lock")); - connect(aPublishDeck, &QAction::triggered, this, &TabDeckStorage::actPublishDeck); - // Add actions to toolbars leftToolBar->addAction(aOpenLocalDeck); leftToolBar->addAction(aRenameLocal); @@ -203,8 +150,6 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, rightToolBar->addAction(aOpenRemoteDeck); rightToolBar->addAction(aDownload); - rightToolBar->addAction(aShareDecks); - rightToolBar->addAction(aPublishDeck); rightToolBar->addAction(aNewFolder); rightToolBar->addAction(aDeleteRemoteDeck); @@ -233,13 +178,7 @@ void TabDeckStorage::retranslateUi() aNewFolder->setText(tr("New folder")); aDeleteLocalDeck->setText(tr("Delete")); aDeleteRemoteDeck->setText(tr("Delete")); - aShareDecks->setText(tr("Share decks")); - aPublishDeck->setText(tr("Publish/unpublish deck")); aOpenDecksFolder->setText(tr("Open decks folder")); - shareBar->retranslateUi(); - if (shareBar->isVisible()) { - onServerSelectionChanged(); - } } QString TabDeckStorage::getTargetPath() const @@ -272,8 +211,6 @@ void TabDeckStorage::handleConnected(const ServerInfo_User &userInfo) void TabDeckStorage::handleConnectionChanged(ClientStatus status) { if (status == StatusDisconnected) { - visibilityRefreshTimer->stop(); - visibilityRefreshStarted = false; setRemoteEnabled(false); } } @@ -283,15 +220,12 @@ void TabDeckStorage::setRemoteEnabled(bool enabled) aUpload->setEnabled(enabled); aOpenRemoteDeck->setEnabled(enabled); aDownload->setEnabled(enabled); - aShareDecks->setEnabled(enabled); - aPublishDeck->setEnabled(enabled); aNewFolder->setEnabled(enabled); aDeleteRemoteDeck->setEnabled(enabled); if (enabled) { serverDirView->refreshTree(); } else { - setShareModeEnabled(false); serverDirView->clearTree(); } } @@ -413,8 +347,6 @@ void TabDeckStorage::uploadDeck(const QString &filePath, const QString &targetPa cmd.set_path(targetPath.toStdString()); cmd.set_deck_list(deckString.toStdString()); - cmd.set_color_identity(getDeckColorIdentity(deck, CardDatabaseManager::query()).toStdString()); - PendingCommand *pend = client->prepareSessionCommand(cmd); connect(pend, &PendingCommand::finished, this, &TabDeckStorage::uploadFinished); client->sendCommand(pend); @@ -694,241 +626,3 @@ void TabDeckStorage::deleteFolderFinished(const Response &response, const Comman serverDirView->removeNode(toDelete); } } - -void TabDeckStorage::actShareDecks() -{ - setShareModeEnabled(true); -} - -void TabDeckStorage::cancelShareDecks() -{ - setShareModeEnabled(false); -} - -void TabDeckStorage::setShareModeEnabled(bool enabled) -{ - shareBar->setVisible(enabled); - if (enabled) { - shareBar->setCreateEnabled(true); - shareBar->setName(tr("Shared decks")); - onServerSelectionChanged(); - shareBar->focusName(); - } else { - // Abandon any in-flight request: otherwise the timer keeps running and a late - // response reports the share as created after the user already backed out. - shareTimeoutTimer->stop(); - shareInFlightSeq = 0; - serverDirView->clearSelection(); - } -} - -void TabDeckStorage::onServerSelectionChanged() -{ - if (!shareBar->isVisible()) { - return; - } - const auto selection = serverDirView->getCurrentSelection(); - int folders = 0; - int files = 0; - for (const auto *node : selection) { - if (dynamic_cast(node)) { - ++folders; - } else { - ++files; - } - } - - QString hint; - if (folders > 1) { - hint = tr("Only one folder can be shared at a time."); - } else if (folders > 0 && files > 0) { - hint = tr("Share either a folder or decks, not both."); - } else if (folders == 0 && files == 0) { - hint = tr("Select folders or decks in the tree to share."); - } - shareBar->setHintText(hint, !hint.isEmpty()); - - QStringList parts; - if (folders > 0) { - parts << tr("%n folder(s)", "", folders); - } - if (files > 0) { - parts << tr("%n deck(s)", "", files); - } - shareBar->setCountText(parts.isEmpty() ? tr("No decks selected") - : tr("Selected: %1").arg(parts.join(QStringLiteral(", ")))); -} - -void TabDeckStorage::actShareSelection() -{ - const auto selection = serverDirView->getCurrentSelection(); - QString sharedFolder; - bool hasFile = false; - bool hasFolder = false; - for (const auto *node : selection) { - if (const auto *dirNode = dynamic_cast(node)) { - hasFolder = true; - if (!sharedFolder.isEmpty()) { - showShareNotice(tr("Only one folder can be shared at a time."), true); - return; - } - sharedFolder = dirNode->getPath(); - } else { - hasFile = true; - } - } - - if (hasFile && hasFolder) { - showShareNotice(tr("Share either a folder or decks, not both."), true); - return; - } - if (hasFolder && sharedFolder.isEmpty()) { - showShareNotice(tr("The root folder cannot be shared."), true); - return; - } - - Command_DeckShareCreate cmd; - cmd.set_name(shareBar->name().toStdString()); - if (cmd.name().empty()) { - cmd.set_name(tr("Shared decks").toStdString()); - } - - if (!sharedFolder.isEmpty()) { - cmd.set_folder_path(sharedFolder.toStdString()); - } else { - for (const auto *node : selection) { - if (const auto *fileNode = dynamic_cast(node)) { - DeckShareItem *item = cmd.add_items(); - item->set_deck_id(fileNode->getId()); - } - } - } - - if (cmd.items_size() == 0 && cmd.folder_path().empty()) { - showShareNotice(tr("Select decks to share."), true); - return; - } - - shareBar->setCreateEnabled(false); - const int seq = ++shareRequestSeq; - shareInFlightSeq = seq; - PendingCommand *pend = client->prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, - [this, seq](const Response &response, const CommandContainer &commandContainer) { - if (shareInFlightSeq != seq) { - return; // the user cancelled or a newer request superseded this one - } - shareInFlightSeq = 0; - shareFromTreeFinished(response, commandContainer); - }); - client->sendCommand(pend); - shareTimeoutTimer->start(); -} - -void TabDeckStorage::shareFromTreeFinished(const Response &response, const CommandContainer & /*commandContainer*/) -{ - shareTimeoutTimer->stop(); - shareBar->setCreateEnabled(true); - if (response.response_code() != Response::RespOk) { - qWarning() << "failed to create deck share:" << response.response_code(); - showShareNotice(tr("Failed to create the share link (server response code %1).") - .arg(QString::number(static_cast(response.response_code()))), - true); - return; - } - const DeckShareUtils::ShareResponse share = DeckShareUtils::handleShareResponse(client, response); - - showShareNotice( - tr("Share link copied to the clipboard.\nExpires on %1.").arg(DeckShareUtils::formatShareExpiry(share.expiry))); - setShareModeEnabled(false); -} - -void TabDeckStorage::showShareNotice(const QString &message, bool warning) -{ - QMessageBox box(warning ? QMessageBox::Warning : QMessageBox::Information, tr("Share link"), message, - QMessageBox::Ok, this); - box.exec(); -} - -void TabDeckStorage::onShareFromTreeTimeout() -{ - if (shareInFlightSeq == 0) { - return; // share mode was left while the request was still outstanding - } - shareInFlightSeq = 0; - shareBar->setCreateEnabled(true); - showShareNotice(tr("The server did not respond in time. Try again."), true); -} - -void TabDeckStorage::actPublishDeck() -{ - visibilityFailures.clear(); - // Arm the drain with the full network timeout so a lost reply still costs - // one refresh instead of a dead column; each reply shrinks it to the short - // delay below, so a slow round trip is never drained before it lands. - const int visibilityFailSafeDelay = - static_cast((static_cast(SettingsCache::instance().network().getTimeOut()) + 1) * - SettingsCache::instance().network().getKeepAlive() * 1000); - - const auto selection = serverDirView->getCurrentSelection(); - for (const auto *node : selection) { - Command_DeckSetVisibility cmd; - if (const auto *fileNode = dynamic_cast(node)) { - cmd.set_deck_id(fileNode->getId()); - } else if (const auto *dirNode = dynamic_cast(node)) { - const QString path = dirNode->getPath(); - if (path.isEmpty()) { - continue; // the root folder cannot be published - } - cmd.set_folder_path(path.toStdString()); - } else { - continue; - } - // Toggle the node's own visibility bit (what the server persists); the - // effective visibility shown by the column may additionally be inherited - // from a parent folder. - cmd.set_is_public(!node->isPublic()); - - PendingCommand *pend = client->prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabDeckStorage::setVisibilityFinished); - visibilityRefreshStarted = true; - visibilityRefreshTimer->setInterval(visibilityFailSafeDelay); - visibilityRefreshTimer->start(); - client->sendCommand(pend); - } -} - -void TabDeckStorage::setVisibilityFinished(const Response &r, const CommandContainer & /*commandContainer*/) -{ - if (r.response_code() == Response::RespOk) { - if (visibilityRefreshStarted) { - visibilityRefreshTimer->setInterval(VISIBILITY_REFRESH_DELAY); - visibilityRefreshTimer->start(); - } - return; - } - - // Collect batch failures and surface them once, when publishing quiets - // down, instead of stacking one modal dialog per rejected node. - const QString message = tr("Failed to change deck visibility on server (response code %1).") - .arg(QString::number(static_cast(r.response_code()))); - if (visibilityRefreshStarted) { - visibilityFailures.append(message); - visibilityRefreshTimer->setInterval(VISIBILITY_REFRESH_DELAY); - visibilityRefreshTimer->start(); - } else { - QMessageBox::critical(this, tr("Error"), message); - } -} - -void TabDeckStorage::onVisibilityRefreshTimeout() -{ - visibilityRefreshStarted = false; - if (!visibilityFailures.isEmpty()) { - QMessageBox::critical( - this, tr("Error"), - tr("Failed to change the visibility of %n selected deck(s).", "", visibilityFailures.size())); - visibilityFailures.clear(); - } - serverDirView->refreshTree(); -} diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.h b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.h index f8d585880..a863e0625 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.h +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.h @@ -11,7 +11,6 @@ #include "../interface/widgets/server/remote/remote_decklist_tree_widget.h" #include "tab.h" -#include #include struct LoadedDeck; @@ -23,10 +22,8 @@ class QToolBar; class QTreeWidget; class QTreeWidgetItem; class QGroupBox; -class QTimer; class CommandContainer; class Response; -class ShareBarWidget; class TabDeckStorage : public Tab { @@ -38,25 +35,14 @@ private: QToolBar *leftToolBar, *rightToolBar; RemoteDeckList_TreeWidget *serverDirView; QGroupBox *leftGroupBox, *rightGroupBox; - ShareBarWidget *shareBar; - QTimer *shareTimeoutTimer; - int shareRequestSeq = 0; - int shareInFlightSeq = 0; QAction *aOpenLocalDeck, *aRenameLocal, *aUpload, *aNewLocalFolder, *aDeleteLocalDeck; QAction *aOpenDecksFolder; - QAction *aOpenRemoteDeck, *aDownload, *aShareDecks, *aPublishDeck, *aNewFolder, *aDeleteRemoteDeck; - bool visibilityRefreshStarted = false; - QTimer *visibilityRefreshTimer; - QStringList visibilityFailures; + QAction *aOpenRemoteDeck, *aDownload, *aNewFolder, *aDeleteRemoteDeck; QString getTargetPath() const; void setRemoteEnabled(bool enabled); - void showShareNotice(const QString &message, bool warning = false); - - void setShareModeEnabled(bool enabled); - void uploadDeck(const QString &filePath, const QString &targetPath); void deleteRemoteDeck(const RemoteDeckList_TreeModel::Node *node); @@ -89,17 +75,6 @@ private slots: void actNewFolder(); void newFolderFinished(const Response &response, const CommandContainer &commandContainer); - void actShareDecks(); - void actShareSelection(); - void cancelShareDecks(); - void onServerSelectionChanged(); - void shareFromTreeFinished(const Response &r, const CommandContainer &commandContainer); - void onShareFromTreeTimeout(); - - void actPublishDeck(); - void setVisibilityFinished(const Response &r, const CommandContainer &commandContainer); - void onVisibilityRefreshTimeout(); - void actDeleteRemoteDeck(); void deleteFolderFinished(const Response &response, const CommandContainer &commandContainer); void deleteDeckFinished(const Response &response, const CommandContainer &commandContainer); diff --git a/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp b/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp deleted file mode 100644 index 35389d3bf..000000000 --- a/cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp +++ /dev/null @@ -1,251 +0,0 @@ -#include "tab_public_decks.h" - -#include "../../../client/settings/cache_settings.h" -#include "../../deck_loader/deck_loader.h" -#include "../general/layout_containers/flow_widget.h" -#include "../visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h" -#include "../visual_deck_storage/deck_preview/public_deck_preview_widget.h" -#include "../visual_deck_storage/remote_public_decks_model.h" -#include "../visual_deck_storage/visual_deck_storage_quick_settings_widget.h" -#include "../visual_deck_storage/visual_deck_storage_search_widget.h" -#include "../visual_deck_storage/visual_deck_storage_tag_filter_widget.h" -#include "tab_supervisor.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -TabPublicDecks::TabPublicDecks(TabSupervisor *_tabSupervisor, AbstractClient *_client, const QString &_userName) - : Tab(_tabSupervisor), client(_client), userName(_userName) -{ - model = new RemotePublicDecksModel(client, this); - cardSize = SettingsCache::instance().cardsDisplay().getVisualDeckStorageCardSize(); - - titleLabel = new QLabel(tr("Public decks of %1").arg(userName.toHtmlEscaped()), this); - QFont titleFont = titleLabel->font(); - titleFont.setBold(true); - titleLabel->setFont(titleFont); - - auto *headerLayout = new QHBoxLayout; - headerLayout->addWidget(titleLabel); - headerLayout->addStretch(1); - - // Filter/toolbar row, matching the Visual Deck Storage: color identity filter - // first, the search bar stretching in the middle, and the quick settings - // cogwheel at the end. The card size slider lives inside the cogwheel popup. - emptyLabel = new QLabel(tr("This user has not published any decks."), this); - emptyLabel->setAlignment(Qt::AlignCenter); - emptyLabel->setVisible(false); - - statusLabel = new QLabel(this); - statusLabel->setAlignment(Qt::AlignCenter); - statusLabel->setVisible(false); - - flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); - flowWidget->setSpacing(8, 8); - - colorIdentityFilter = new DeckPreviewColorIdentityFilterWidget(this); - searchWidget = new VisualDeckStorageSearchWidget(this); - refreshButton = new QToolButton(this); - refreshButton->setIcon(QPixmap("theme:icons/reload")); - refreshButton->setFixedSize(32, 32); - quickSettingsWidget = new VisualDeckStorageQuickSettingsWidget(this); - quickSettingsWidget->setPublicDecksMode(true); - - auto *filterLayout = new QHBoxLayout; - filterLayout->addWidget(colorIdentityFilter); - filterLayout->addWidget(searchWidget, 1); - filterLayout->addWidget(refreshButton); - filterLayout->addWidget(quickSettingsWidget); - - tagFilterWidget = new VisualDeckStorageTagFilterWidget(this); - tagFilterWidget->setAllTagsProvider([this] { return model->allTags(); }); - updateTagsVisibility(quickSettingsWidget->getShowTagFilter()); - - auto *layout = new QVBoxLayout; - layout->addLayout(headerLayout); - layout->addLayout(filterLayout); - layout->addWidget(tagFilterWidget); - layout->addWidget(statusLabel); - layout->addWidget(emptyLabel); - layout->addWidget(flowWidget, 1); - - auto *mainWidget = new QWidget(this); - mainWidget->setLayout(layout); - setCentralWidget(mainWidget); - - connect(refreshButton, &QToolButton::clicked, this, [this] { model->refresh(userName); }); - connect(model, &QAbstractItemModel::modelReset, this, &TabPublicDecks::rebuildGrid); - connect(model, &RemotePublicDecksModel::loadingChanged, this, &TabPublicDecks::updateLoadingState); - connect(model, &RemotePublicDecksModel::loadFailed, this, [this](const QString &message) { - lastFailureMessage = message; - statusLabel->setText(message); - statusLabel->setVisible(true); - flowWidget->setVisible(false); - emptyLabel->setVisible(false); - }); - connect(searchWidget, &VisualDeckStorageSearchWidget::searchTextChanged, this, - [this](const QString &text) { model->setSearchText(text); }); - connect(colorIdentityFilter, &DeckPreviewColorIdentityFilterWidget::activeColorsChanged, this, - &TabPublicDecks::updateColorFilter); - connect(colorIdentityFilter, &DeckPreviewColorIdentityFilterWidget::filterModeChanged, this, - &TabPublicDecks::updateColorFilter); - connect(tagFilterWidget, &VisualDeckStorageTagFilterWidget::filterChanged, this, &TabPublicDecks::updateTagFilter); - connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::cardSizeChanged, this, - &TabPublicDecks::updateCardSize); - connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::showTagFilterChanged, this, - &TabPublicDecks::updateTagsVisibility); - - retranslateUi(); - model->refresh(userName); -} - -QString TabPublicDecks::getTabText() const -{ - return tr("Public decks of %1").arg(userName); -} - -void TabPublicDecks::retranslateUi() -{ - // The username is another user's data, so escape it for the AutoText QLabel. - titleLabel->setText(tr("Public decks of %1").arg(userName.toHtmlEscaped())); - // The same choice rebuildGrid makes, so a language change does not swap - // the "no match" variant for the "nothing published" one. - emptyLabel->setText(model->totalCount() > 0 ? tr("No decks match your filters.") - : tr("This user has not published any decks.")); - refreshButton->setToolTip(tr("Refresh")); - refreshButton->setAccessibleName(tr("Refresh")); - quickSettingsWidget->setToolTip(tr("Public Decks Settings")); - // Re-show whatever the status label is showing so a language change picks up - // the new language or, for a failure message, at least does not hide it. - if (model->isLoading()) { - updateLoadingState(true); - } else if (!lastFailureMessage.isEmpty()) { - statusLabel->setText(lastFailureMessage); - statusLabel->setVisible(true); - flowWidget->setVisible(false); - emptyLabel->setVisible(false); - } else { - updateLoadingState(false); - } - emit tabTextChanged(this, getTabText()); -} - -bool TabPublicDecks::closeRequest() -{ - emit closing(this); - return Tab::closeRequest(); -} - -void TabPublicDecks::rebuildGrid() -{ - flowWidget->clearLayout(); - - const int count = model->rowCount(); - if (count == 0) { - emptyLabel->setText(model->totalCount() > 0 ? tr("No decks match your filters.") - : tr("This user has not published any decks.")); - } - emptyLabel->setVisible(count == 0); - for (int i = 0; i < count; ++i) { - auto *tile = new PublicDeckPreviewWidget(flowWidget, model->entryAt(i)); - tile->setScaleFactor(cardSize); - connect(tile, &PublicDeckPreviewWidget::openDeckRequested, this, &TabPublicDecks::openDeck); - flowWidget->addWidget(tile); - } - - // The deck set changed, so the tag filter chips are re-gathered from it. - tagFilterWidget->refreshTags(); -} - -void TabPublicDecks::updateColorFilter() -{ - model->setColorFilter(colorIdentityFilter->getFilterMode(), colorIdentityFilter->getActiveColors()); -} - -void TabPublicDecks::updateTagFilter() -{ - const QStringList selectedTags = tagFilterWidget->selectedTags(); - const QStringList excludedTags = tagFilterWidget->excludedTags(); - model->setTagFilter(QSet(selectedTags.cbegin(), selectedTags.cend()), - QSet(excludedTags.cbegin(), excludedTags.cend())); - tagFilterWidget->refreshTags(); -} - -void TabPublicDecks::updateTagsVisibility(bool visible) -{ - tagFilterWidget->setVisible(visible); -} - -void TabPublicDecks::updateLoadingState(bool loading) -{ - if (loading) { - // A new attempt is under way, so the previously shown failure, if any, - // no longer describes the current state. - lastFailureMessage.clear(); - statusLabel->setText(tr("Loading public decks…")); - statusLabel->setVisible(true); - flowWidget->setVisible(false); - emptyLabel->setVisible(false); - } else { - statusLabel->setVisible(false); - flowWidget->setVisible(true); - } -} - -void TabPublicDecks::updateCardSize(int scale) -{ - cardSize = scale; - applyCardSize(scale); -} - -void TabPublicDecks::applyCardSize(int scale) -{ - const auto tiles = flowWidget->findChildren(); - for (PublicDeckPreviewWidget *tile : tiles) { - tile->setScaleFactor(scale); - } - flowWidget->setMinimumSizeToMaxSizeHint(); -} - -void TabPublicDecks::openDeck(int deckId) -{ - Command_DeckDownloadPublic cmd; - cmd.set_deck_id(deckId); - - PendingCommand *pend = client->prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabPublicDecks::openDeckFinished); - client->sendCommand(pend); -} - -void TabPublicDecks::openDeckFinished(const Response &response, const CommandContainer & /*commandContainer*/) -{ - if (response.response_code() != Response::RespOk) { - QMessageBox::warning(this, tr("Open public deck"), - tr("Failed to open the public deck (server response code %1).") - .arg(QString::number(static_cast(response.response_code())))); - return; - } - - const Response_DeckDownload &resp = response.GetExtension(Response_DeckDownload::ext); - std::optional deckOpt = - DeckLoader::loadFromRemote(QString::fromStdString(resp.deck()), LoadedDeck::LoadInfo::NON_REMOTE_ID); - if (!deckOpt) { - QMessageBox::warning(this, tr("Open public deck"), tr("The public deck could not be parsed.")); - return; - } - - tabSupervisor->openDeckInNewTab(deckOpt.value()); -} diff --git a/cockatrice/src/interface/widgets/tabs/tab_public_decks.h b/cockatrice/src/interface/widgets/tabs/tab_public_decks.h deleted file mode 100644 index 492fdeafa..000000000 --- a/cockatrice/src/interface/widgets/tabs/tab_public_decks.h +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @file tab_public_decks.h - * @ingroup Tabs - */ - -#ifndef TAB_PUBLIC_DECKS_H -#define TAB_PUBLIC_DECKS_H - -#include "tab.h" - -class AbstractClient; -class CommandContainer; -class DeckPreviewColorIdentityFilterWidget; -class FlowWidget; -class PublicDeckPreviewWidget; -class QLabel; -class QToolButton; -class RemotePublicDecksModel; -class Response; -class VisualDeckStorageQuickSettingsWidget; -class VisualDeckStorageSearchWidget; -class VisualDeckStorageTagFilterWidget; - -/** - * @brief A visual grid of the public decks published by another user. - * - * The grid is rendered from the preview metadata the server stores for the - * decks, so browsing costs no downloads; the deck list is fetched via - * Command_DeckDownloadPublic only when the user opens a deck. Multiple users - * can be browsed simultaneously; each gets its own tab. - */ -class TabPublicDecks final : public Tab -{ - Q_OBJECT - -public: - TabPublicDecks(TabSupervisor *tabSupervisor, AbstractClient *client, const QString &userName); - - [[nodiscard]] QString getTabText() const override; - void retranslateUi() override; - bool closeRequest() override; - - [[nodiscard]] QString getUserName() const - { - return userName; - } - -signals: - void closing(TabPublicDecks *tab); - -private slots: - void openDeck(int deckId); - void openDeckFinished(const Response &response, const CommandContainer &commandContainer); - void updateColorFilter(); - void updateTagFilter(); - void updateCardSize(int scale); - void updateTagsVisibility(bool visible); - void updateLoadingState(bool loading); - -private: - void rebuildGrid(); - void applyCardSize(int scale); - - AbstractClient *client; - QString userName; - RemotePublicDecksModel *model; - FlowWidget *flowWidget; - VisualDeckStorageSearchWidget *searchWidget; - DeckPreviewColorIdentityFilterWidget *colorIdentityFilter; - VisualDeckStorageTagFilterWidget *tagFilterWidget; - QToolButton *refreshButton; - VisualDeckStorageQuickSettingsWidget *quickSettingsWidget; - QLabel *titleLabel; - QLabel *statusLabel; - QLabel *emptyLabel; - QString lastFailureMessage; ///< Last load-failure text, re-shown on retranslate. - int cardSize = 100; -}; - -#endif // TAB_PUBLIC_DECKS_H diff --git a/cockatrice/src/interface/widgets/tabs/tab_server.cpp b/cockatrice/src/interface/widgets/tabs/tab_server.cpp index fca32094c..13a77e957 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_server.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_server.cpp @@ -13,7 +13,6 @@ #include #include #include -#include #include #include @@ -186,37 +185,25 @@ void TabServer::processServerMessageEvent(const Event_ServerMessage &event) void TabServer::joinRoom(int id, bool setCurrent) { TabRoom *room = tabSupervisor->getRoomTabs().value(id); - if (room) { - if (setCurrent) { - tabSupervisor->setCurrentWidget((QWidget *)room); - } + if (!room) { + 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); + return; } - auto pendingIt = pendingRoomJoins.find(id); - 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; + if (setCurrent) { + tabSupervisor->setCurrentWidget((QWidget *)room); } - - 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, @@ -224,72 +211,34 @@ void TabServer::joinRoomFinished(const Response &r, const QVariant &extraData, 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()) { case Response::RespOk: break; case Response::RespNameNotFound: - if (setCurrent) { - QMessageBox::critical(this, tr("Error"), - 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); return; case Response::RespContextError: - if (healedJoin) { - // The rejoin below was already answered and the server still rejects the join, so - // the stale-membership heal cannot help: surface the error. The guard was already - // released above so a later user-initiated join may try a fresh heal. - 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); + 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; case Response::RespUserLevelTooLow: - if (setCurrent) { - QMessageBox::critical(this, tr("Error"), - 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); return; default: - if (setCurrent) { - QMessageBox::critical( - this, tr("Error"), - tr("Failed to join the server room due to an unknown error: %1.").arg(r.response_code())); - } + QMessageBox::critical( + this, tr("Error"), + tr("Failed to join the server room due to an unknown error: %1.").arg(r.response_code())); emit roomJoinFailed(roomId); return; } const Response_JoinRoom &resp = r.GetExtension(Response_JoinRoom::ext); - 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); + emit roomJoined(resp.room_info(), extraData.toBool()); } diff --git a/cockatrice/src/interface/widgets/tabs/tab_server.h b/cockatrice/src/interface/widgets/tabs/tab_server.h index 121ff814d..c10b7945b 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_server.h +++ b/cockatrice/src/interface/widgets/tabs/tab_server.h @@ -10,8 +10,6 @@ #include "tab.h" #include -#include -#include #include #include @@ -60,17 +58,10 @@ private slots: int roomId); private: - void leaveAndRejoinRoom(int roomId, bool setCurrent); - AbstractClient *client; RoomSelector *roomSelector; QTextBrowser *serverInfoBox; bool shouldEmitUpdate = false; - /** Room ids with a join command in flight, mapped to whether the tab should be focused once it opens. */ - QHash 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 healedRoomJoins; public: TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client); diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index c9bda7703..462aa420b 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -21,7 +21,6 @@ #include "tab_logs.h" #include "tab_message.h" #include "tab_moderation.h" -#include "tab_public_decks.h" #include "tab_replays.h" #include "tab_report.h" #include "tab_room.h" @@ -275,10 +274,6 @@ void TabSupervisor::retranslateUi() while (gameIterator.hasNext()) { tabs.append(gameIterator.next().value()); } - QMapIterator publicDecksIterator(publicDecksTabs); - while (publicDecksIterator.hasNext()) { - tabs.append(publicDecksIterator.next().value()); - } QListIterator replayIterator(replayTabs); while (replayIterator.hasNext()) { tabs.append(replayIterator.next()); @@ -631,10 +626,6 @@ void TabSupervisor::stop() tabsToDelete << i.value(); } - for (auto i = publicDecksTabs.cbegin(), end = publicDecksTabs.cend(); i != end; ++i) { - tabsToDelete << i.value(); - } - for (const auto tab : tabsToDelete) { tab->close(); } @@ -681,7 +672,7 @@ void TabSupervisor::actTabVisualDeckStorage(bool checked) void TabSupervisor::openTabVisualDeckStorage() { - tabVisualDeckStorage = new TabDeckStorageVisual(this, client); + tabVisualDeckStorage = new TabDeckStorageVisual(this); myAddTab(tabVisualDeckStorage, aTabVisualDeckStorage); connect(tabVisualDeckStorage, &QObject::destroyed, this, [this] { tabVisualDeckStorage = nullptr; @@ -1046,30 +1037,6 @@ void TabSupervisor::roomLeft(TabRoom *tab) removeTab(indexOf(tab)); } -void TabSupervisor::openTabPublicDecks(const QString &userName) -{ - if (auto *existing = publicDecksTabs.value(userName, nullptr)) { - setCurrentWidget(existing); - return; - } - - auto *tab = new TabPublicDecks(this, client, userName); - connect(tab, &TabPublicDecks::closing, this, &TabSupervisor::publicDecksClosed); - myAddTab(tab); - publicDecksTabs.insert(userName, tab); - setCurrentWidget(tab); -} - -void TabSupervisor::publicDecksClosed(TabPublicDecks *tab) -{ - if (tab == currentWidget()) { - emit setMenu(); - } - - publicDecksTabs.remove(tab->getUserName()); - removeTab(indexOf(tab)); -} - void TabSupervisor::switchToFirstAvailableNetworkTab() { if (!roomTabs.isEmpty()) { @@ -1484,11 +1451,6 @@ bool TabSupervisor::getAdminLocked() const return tabAdmin->getLocked(); } -bool TabSupervisor::canOverrideGameRestrictions() const -{ - return !getAdminLocked() || (userInfo->user_level() & ServerInfo_User::IsJudge); -} - void TabSupervisor::processNotifyUserEvent(const Event_NotifyUser &event) { diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h index 066c84a77..aec1d7418 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h @@ -47,7 +47,6 @@ class TabAccount; class TabDeckEditor; class TabDeveloper; class TabLog; -class TabPublicDecks; class RoomEvent; class GameEventContainer; class Event_GameJoined; @@ -115,7 +114,6 @@ private: QMap gameTabs; QList replayTabs; QMap messageTabs; - QMap publicDecksTabs; QList deckEditorTabs; bool isLocalGame; @@ -154,10 +152,6 @@ public: return userInfo; } [[nodiscard]] AbstractClient *getClient() const; - [[nodiscard]] AbstractClient *getServerClient() const - { - return client; - } [[nodiscard]] UserListManager *getUserListManager() const { return userListManager; @@ -177,7 +171,6 @@ public: [[nodiscard]] QList getGameInviteLinksForRoom(int roomId) const; void sendInviteToUser(const QString &userName, const QString &inviteText); [[nodiscard]] bool getAdminLocked() const; - [[nodiscard]] bool canOverrideGameRestrictions() const; void closeEvent(QCloseEvent *event) override; bool switchToGameTabIfAlreadyExists(const int gameId); static void actShowPopup(const QString &message); @@ -205,7 +198,6 @@ public slots: void actTabReplays(bool checked); void openTabServer(); void addRoomTab(const ServerInfo_Room &info, bool setCurrent); - void openTabPublicDecks(const QString &userName); private slots: void refreshShortcuts(); @@ -238,7 +230,6 @@ private slots: void localGameJoined(const Event_GameJoined &event); void gameLeft(TabGame *tab); void roomLeft(TabRoom *tab); - void publicDecksClosed(TabPublicDecks *tab); TabMessage *addMessageTab(const QString &userName, bool focus); void replayLeft(TabGame *tab); void processUserLeft(const QString &userName); diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp index 03df76b03..0cbcb641a 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp @@ -1,78 +1,25 @@ #include "tab_deck_storage_visual.h" -#include "../../../../client/settings/cache_settings.h" -#include "../../../deck_loader/deck_loader.h" -#include "../../cards/additional_info/deck_color_identity.h" -#include "../../deck_share/deck_share_utils.h" #include "../../interface/widgets/visual_deck_storage/visual_deck_storage_widget.h" #include "../tab_supervisor.h" #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include -TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor, AbstractClient *_client) - : Tab(_tabSupervisor), visualDeckStorageWidget(new VisualDeckStorageWidget(this)), client(_client), - shareTimeoutTimer(new QTimer(this)) +TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor) + : Tab(_tabSupervisor), visualDeckStorageWidget(new VisualDeckStorageWidget(this)) { connect(this, &TabDeckStorageVisual::openDeckEditor, tabSupervisor, &TabSupervisor::openDeckInNewTab); connect(visualDeckStorageWidget, &VisualDeckStorageWidget::deckLoadRequested, this, &TabDeckStorageVisual::actOpenLocalDeck); connect(visualDeckStorageWidget, &VisualDeckStorageWidget::openDeckEditor, this, &TabDeckStorageVisual::openDeckEditor); - connect(visualDeckStorageWidget, &VisualDeckStorageWidget::shareDeckRequested, this, - &TabDeckStorageVisual::actShareDeck); - connect(visualDeckStorageWidget, &VisualDeckStorageWidget::shareSelectionChanged, this, - &TabDeckStorageVisual::onShareSelectionChanged); - connect(visualDeckStorageWidget, &VisualDeckStorageWidget::shareRequested, this, [this] { - if (shareDeckAvailable) { - enterShareMode(); - } - }); auto *widget = new QWidget(this); auto *layout = new QVBoxLayout(widget); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(0); widget->setLayout(layout); this->setCentralWidget(widget); layout->addWidget(visualDeckStorageWidget); - - shareBar = new ShareBarWidget(this); - connect(shareBar, &ShareBarWidget::createRequested, this, &TabDeckStorageVisual::actShareSelected); - connect(shareBar, &ShareBarWidget::cancelRequested, this, &TabDeckStorageVisual::exitShareMode); - - layout->insertWidget(0, shareBar); - shareBar->setVisible(false); - - connect(client, &AbstractClient::statusChanged, this, &TabDeckStorageVisual::handleConnectionChanged); - shareDeckAvailable = (client->getStatus() == StatusLoggedIn); - visualDeckStorageWidget->setShareAvailable(shareDeckAvailable); - - shareTimeoutTimer->setSingleShot(true); - shareTimeoutTimer->setInterval( - static_cast((static_cast(SettingsCache::instance().network().getTimeOut()) + 1) * - SettingsCache::instance().network().getKeepAlive() * 1000)); - connect(shareTimeoutTimer, &QTimer::timeout, this, &TabDeckStorageVisual::onShareTimeout); - - retranslateUi(); -} - -void TabDeckStorageVisual::retranslateUi() -{ - visualDeckStorageWidget->retranslateUi(); - shareBar->retranslateUi(); - if (shareBar->isVisible()) { - updateShareHint(); - } } void TabDeckStorageVisual::actOpenLocalDeck(const QString &filePath) @@ -86,144 +33,3 @@ void TabDeckStorageVisual::actOpenLocalDeck(const QString &filePath) emit openDeckEditor(deckOpt.value()); } - -void TabDeckStorageVisual::enterShareMode(const QStringList &preselectFiles) -{ - if (!shareDeckAvailable) { - return; // sharing is gated on being logged in - } - shareBar->setCreateEnabled(true); - visualDeckStorageWidget->setShareSelectable(true); - visualDeckStorageWidget->setShareSelectedFiles(preselectFiles); - shareBar->setName(tr("Shared decks")); - shareBar->setVisible(true); - updateShareHint(); - shareBar->focusName(); -} - -void TabDeckStorageVisual::exitShareMode() -{ - // Abandon any in-flight request: otherwise the timer keeps running and a late - // response reports the share as created after the user already backed out. - shareTimeoutTimer->stop(); - shareInFlightSeq = 0; - visualDeckStorageWidget->setShareSelectable(false); - visualDeckStorageWidget->clearShareSelection(); - shareBar->setVisible(false); -} - -void TabDeckStorageVisual::actShareDeck(const QString &filePath) -{ - if (!shareDeckAvailable) { - QMessageBox::information(this, tr("Share deck"), tr("You must be connected to the server to share a deck.")); - return; - } - enterShareMode({filePath}); -} - -void TabDeckStorageVisual::onShareSelectionChanged() -{ - if (shareBar->isVisible()) { - updateShareHint(); - } -} - -void TabDeckStorageVisual::updateShareHint() -{ - const int count = visualDeckStorageWidget->selectedFilePaths().size(); - shareBar->setCountText(tr("%n deck(s)", "", count)); - if (count == 0) { - shareBar->setHintText(tr("Click deck tiles to select the decks you want to share."), true); - } else if (count == 1) { - shareBar->setHintText(tr("One deck selected. Create the link to share it with other players."), true); - } else { - shareBar->setHintText(tr("%n decks selected. Create the link to share them with other players.", "", count), - true); - } -} - -void TabDeckStorageVisual::actShareSelected() -{ - const QStringList filePaths = visualDeckStorageWidget->selectedFilePaths(); - if (filePaths.isEmpty()) { - QMessageBox::warning(this, tr("Share decks"), tr("Select at least one deck to share.")); - return; - } - - Command_DeckShareCreate cmd; - cmd.set_name(shareBar->name().toStdString()); - if (cmd.name().empty()) { - cmd.set_name(tr("Shared decks").toStdString()); - } - - for (const QString &filePath : filePaths) { - std::optional deckOpt = - DeckLoader::loadFromFile(filePath, DeckFileFormat::getFormatFromName(filePath), true); - if (!deckOpt) { - QMessageBox::warning(this, tr("Share decks"), tr("Unable to load deck file %1").arg(filePath)); - return; - } - DeckShareItem *item = cmd.add_items(); - item->set_deck_list(deckOpt->deckList.writeToString_Native().toStdString()); - item->set_color_identity(getDeckColorIdentity(deckOpt->deckList, CardDatabaseManager::query()).toStdString()); - } - - shareBar->setCreateEnabled(false); - const int seq = ++shareRequestSeq; - shareInFlightSeq = seq; - PendingCommand *pend = client->prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, - [this, seq](const Response &response, const CommandContainer &commandContainer) { - if (shareInFlightSeq != seq) { - return; // the user cancelled or a newer request superseded this one - } - shareInFlightSeq = 0; - shareFinished(response, commandContainer); - }); - client->sendCommand(pend); - shareTimeoutTimer->start(); -} - -void TabDeckStorageVisual::shareFinished(const Response &response, const CommandContainer & /*commandContainer*/) -{ - shareTimeoutTimer->stop(); - shareBar->setCreateEnabled(true); - if (response.response_code() != Response::RespOk) { - showShareNotice(tr("Failed to create the share link (server response code %1).") - .arg(QString::number(static_cast(response.response_code()))), - true); - return; - } - - const DeckShareUtils::ShareResponse share = DeckShareUtils::handleShareResponse(client, response); - - showShareNotice( - tr("Share link copied to the clipboard.\nExpires on %1.").arg(DeckShareUtils::formatShareExpiry(share.expiry))); - exitShareMode(); -} - -void TabDeckStorageVisual::handleConnectionChanged(ClientStatus status) -{ - shareDeckAvailable = (status == StatusLoggedIn); - visualDeckStorageWidget->setShareAvailable(shareDeckAvailable); - if (!shareDeckAvailable && shareBar->isVisible()) { - exitShareMode(); - } -} - -void TabDeckStorageVisual::showShareNotice(const QString &message, bool warning) -{ - QMessageBox box(warning ? QMessageBox::Warning : QMessageBox::Information, tr("Share link"), message, - QMessageBox::Ok, this); - box.exec(); -} - -void TabDeckStorageVisual::onShareTimeout() -{ - if (shareInFlightSeq == 0) { - return; // share mode was left while the request was still outstanding - } - shareInFlightSeq = 0; - shareBar->setCreateEnabled(true); - showShareNotice(tr("The server did not respond in time. Try again."), true); -} diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.h b/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.h index 7cd4e7d17..d3f64e23d 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.h +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.h @@ -7,19 +7,14 @@ #ifndef TAB_DECK_STORAGE_VISUAL_H #define TAB_DECK_STORAGE_VISUAL_H -#include "../../deck_share/share_bar_widget.h" #include "../tab.h" -#include -#include - struct LoadedDeck; class AbstractClient; class CommandContainer; class DeckPreviewWidget; class QFileSystemModel; class QGroupBox; -class QTimer; class QToolBar; class QTreeView; class QTreeWidget; @@ -31,55 +26,23 @@ class TabDeckStorageVisual final : public Tab { Q_OBJECT public: - explicit TabDeckStorageVisual(TabSupervisor *_tabSupervisor, AbstractClient *_client); - void retranslateUi() override; - + explicit TabDeckStorageVisual(TabSupervisor *_tabSupervisor); + void retranslateUi() override + { + } [[nodiscard]] QString getTabText() const override { return tr("Visual Deck Storage"); } - /** - * @brief Enters share-selection mode, optionally preselecting the given deck files. - */ - void enterShareMode(const QStringList &preselectFiles = {}); - - /** - * @brief Leaves share-selection mode and clears the selection. - */ - void exitShareMode(); - - [[nodiscard]] bool isShareModeActive() const - { - return shareBar->isVisible(); - } - public slots: void actOpenLocalDeck(const QString &filePath); - void actShareDeck(const QString &filePath); signals: void openDeckEditor(const LoadedDeck &deck); -private slots: - void actShareSelected(); - void shareFinished(const Response &response, const CommandContainer &commandContainer); - void onShareTimeout(); - void onShareSelectionChanged(); - void handleConnectionChanged(ClientStatus status); - private: - void showShareNotice(const QString &message, bool warning = false); - void updateShareHint(); - VisualDeckStorageWidget *visualDeckStorageWidget; - - ShareBarWidget *shareBar = nullptr; - AbstractClient *client; - QTimer *shareTimeoutTimer; - int shareRequestSeq = 0; - int shareInFlightSeq = 0; - bool shareDeckAvailable = false; }; #endif diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp index d1a780c38..fd529ff69 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp @@ -1,10 +1,11 @@ #include "deck_preview_color_identity_filter_widget.h" #include "../../cards/additional_info/mana_symbol_widget.h" +#include "../visual_deck_storage_widget.h" #include -DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(QWidget *parent) +DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent) : QWidget(parent), layout(new QHBoxLayout(this)) { setLayout(layout); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h index d54984bcf..def45de66 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h @@ -14,12 +14,14 @@ #include #include +class VisualDeckStorageWidget; + class DeckPreviewColorIdentityFilterWidget : public QWidget { Q_OBJECT public: - explicit DeckPreviewColorIdentityFilterWidget(QWidget *parent = nullptr); + explicit DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent); void retranslateUi(); /** diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp index a7fef5031..db466b77a 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp @@ -48,7 +48,6 @@ QSize DeckPreviewTagDisplayWidget::sizeHint() const void DeckPreviewTagDisplayWidget::mousePressEvent(QMouseEvent *event) { - const TagState previousState = state; switch (event->button()) { case Qt::LeftButton: setState(state != TagState::Selected ? TagState::Selected : TagState::NotSelected); @@ -63,12 +62,7 @@ void DeckPreviewTagDisplayWidget::mousePressEvent(QMouseEvent *event) break; } - // Only announce a change when the state was actually toggled, so a click that falls - // through the switch (e.g. a button the widget does not react to) does not drive a - // full tag-filter update and layout pass for nothing. - if (state != previousState) { - emit tagClicked(); - } + emit tagClicked(); QWidget::mousePressEvent(event); } diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.h index df2a6b404..980741c5d 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.h @@ -48,10 +48,7 @@ public: signals: /** - * @brief Emitted when a click toggles the chip's selection/exclusion state. - * - * Not emitted for clicks that leave the state unchanged. Connected handlers use - * this as the trigger to update filters built from selectedTags()/excludedTags(). + * @brief Emitted when the tag is clicked. */ void tagClicked(); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp index 53fe32314..876fbf6ad 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp @@ -11,7 +11,6 @@ #include "deck_preview_deck_tags_display_widget.h" #include -#include #include #include #include @@ -28,7 +27,7 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent, VisualDeckStorageWidget *_visualDeckStorageWidget, VisualDeckStorageModel *_model, const QString &_filePath) - : QWidget(_parent), filePath(_filePath), visualDeckStorageWidget(_visualDeckStorageWidget), model(_model) + : QWidget(_parent), visualDeckStorageWidget(_visualDeckStorageWidget), model(_model), filePath(_filePath) { layout = new QVBoxLayout(this); setLayout(layout); @@ -37,8 +36,6 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent, new DeckPreviewCardPictureWidget(this, false, visualDeckStorageWidget->deckPreviewSelectionAnimationEnabled); pictureWidget->setFontSize(24); connect(pictureWidget, &DeckPreviewCardPictureWidget::imageClicked, this, &DeckPreviewWidget::imageClickedEvent); - connect(pictureWidget, &DeckPreviewCardPictureWidget::imageSingleClicked, this, - &DeckPreviewWidget::imageSingleClicked); connect(pictureWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this, &DeckPreviewWidget::imageDoubleClickedEvent); bannerCardDisplayWidget = pictureWidget; @@ -102,15 +99,6 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent, // to keep the resize handler from searching the widget tree on every layout pass. fixedWidthChildren = {bannerCardDisplayWidget, colorIdentityWidget, deckTagsDisplayWidget, bannerCardLabel, bannerCardComboBox}; - - // Child of the banner widget so the frame tracks the banner's selection animation - // (which animates the banner's position) instead of staying at a stale static offset. - selectionFrame = new QFrame(bannerCardDisplayWidget); - selectionFrame->setAttribute(Qt::WA_TransparentForMouseEvents); - selectionFrame->setStyleSheet(QStringLiteral( - "QFrame { border: 2px solid palette(highlight); border-radius: 4px; background: transparent; }")); - selectionFrame->setVisible(false); - bannerCardDisplayWidget->installEventFilter(this); } void DeckPreviewWidget::retranslateUi() @@ -134,63 +122,6 @@ void DeckPreviewWidget::resizeEvent(QResizeEvent *event) for (QWidget *widget : fixedWidthChildren) { widget->setMaximumWidth(width); } - updateSelectionFrameGeometry(); -} - -bool DeckPreviewWidget::eventFilter(QObject *watched, QEvent *event) -{ - if (watched == bannerCardDisplayWidget && (event->type() == QEvent::Resize || event->type() == QEvent::Move)) { - updateSelectionFrameGeometry(); - } - return QWidget::eventFilter(watched, event); -} - -void DeckPreviewWidget::setShareSelectable(bool selectable) -{ - shareSelectable = selectable; - if (!selectable) { - setShareSelected(false); - } - updateSelectionStyle(); -} - -void DeckPreviewWidget::setShareSelected(bool selected) -{ - if (shareSelected == selected) { - return; - } - shareSelected = selected; - updateSelectionStyle(); - emit shareSelectionToggled(selected); -} - -bool DeckPreviewWidget::isShareSelected() const -{ - return shareSelected; -} - -bool DeckPreviewWidget::isShareSelectable() const -{ - return shareSelectable; -} - -void DeckPreviewWidget::updateSelectionFrameGeometry() -{ - if (selectionFrame == nullptr || bannerCardDisplayWidget == nullptr) { - return; - } - // Frame is a child of the banner, so it is positioned in banner coordinates and - // tracks the banner's selection animation automatically. A small inset keeps the - // highlight visible around the card art without occluding it. - selectionFrame->setGeometry(bannerCardDisplayWidget->rect().adjusted(1, 1, -1, -1)); - selectionFrame->raise(); -} - -void DeckPreviewWidget::updateSelectionStyle() -{ - if (selectionFrame != nullptr) { - selectionFrame->setVisible(shareSelectable && isShareSelected()); - } } void DeckPreviewWidget::enterEvent(QEnterEvent *event) @@ -406,20 +337,10 @@ void DeckPreviewWidget::imageClickedEvent(QMouseEvent *event, DeckPreviewCardPic } } -void DeckPreviewWidget::imageSingleClicked() -{ - if (isShareSelectable()) { - setShareSelected(!isShareSelected()); - } -} - void DeckPreviewWidget::imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance) { Q_UNUSED(event); Q_UNUSED(instance); - if (isShareSelectable()) { - return; // in share mode a double click would just toggle a single selection - } emit deckLoadRequested(filePath); } @@ -444,9 +365,6 @@ QMenu *DeckPreviewWidget::createRightClickMenu() } }); - connect(menu->addAction(tr("Share deck...")), &QAction::triggered, this, - [this] { emit shareDeckRequested(filePath); }); - connect(menu->addAction(tr("Edit Tags")), &QAction::triggered, deckTagsDisplayWidget, &DeckPreviewDeckTagsDisplayWidget::openTagEditDlg); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h index f4909eda8..7bb69f9b9 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h @@ -17,7 +17,6 @@ #include class QEnterEvent; -class QFrame; class QLabel; class QMenu; class QMouseEvent; @@ -42,19 +41,9 @@ public: */ DeckPreviewCardPictureWidget *bannerCardDisplayWidget; - /** @brief The path of the deck file backing this preview. */ - QString filePath; - - void setShareSelectable(bool selectable); - void setShareSelected(bool selected); - [[nodiscard]] bool isShareSelected() const; - [[nodiscard]] bool isShareSelectable() const; - signals: void deckLoadRequested(const QString &filePath); void openDeckEditor(const LoadedDeck &deck); - void shareDeckRequested(const QString &filePath); - void shareSelectionToggled(bool selected); public slots: /** @@ -77,7 +66,6 @@ public slots: protected: void enterEvent(QEnterEvent *event) override; void resizeEvent(QResizeEvent *event) override; - bool eventFilter(QObject *watched, QEvent *event) override; private: [[nodiscard]] int row() const; @@ -88,7 +76,6 @@ private: QMenu *createRightClickMenu(); void addSetBannerCardMenu(QMenu *menu); void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); - void imageSingleClicked(); void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); void actRenameDeck(); @@ -97,6 +84,7 @@ private: VisualDeckStorageWidget *visualDeckStorageWidget; VisualDeckStorageModel *model; + QString filePath; QVBoxLayout *layout; ColorIdentityWidget *colorIdentityWidget; DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget; @@ -104,12 +92,6 @@ private: QComboBox *bannerCardComboBox; QList fixedWidthChildren; ///< Children clamped to the picture width on resize. int lastKnownBannerWidth = -1; ///< The picture width last applied to the children. - QFrame *selectionFrame = nullptr; - bool shareSelectable = false; - bool shareSelected = false; - - void updateSelectionStyle(); - void updateSelectionFrameGeometry(); }; class NoScrollFilter : public QObject diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.cpp deleted file mode 100644 index b49d6f9f4..000000000 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.cpp +++ /dev/null @@ -1,167 +0,0 @@ -#include "public_deck_preview_widget.h" - -#include "../../../../client/settings/cache_settings.h" -#include "../../cards/additional_info/color_identity_widget.h" -#include "../../cards/deck_preview_card_picture_widget.h" -#include "../../general/layout_containers/flow_widget.h" -#include "deck_preview_tag_display_widget.h" - -#include -#include -#include -#include -#include -#include -#include - -PublicDeckPreviewWidget::PublicDeckPreviewWidget(QWidget *parent, const RemotePublicDecksModel::DeckEntry &entry) - : QWidget(parent) -{ - bannerCardDisplayWidget = new DeckPreviewCardPictureWidget(this); - bannerCardDisplayWidget->setFontSize(24); - - // The whole tile is a single focusable, keyboard-operable control: Tab lands - // on it and Space/Enter opens the deck, mirroring the shared-deck preview tile. - setFocusPolicy(Qt::StrongFocus); - - uploadTimeLabel = new QLabel(this); - uploadTimeLabel->setAlignment(Qt::AlignHCenter); - - colorIdentityWidget = new ColorIdentityWidget(this); - - tagsFlowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded); - tagsFlowWidget->setSpacing(3, 3); - - auto *layout = new QVBoxLayout(this); - layout->setContentsMargins(0, 0, 0, 0); - layout->addWidget(bannerCardDisplayWidget); - layout->addWidget(uploadTimeLabel); - layout->addWidget(colorIdentityWidget); - layout->addWidget(tagsFlowWidget); - setLayout(layout); - - connect(&SettingsCache::instance().visualDeckStorage(), - &VisualDeckStorageSettings::visualDeckStorageShowColorIdentityChanged, this, - &PublicDeckPreviewWidget::updateColorIdentityVisibility); - connect(&SettingsCache::instance().visualDeckStorage(), - &VisualDeckStorageSettings::visualDeckStorageShowTagsOnDeckPreviewsChanged, this, - &PublicDeckPreviewWidget::updateTagsVisibility); - connect(&SettingsCache::instance().visualDeckStorage(), - &VisualDeckStorageSettings::visualDeckStorageShowUploadTimeChanged, this, - &PublicDeckPreviewWidget::updateUploadTimeVisibility); - - setEntry(entry); - - connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageClicked, this, - &PublicDeckPreviewWidget::imageClickedEvent); - connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this, - &PublicDeckPreviewWidget::imageDoubleClickedEvent); - - // resizeEvent clamps every child to the banner picture's width, so collect them - // once here to keep the resize handler from searching the widget tree on every pass. - fixedWidthChildren = {bannerCardDisplayWidget, uploadTimeLabel, colorIdentityWidget, tagsFlowWidget}; -} - -void PublicDeckPreviewWidget::resizeEvent(QResizeEvent *event) -{ - QWidget::resizeEvent(event); - if (bannerCardDisplayWidget == nullptr) { - return; - } - - const int width = bannerCardDisplayWidget->width(); - if (width == lastKnownBannerWidth) { - return; - } - lastKnownBannerWidth = width; - - for (QWidget *widget : fixedWidthChildren) { - widget->setMaximumWidth(width); - } -} - -void PublicDeckPreviewWidget::setEntry(const RemotePublicDecksModel::DeckEntry &entry) -{ - deckId = entry.id; - - hasColorIdentity = !entry.colorIdentity.isEmpty(); - colorIdentityWidget->setColorIdentity(entry.colorIdentity); - updateColorIdentityVisibility(); - - const ExactCard bannerCard = - entry.bannerCardName.isEmpty() - ? ExactCard() - : CardDatabaseManager::query()->getCard(CardRef{entry.bannerCardName, entry.bannerCardProvider}); - bannerCardDisplayWidget->setCard(bannerCard); - - // The deck name is the overlay text on the banner, like the local preview. - bannerCardDisplayWidget->setOverlayText(entry.name); - // The deck name comes from another user's record, and Qt tooltips are - // rendered as AutoText, so escape and bound it to keep it readable text - // (the overlay painted onto the banner is already a plain painter draw). - setToolTip(entry.name.left(200).toHtmlEscaped()); - setBaseAccessibleName(entry.name); - - tagsFlowWidget->clearLayout(); - for (const QString &tag : entry.tags) { - auto *chip = new DeckPreviewTagDisplayWidget(tagsFlowWidget, tag); - chip->setAttribute(Qt::WA_TransparentForMouseEvents); - tagsFlowWidget->addWidget(chip); - } - hasTags = !entry.tags.isEmpty(); - updateTagsVisibility(); - - uploadTimeLabel->setText(tr("Uploaded %1").arg(entry.uploadTime.toString(Qt::TextDate))); - hasUploadTime = !entry.uploadTime.isNull(); - updateUploadTimeVisibility(); -} - -void PublicDeckPreviewWidget::updateColorIdentityVisibility() -{ - colorIdentityWidget->setVisible( - hasColorIdentity && SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowColorIdentity()); -} - -void PublicDeckPreviewWidget::updateTagsVisibility() -{ - tagsFlowWidget->setVisible( - hasTags && SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowTagsOnDeckPreviews()); -} - -void PublicDeckPreviewWidget::updateUploadTimeVisibility() -{ - uploadTimeLabel->setVisible(hasUploadTime && - SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowUploadTime()); -} - -void PublicDeckPreviewWidget::keyPressEvent(QKeyEvent *event) -{ - if (event->key() == Qt::Key_Space || event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) { - event->accept(); - emit openDeckRequested(deckId); - return; - } - QWidget::keyPressEvent(event); -} - -void PublicDeckPreviewWidget::setBaseAccessibleName(const QString &name) -{ - baseAccessibleName = name; - setAccessibleName(name); -} - -void PublicDeckPreviewWidget::setScaleFactor(int scale) -{ - bannerCardDisplayWidget->setScaleFactor(scale); -} - -void PublicDeckPreviewWidget::imageClickedEvent(QMouseEvent * /*event*/, DeckPreviewCardPictureWidget * /*instance*/) -{ - // Reserved: clicking could show a card popup for the banner card. -} - -void PublicDeckPreviewWidget::imageDoubleClickedEvent(QMouseEvent * /*event*/, - DeckPreviewCardPictureWidget * /*instance*/) -{ - emit openDeckRequested(deckId); -} diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.h deleted file mode 100644 index a0e5dd43a..000000000 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.h +++ /dev/null @@ -1,75 +0,0 @@ -/** - * @file public_deck_preview_widget.h - * @ingroup VisualDeckPreviewWidgets - */ - -#ifndef PUBLIC_DECK_PREVIEW_WIDGET_H -#define PUBLIC_DECK_PREVIEW_WIDGET_H - -#include "../remote_public_decks_model.h" - -#include -#include -#include - -class ColorIdentityWidget; -class DeckPreviewCardPictureWidget; -class FlowWidget; -class QKeyEvent; -class QLabel; -class QMouseEvent; -class QResizeEvent; - -/** - * @brief A preview tile for a public deck published by another user. - * - * Renders the banner card picture (looked up by name/provider in the card - * database) with the deck name overlaid, the color identity, the deck's tags - * (read-only) and its upload time, all from the metadata the server stores for - * the deck, so no deck list is downloaded until the user actually opens the - * deck. Double-clicking the banner requests opening it. - */ -class PublicDeckPreviewWidget final : public QWidget -{ - Q_OBJECT - -public: - explicit PublicDeckPreviewWidget(QWidget *parent, const RemotePublicDecksModel::DeckEntry &entry); - - void setEntry(const RemotePublicDecksModel::DeckEntry &entry); - - /** @brief Sets the accessible name announced to assistive technologies. */ - void setBaseAccessibleName(const QString &name); - - /** @brief Scales the banner card picture, mirroring the Visual Deck Storage. */ - void setScaleFactor(int scale); - -signals: - void openDeckRequested(int deckId); - -protected: - void resizeEvent(QResizeEvent *event) override; - void keyPressEvent(QKeyEvent *event) override; - -private slots: - void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); - void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance); - void updateColorIdentityVisibility(); - void updateTagsVisibility(); - void updateUploadTimeVisibility(); - -private: - int deckId = 0; - QString baseAccessibleName; - bool hasColorIdentity = false; - bool hasTags = false; - bool hasUploadTime = false; - int lastKnownBannerWidth = 0; - QList fixedWidthChildren; - DeckPreviewCardPictureWidget *bannerCardDisplayWidget; - ColorIdentityWidget *colorIdentityWidget; - FlowWidget *tagsFlowWidget; - QLabel *uploadTimeLabel; -}; - -#endif // PUBLIC_DECK_PREVIEW_WIDGET_H diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/remote_public_decks_model.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/remote_public_decks_model.cpp deleted file mode 100644 index a1cabd729..000000000 --- a/cockatrice/src/interface/widgets/visual_deck_storage/remote_public_decks_model.cpp +++ /dev/null @@ -1,220 +0,0 @@ -#include "remote_public_decks_model.h" - -#include "../../../client/settings/cache_settings.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -RemotePublicDecksModel::RemotePublicDecksModel(AbstractClient *_client, QObject *parent) - : QAbstractListModel(parent), client(_client) -{ - // The ping sweep can drop a pending command without ever emitting finished, - // so loading must not be a latch: time it out and clear it when the client - // goes away, or the tab is stuck on the loading state for the session. - loadingTimeoutTimer = new QTimer(this); - loadingTimeoutTimer->setSingleShot(true); - loadingTimeoutTimer->setInterval( - static_cast((static_cast(SettingsCache::instance().network().getTimeOut()) + 1) * - SettingsCache::instance().network().getKeepAlive() * 1000)); - connect(loadingTimeoutTimer, &QTimer::timeout, this, &RemotePublicDecksModel::onLoadingTimeout); - connect(client, &AbstractClient::statusChanged, this, [this](ClientStatus status) { - if (status == StatusDisconnected) { - loadingTimeoutTimer->stop(); - setLoading(false); - } - }); -} - -int RemotePublicDecksModel::rowCount(const QModelIndex &parent) const -{ - return parent.isValid() ? 0 : visibleIndices.size(); -} - -QVariant RemotePublicDecksModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid() || index.row() < 0 || index.row() >= visibleIndices.size()) { - return QVariant(); - } - if (role == Qt::DisplayRole || role == Qt::ToolTipRole) { - return decks.at(visibleIndices.at(index.row())).name; - } - return QVariant(); -} - -RemotePublicDecksModel::DeckEntry RemotePublicDecksModel::entryAt(int row) const -{ - if (row < 0 || row >= visibleIndices.size()) { - return DeckEntry{}; - } - return decks.at(visibleIndices.at(row)); -} - -void RemotePublicDecksModel::setSearchText(const QString &text) -{ - searchText = text.trimmed(); - rebuildVisibleIndices(); -} - -void RemotePublicDecksModel::setColorFilter(VisualDeckStorageSortFilterProxyModel::FilterMode mode, - const QSet &colors) -{ - colorFilterMode = mode; - activeColors = colors; - rebuildVisibleIndices(); -} - -void RemotePublicDecksModel::setTagFilter(const QSet &selected, const QSet &excluded) -{ - includedTags = selected; - excludedTags = excluded; - rebuildVisibleIndices(); -} - -QSet RemotePublicDecksModel::allTags() const -{ - QSet all; - for (const DeckEntry &entry : decks) { - all.unite(QSet(entry.tags.cbegin(), entry.tags.cend())); - } - return all; -} - -void RemotePublicDecksModel::rebuildVisibleIndices() -{ - QList newIndices; - newIndices.reserve(decks.size()); - for (int row = 0; row < decks.size(); ++row) { - const DeckEntry &entry = decks.at(row); - - if (!searchText.isEmpty() && !entry.name.contains(searchText, Qt::CaseInsensitive)) { - continue; - } - - if (!activeColors.isEmpty()) { - const QString &identity = entry.colorIdentity; - if (!colorIdentityMatches(colorFilterMode, activeColors, identity)) { - continue; - } - } - - if (!includedTags.isEmpty()) { - const QSet entryTags(entry.tags.cbegin(), entry.tags.cend()); - bool hasAll = std::all_of(includedTags.begin(), includedTags.end(), - [&entryTags](const QString &tag) { return entryTags.contains(tag); }); - if (!hasAll) { - continue; - } - } - - if (!excludedTags.isEmpty() && std::any_of(excludedTags.begin(), excludedTags.end(), - [&entry](const QString &tag) { return entry.tags.contains(tag); })) { - continue; - } - - newIndices.append(row); - } - - beginResetModel(); - visibleIndices = newIndices; - endResetModel(); -} - -void RemotePublicDecksModel::refresh(const QString &userName) -{ - if (loading) { - return; - } - // Every refresh captures its own request id so a reply that lands after its - // loading timeout (the reverse of the ping sweep dropping the command) is - // recognised as stale: it must not stop the newer request's timer or paint - // the grid with out-of-date data. - const int seq = ++requestSequence; - setLoading(true); - loadingTimeoutTimer->start(); - Command_DeckListOtherUser cmd; - cmd.set_user_name(userName.toStdString()); - PendingCommand *pend = client->prepareSessionCommand(cmd); - connect(pend, &PendingCommand::finished, this, - [this, seq](const Response &response, const CommandContainer &commandContainer) { - if (seq != requestSequence) { - return; // a newer refresh superseded this one - } - decksReceived(response, commandContainer); - }); - client->sendCommand(pend); -} - -void RemotePublicDecksModel::onLoadingTimeout() -{ - setLoading(false); - emit loadFailed(tr("The server did not respond in time. Try again.")); -} - -void RemotePublicDecksModel::clear() -{ - decks.clear(); - rebuildVisibleIndices(); -} - -void RemotePublicDecksModel::setLoading(bool value) -{ - if (loading == value) { - return; - } - loading = value; - emit loadingChanged(loading); -} - -void RemotePublicDecksModel::decksReceived(const Response &response, const CommandContainer & /*commandContainer*/) -{ - setLoading(false); - loadingTimeoutTimer->stop(); - if (response.response_code() != Response::RespOk) { - emit loadFailed(tr("Failed to load the user's public decks (server response code %1).") - .arg(QString::number(static_cast(response.response_code())))); - return; - } - - const Response_DeckList &resp = response.GetExtension(Response_DeckList::ext); - decks.clear(); - addFolder(resp.root()); - rebuildVisibleIndices(); -} - -void RemotePublicDecksModel::addFolder(const ServerInfo_DeckStorage_Folder &folder) -{ - const int itemCount = folder.items_size(); - for (int i = 0; i < itemCount; ++i) { - addTreeItem(folder.items(i)); - } -} - -void RemotePublicDecksModel::addTreeItem(const ServerInfo_DeckStorage_TreeItem &item) -{ - if (item.has_folder()) { - addFolder(item.folder()); - return; - } - - const ServerInfo_DeckStorage_File &file = item.file(); - DeckEntry entry; - entry.id = item.id(); - entry.name = QString::fromStdString(item.name()); - entry.uploadTime = QDateTime::fromSecsSinceEpoch(file.creation_time()); - entry.bannerCardName = QString::fromStdString(file.banner_card_name()); - entry.bannerCardProvider = QString::fromStdString(file.banner_card_provider()); - entry.colorIdentity = QString::fromStdString(file.color_identity()); - QStringList tags; - for (const auto &tag : file.tags()) { - tags.append(QString::fromStdString(tag)); - } - entry.tags = tags; - decks.append(entry); -} diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/remote_public_decks_model.h b/cockatrice/src/interface/widgets/visual_deck_storage/remote_public_decks_model.h deleted file mode 100644 index eeb606442..000000000 --- a/cockatrice/src/interface/widgets/visual_deck_storage/remote_public_decks_model.h +++ /dev/null @@ -1,129 +0,0 @@ -/** - * @file remote_public_decks_model.h - * @ingroup DeckStorageWidgets - */ - -#ifndef REMOTE_PUBLIC_DECKS_MODEL_H -#define REMOTE_PUBLIC_DECKS_MODEL_H - -#include "visual_deck_storage_sort_filter_proxy_model.h" - -#include -#include -#include -#include -#include - -class AbstractClient; -class CommandContainer; -class QTimer; -class Response; -class ServerInfo_DeckStorage_Folder; -class ServerInfo_DeckStorage_TreeItem; - -/** - * @brief Flat, read-only list of the public decks published by another user. - * - * Fetches the target user's public decks via Command_DeckListOtherUser and - * flattens the response tree into entries carrying the preview metadata stored - * on the server (banner card name/provider and color identity). No deck list is - * downloaded until the user actually opens a deck. - * - * Name and color-identity filtering is applied against this metadata, mirroring - * the Visual Deck Storage's filter semantics, so the grid can be narrowed like - * the local deck storage. - */ -class RemotePublicDecksModel : public QAbstractListModel -{ - Q_OBJECT - -public: - struct DeckEntry - { - int id = 0; - QString name; - QDateTime uploadTime; - QString bannerCardName; - QString bannerCardProvider; - QString colorIdentity; - QStringList tags; - }; - - /** - * @brief The color identity filter mode, shared with the Visual Deck Storage. - */ - using FilterMode = VisualDeckStorageSortFilterProxyModel::FilterMode; - - explicit RemotePublicDecksModel(AbstractClient *client, QObject *parent = nullptr); - - [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; - [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - /** @brief Fetches the public decks of another user, replacing the current contents. */ - void refresh(const QString &userName); - void clear(); - - /** @brief Sets a case-insensitive substring filter on the deck name. */ - void setSearchText(const QString &text); - - /** @brief Sets the active color identity filter and mode. */ - void setColorFilter(FilterMode mode, const QSet &colors); - - /** @brief Filters decks by required (`selected`) and forbidden (`excluded`) tags. */ - void setTagFilter(const QSet &selected, const QSet &excluded); - - /** @brief All tags present across all loaded decks, for building filter chips. */ - [[nodiscard]] QSet allTags() const; - - /** @brief The number of decks after filtering. */ - [[nodiscard]] int filteredCount() const - { - return visibleIndices.size(); - } - - /** @brief The number of decks before filtering. */ - [[nodiscard]] int totalCount() const - { - return decks.size(); - } - - /** @brief True while a refresh request is in flight and the grid has no data yet. */ - [[nodiscard]] bool isLoading() const - { - return loading; - } - - [[nodiscard]] DeckEntry entryAt(int row) const; - -signals: - /** @brief Emitted when a refresh starts, completes, or fails (see loading()). */ - void loadingChanged(bool loading); - - /** @brief Emitted when the last refresh failed; contains a user-facing message. */ - void loadFailed(const QString &message); - -private slots: - void decksReceived(const Response &response, const CommandContainer &commandContainer); - void onLoadingTimeout(); - -private: - void addFolder(const ServerInfo_DeckStorage_Folder &folder); - void addTreeItem(const ServerInfo_DeckStorage_TreeItem &item); - void rebuildVisibleIndices(); - void setLoading(bool value); - - AbstractClient *client; - QTimer *loadingTimeoutTimer; - QList decks; - QList visibleIndices; ///< Row indices into `decks` that pass the current filters. - bool loading = false; - int requestSequence = 0; ///< Monotonically increases per refresh; only the newest request may update the grid. - - QString searchText; - VisualDeckStorageSortFilterProxyModel::FilterMode colorFilterMode = VisualDeckStorageSortFilterProxyModel::Includes; - QSet activeColors; - QSet includedTags; - QSet excludedTags; -}; - -#endif // REMOTE_PUBLIC_DECKS_MODEL_H diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp index 911f3dee9..22d73b604 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp @@ -126,11 +126,6 @@ void VisualDeckStorageFolderDisplayWidget::continueDeckPass() const bool matches = index.data(VisualDeckStorageRoles::FilterMatchRole).toBool(); deckPreviewWidget->setVisible(matches); - if (!matches) { - // A deck that no longer matches the filters is dropped from the selection so its - // highlight cannot linger on an invisible preview or be counted in the share. - deckPreviewWidget->setShareSelected(false); - } if (matches) { ++visibleDeckCount; } @@ -214,11 +209,6 @@ DeckPreviewWidget *VisualDeckStorageFolderDisplayWidget::createDeckPreviewWidget &VisualDeckStorageWidget::deckLoadRequested); connect(deckPreviewWidget, &DeckPreviewWidget::openDeckEditor, visualDeckStorageWidget, &VisualDeckStorageWidget::openDeckEditor); - connect(deckPreviewWidget, &DeckPreviewWidget::shareDeckRequested, visualDeckStorageWidget, - &VisualDeckStorageWidget::shareDeckRequested); - connect(deckPreviewWidget, &DeckPreviewWidget::shareSelectionToggled, visualDeckStorageWidget, - &VisualDeckStorageWidget::shareSelectionChanged); - deckPreviewWidget->setShareSelectable(visualDeckStorageWidget->isShareSelectable()); connect(visualDeckStorageWidget->settings(), &VisualDeckStorageQuickSettingsWidget::cardSizeChanged, deckPreviewWidget->bannerCardDisplayWidget, &CardInfoPictureWidget::setScaleFactor); deckPreviewWidget->bannerCardDisplayWidget->setScaleFactor(visualDeckStorageWidget->settings()->getCardSize()); @@ -226,18 +216,6 @@ DeckPreviewWidget *VisualDeckStorageFolderDisplayWidget::createDeckPreviewWidget return deckPreviewWidget; } -void VisualDeckStorageFolderDisplayWidget::setShareSelectable(bool selectable) -{ - const auto previews = flowWidget->findChildren(); - for (DeckPreviewWidget *preview : previews) { - preview->setShareSelectable(selectable); - } - const auto subFolders = findChildren(); - for (VisualDeckStorageFolderDisplayWidget *subFolder : subFolders) { - subFolder->setShareSelectable(selectable); - } -} - /** * @brief Creates, removes and keeps in sync the subfolder widgets of this folder. * diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.h index 5a81457b0..257ce1778 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.h @@ -51,7 +51,6 @@ public slots: */ void scheduleReconcile(); void updateShowFolders(bool enabled); - void setShareSelectable(bool selectable); signals: /** diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp index daca55fc8..5d0006539 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp @@ -1,7 +1,6 @@ #include "visual_deck_storage_model.h" #include "../../deck_loader/deck_loader.h" -#include "../cards/additional_info/deck_color_identity.h" #include #include @@ -120,6 +119,8 @@ DeckScanResult scanDeckDirectory(const QString &deckPath) } } // namespace +static QString computeColorIdentity(const LoadedDeck &deck); + VisualDeckStorageModel::VisualDeckStorageModel(QObject *parent) : QAbstractListModel(parent) { } @@ -305,7 +306,7 @@ void VisualDeckStorageModel::beginLoad(int row) } // Color identity walks every card through the database, so compute it here to // keep the completion handler on the UI thread cheap. - const QString colorIdentity = getDeckColorIdentity(deck->deckList, CardDatabaseManager::query()); + const QString colorIdentity = computeColorIdentity(*deck); return DeckLoadResult{std::move(*deck), QFileInfo(filePath).lastModified(), colorIdentity}; })); } @@ -366,6 +367,40 @@ void VisualDeckStorageModel::drainPendingLoads() } } +/** + * @brief Computes the color identity of a deck in WUBRG order. + */ +static QString computeColorIdentity(const LoadedDeck &deck) +{ + QStringList cardList = deck.deckList.getCardList({DECK_ZONE_MAIN, DECK_ZONE_SIDE}); + if (cardList.isEmpty()) { + return {}; + } + + QSet colorSet; // A set to collect unique color symbols (e.g., W, U, B, R, G) + + for (const QString &cardName : cardList) { + CardInfoPtr currentCard = CardDatabaseManager::query()->getCardInfo(cardName); + if (currentCard) { + const QString colors = currentCard->getColors(); // Something like "WUB" + for (const QChar &color : colors) { + colorSet.insert(color); + } + } + } + + // Ensure the color identity is in WUBRG order + QString colorIdentity; + const QString wubrgOrder = "WUBRG"; + for (const QChar &color : wubrgOrder) { + if (colorSet.contains(color)) { + colorIdentity.append(color); + } + } + + return colorIdentity; +} + /** * @brief Recomputes all derived metadata of a row from its loaded deck. */ @@ -379,7 +414,7 @@ void VisualDeckStorageModel::recomputeDeckMetadata(DeckPreviewData &data, bool r data.lastLoaded = QDateTime::fromString(deckList.getLastLoadedTimestamp()); data.bannerCard = deckList.getBannerCard(); if (recomputeColorIdentity) { - data.colorIdentity = getDeckColorIdentity(data.deck.deckList, CardDatabaseManager::query()); + data.colorIdentity = computeColorIdentity(data.deck); } } diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp index c19ac0fec..478431703 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp @@ -50,15 +50,6 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg &SettingsCache::instance().visualDeckStorage(), &VisualDeckStorageSettings::setVisualDeckStorageShowTagsOnDeckPreviews); - // show upload time on DeckPreviewWidget checkbox - showUploadTimeCheckBox = new QCheckBox(this); - showUploadTimeCheckBox->setChecked( - SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowUploadTime()); - connect(showUploadTimeCheckBox, &QCheckBox::QT_STATE_CHANGED, this, - &VisualDeckStorageQuickSettingsWidget::showUploadTimeChanged); - connect(showUploadTimeCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().visualDeckStorage(), - &VisualDeckStorageSettings::setVisualDeckStorageShowUploadTime); - // show banner card selector checkbox showBannerCardComboBoxCheckBox = new QCheckBox(this); showBannerCardComboBoxCheckBox->setChecked( @@ -103,7 +94,7 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg unusedColorIdentityOpacityLayout->addWidget(unusedColorIdentitiesOpacitySpinBox); // tooltip selector - deckPreviewTooltipWidget = new QWidget(this); + auto deckPreviewTooltipWidget = new QWidget(this); deckPreviewTooltipLabel = new QLabel(deckPreviewTooltipWidget); deckPreviewTooltipComboBox = new QComboBox(deckPreviewTooltipWidget); @@ -137,7 +128,6 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg this->addSettingsWidget(showTagFilterCheckBox); this->addSettingsWidget(showColorIdentityCheckBox); this->addSettingsWidget(showTagsOnDeckPreviewsCheckBox); - this->addSettingsWidget(showUploadTimeCheckBox); this->addSettingsWidget(showBannerCardComboBoxCheckBox); this->addSettingsWidget(drawUnusedColorIdentitiesCheckBox); this->addSettingsWidget(unusedColorIdentityOpacityWidget); @@ -155,7 +145,6 @@ void VisualDeckStorageQuickSettingsWidget::retranslateUi() showTagFilterCheckBox->setText(tr("Show Tag Filter")); showColorIdentityCheckBox->setText(tr("Show Color Identity")); showTagsOnDeckPreviewsCheckBox->setText(tr("Show Tags On Deck Previews")); - showUploadTimeCheckBox->setText(tr("Show Upload Time")); showBannerCardComboBoxCheckBox->setText(tr("Show Banner Card Selection Option")); drawUnusedColorIdentitiesCheckBox->setText(tr("Draw unused Color Identities")); unusedColorIdentitiesOpacityLabel->setText(tr("Unused Color Identities Opacity")); @@ -166,14 +155,6 @@ void VisualDeckStorageQuickSettingsWidget::retranslateUi() deckPreviewTooltipComboBox->setItemText(1, tr("Filepath")); } -void VisualDeckStorageQuickSettingsWidget::setPublicDecksMode(bool enabled) -{ - const bool hidden = enabled; - showFoldersCheckBox->setVisible(!hidden); - showBannerCardComboBoxCheckBox->setVisible(!hidden); - deckPreviewTooltipWidget->setVisible(!hidden); -} - bool VisualDeckStorageQuickSettingsWidget::getShowFolders() const { return showFoldersCheckBox->isChecked(); @@ -204,11 +185,6 @@ bool VisualDeckStorageQuickSettingsWidget::getShowTagsOnDeckPreviews() const return showTagsOnDeckPreviewsCheckBox->isChecked(); } -bool VisualDeckStorageQuickSettingsWidget::getShowUploadTime() const -{ - return showUploadTimeCheckBox->isChecked(); -} - int VisualDeckStorageQuickSettingsWidget::getUnusedColorIdentitiesOpacity() const { return unusedColorIdentitiesOpacitySpinBox->value(); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.h index fc250cb20..ea4330a15 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.h @@ -27,12 +27,10 @@ class VisualDeckStorageQuickSettingsWidget : public SettingsButtonWidget QCheckBox *showBannerCardComboBoxCheckBox; QCheckBox *showTagFilterCheckBox; QCheckBox *showTagsOnDeckPreviewsCheckBox; - QCheckBox *showUploadTimeCheckBox; QLabel *unusedColorIdentitiesOpacityLabel; QSpinBox *unusedColorIdentitiesOpacitySpinBox; QLabel *deckPreviewTooltipLabel; QComboBox *deckPreviewTooltipComboBox; - QWidget *deckPreviewTooltipWidget; CardSizeWidget *cardSizeWidget; public: @@ -48,15 +46,6 @@ public: explicit VisualDeckStorageQuickSettingsWidget(QWidget *parent = nullptr); - /** - * @brief Hides the controls that do not apply to the public decks tab. - * - * The public decks tab reuses this widget for its quick settings menu but - * has no folders, banner selection or per-deck tooltip, so those controls - * are hidden while every shared key keeps syncing with SettingsCache. - */ - void setPublicDecksMode(bool enabled); - void retranslateUi(); [[nodiscard]] bool getShowFolders() const; @@ -65,7 +54,6 @@ public: [[nodiscard]] bool getShowBannerCardComboBox() const; [[nodiscard]] bool getShowTagFilter() const; [[nodiscard]] bool getShowTagsOnDeckPreviews() const; - [[nodiscard]] bool getShowUploadTime() const; [[nodiscard]] int getUnusedColorIdentitiesOpacity() const; [[nodiscard]] TooltipType getDeckPreviewTooltip() const; [[nodiscard]] int getCardSize() const; @@ -77,7 +65,6 @@ signals: void showBannerCardComboBoxChanged(bool enabled); void showTagFilterChanged(bool enabled); void showTagsOnDeckPreviewsChanged(bool enabled); - void showUploadTimeChanged(bool enabled); void unusedColorIdentitiesOpacityChanged(int opacity); void deckPreviewTooltipChanged(TooltipType tooltip); void cardSizeChanged(int scale); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp index 4b3a1ac29..c05da1cb3 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.cpp @@ -11,34 +11,6 @@ VisualDeckStorageSortFilterProxyModel::VisualDeckStorageSortFilterProxyModel(QOb setDynamicSortFilter(false); } -bool colorIdentityMatches(VisualDeckStorageSortFilterProxyModel::FilterMode mode, - const QSet &colors, - const QString &identity) -{ - switch (mode) { - case VisualDeckStorageSortFilterProxyModel::ExactMatch: { - QSet activeColorSet; - for (const QChar &color : colors) { - activeColorSet.insert(color.toUpper()); - } - - QSet colorIdentitySet; - for (const QChar &color : identity) { - colorIdentitySet.insert(color.toUpper()); - } - - return activeColorSet == colorIdentitySet; - } - case VisualDeckStorageSortFilterProxyModel::Includes: - return std::all_of(colors.begin(), colors.end(), - [&identity](const QChar &color) { return identity.contains(color); }); - case VisualDeckStorageSortFilterProxyModel::Excludes: - return std::none_of(colors.begin(), colors.end(), - [&identity](const QChar &color) { return identity.contains(color); }); - } - return false; -} - void VisualDeckStorageSortFilterProxyModel::setSourceModel(QAbstractItemModel *model) { if (QAbstractItemModel *oldModel = sourceModel()) { @@ -283,7 +255,34 @@ void VisualDeckStorageSortFilterProxyModel::updateColorMatches() for (int row = 0; row < count; ++row) { const QString colorIdentity = source->dataForRow(row).colorIdentity; - colorMatches[row] = colorIdentityMatches(colorFilterMode, activeColors, colorIdentity); + + bool matches = true; + switch (colorFilterMode) { + case ExactMatch: { + QSet activeColorSet; + for (const QChar &color : activeColors) { + activeColorSet.insert(color.toUpper()); + } + + QSet colorIdentitySet; + for (const QChar &color : colorIdentity) { + colorIdentitySet.insert(color.toUpper()); + } + + matches = activeColorSet == colorIdentitySet; + break; + } + case Includes: + matches = std::all_of(activeColors.begin(), activeColors.end(), + [&colorIdentity](const QChar &color) { return colorIdentity.contains(color); }); + break; + case Excludes: + matches = std::none_of(activeColors.begin(), activeColors.end(), + [&colorIdentity](const QChar &color) { return colorIdentity.contains(color); }); + break; + } + + colorMatches[row] = matches; } } diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.h index a6c40a2d7..7e771f6a9 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_sort_filter_proxy_model.h @@ -102,14 +102,4 @@ private: QList colorMatches; ///< Per-row color identity match. }; -/** - * @brief Whether an identity string matches the active color-identity filter. - * - * The single source of truth for the color identity matching rule, shared by - * the Visual Deck Storage proxy and the remote public decks model. - */ -[[nodiscard]] bool colorIdentityMatches(VisualDeckStorageSortFilterProxyModel::FilterMode mode, - const QSet &colors, - const QString &identity); - #endif // VISUAL_DECK_STORAGE_SORT_FILTER_PROXY_MODEL_H diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp index 6f954b01b..ba52cf8e9 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp @@ -2,10 +2,14 @@ #include "../general/layout_containers/flow_widget.h" #include "deck_preview/deck_preview_tag_display_widget.h" +#include "visual_deck_storage_model.h" +#include "visual_deck_storage_sort_filter_proxy_model.h" +#include "visual_deck_storage_widget.h" #include -VisualDeckStorageTagFilterWidget::VisualDeckStorageTagFilterWidget(QWidget *parent) : QWidget(parent) +VisualDeckStorageTagFilterWidget::VisualDeckStorageTagFilterWidget(VisualDeckStorageWidget *_parent) + : QWidget(_parent), parent(_parent) { setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); @@ -21,72 +25,99 @@ VisualDeckStorageTagFilterWidget::VisualDeckStorageTagFilterWidget(QWidget *pare layout->addWidget(flowWidget); } -void VisualDeckStorageTagFilterWidget::setAllTagsProvider(const std::function()> &provider) -{ - allTagsProvider = provider; -} - void VisualDeckStorageTagFilterWidget::showEvent(QShowEvent *event) { QWidget::showEvent(event); refreshTags(); } +/** + * @brief The tags of all decks currently accepted by the proxy model. + */ +QSet VisualDeckStorageTagFilterWidget::gatherAllTags() const +{ + QSet allTags; + auto *proxy = parent->proxyModel(); + + for (int proxyRow = 0; proxyRow < proxy->rowCount(); ++proxyRow) { + const QModelIndex index = proxy->index(proxyRow, 0); + if (!index.data(VisualDeckStorageRoles::FilterMatchRole).toBool()) { + continue; + } + const QStringList deckTags = index.data(VisualDeckStorageRoles::TagsRole).toStringList(); + for (const QString &tag : deckTags) { + allTags.insert(tag); + } + } + + return allTags; +} + void VisualDeckStorageTagFilterWidget::refreshTags() { - const QSet allTags = allTagsProvider ? allTagsProvider() : QSet(); + QSet allTags = gatherAllTags(); + removeTagsNotInList(allTags); + addTagsIfNotPresent(allTags); + sortTags(); +} - // Existing chips survive if their tag is still part of the deck set, or if the chip - // is currently selected/excluded. Everything else is dropped. Dropped chips must NOT - // be re-added to the layout afterwards: they are reparented to nullptr and scheduled - // for a deferred delete (QWidget::setParent(nullptr) also hides them), and the flow - // layout would keep a dangling reference to them once the deletion runs on the next - // event-loop cycle. - QList chips; +void VisualDeckStorageTagFilterWidget::removeTagsNotInList(const QSet &tags) +{ for (DeckPreviewTagDisplayWidget *tagWidget : findChildren()) { - if (tagWidget->getState() != TagState::NotSelected || allTags.contains(tagWidget->getTagName())) { - chips.append(tagWidget); - } else { + const QString &tagName = tagWidget->getTagName(); + + // Keep the tag widget if it is either selected or excluded + if (!tags.contains(tagName) && tagWidget->getState() == TagState::NotSelected) { flowWidget->removeWidget(tagWidget); - tagWidget->setParent(nullptr); tagWidget->deleteLater(); } } +} - // Add chips for tags that are not shown yet. - QSet existingTags; - for (DeckPreviewTagDisplayWidget *tagWidget : chips) { - existingTags.insert(tagWidget->getTagName()); +void VisualDeckStorageTagFilterWidget::addTagsIfNotPresent(const QSet &tags) +{ + for (const QString &tag : tags) { + addTagIfNotPresent(tag); } - for (const QString &tag : allTags) { - if (!existingTags.contains(tag)) { - auto *newTagWidget = new DeckPreviewTagDisplayWidget(this, tag); - connect(newTagWidget, &DeckPreviewTagDisplayWidget::tagClicked, this, - &VisualDeckStorageTagFilterWidget::filterChanged); - flowWidget->addWidget(newTagWidget); - chips.append(newTagWidget); +} + +void VisualDeckStorageTagFilterWidget::addTagIfNotPresent(const QString &tag) +{ + // Check if the tag already exists in the flow widget + bool tagExists = false; + for (DeckPreviewTagDisplayWidget *tagWidget : findChildren()) { + if (tagWidget->getTagName() == tag) { + tagExists = true; + break; } } - // Sort, but skip the full remove/re-add when the order already matches the layout. - // FlowWidget inherits QLayout::removeWidget's linear scan, so rebuilding an unchanged - // order would be quadratic plus a full relayout on every chip click and load batch. - std::sort(chips.begin(), chips.end(), [](DeckPreviewTagDisplayWidget *a, DeckPreviewTagDisplayWidget *b) { - const QString aName = a->getTagName(); - const QString bName = b->getTagName(); - const int compared = aName.compare(bName, Qt::CaseInsensitive); - return compared != 0 ? compared < 0 : aName < bName; - }); - if (chips == currentChipOrder) { - return; + // If the tag doesn't exist, add a new DeckPreviewTagDisplayWidget + if (!tagExists) { + auto *newTagWidget = new DeckPreviewTagDisplayWidget(this, tag); + connect(newTagWidget, &DeckPreviewTagDisplayWidget::tagClicked, parent, + &VisualDeckStorageWidget::updateTagFilter); + flowWidget->addWidget(newTagWidget); } - for (DeckPreviewTagDisplayWidget *tagWidget : chips) { +} + +void VisualDeckStorageTagFilterWidget::sortTags() +{ + // Get all tag widgets + QList tagWidgets = findChildren(); + + // Sort widgets by tag name + std::sort(tagWidgets.begin(), tagWidgets.end(), [](DeckPreviewTagDisplayWidget *a, DeckPreviewTagDisplayWidget *b) { + return a->getTagName().toLower() < b->getTagName().toLower(); + }); + + // Clear and re-add widgets in sorted order + for (DeckPreviewTagDisplayWidget *tagWidget : tagWidgets) { flowWidget->removeWidget(tagWidget); } - for (DeckPreviewTagDisplayWidget *tagWidget : chips) { + for (DeckPreviewTagDisplayWidget *tagWidget : tagWidgets) { flowWidget->addWidget(tagWidget); } - currentChipOrder = chips; } QStringList VisualDeckStorageTagFilterWidget::selectedTags() const diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.h index 5e3cb398c..337c053c7 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.h @@ -9,28 +9,26 @@ #include #include #include -#include -class DeckPreviewTagDisplayWidget; class FlowWidget; - +class VisualDeckStorageWidget; class VisualDeckStorageTagFilterWidget : public QWidget { Q_OBJECT + VisualDeckStorageWidget *parent; FlowWidget *flowWidget; - std::function()> allTagsProvider; - QList currentChipOrder; + + [[nodiscard]] QSet gatherAllTags() const; + void removeTagsNotInList(const QSet &tags); + void addTagsIfNotPresent(const QSet &tags); + void addTagIfNotPresent(const QString &tag); + void sortTags(); public: - explicit VisualDeckStorageTagFilterWidget(QWidget *parent = nullptr); + explicit VisualDeckStorageTagFilterWidget(VisualDeckStorageWidget *_parent); [[nodiscard]] QStringList getAllKnownTags() const; - /** - * @brief Sets a provider for the full set of tags to draw chips from. - */ - void setAllTagsProvider(const std::function()> &provider); - /** * @brief The tags currently in "selected" state. */ @@ -41,18 +39,9 @@ public: */ [[nodiscard]] QStringList excludedTags() const; -signals: - /** - * Emitted when a chip's selection or exclusion state changes. - * - * The chip only emits when its state actually changed, so this fires once per - * effective toggle rather than on every click. - */ - void filterChanged(); - public slots: /** - * @brief Rebuilds the tag chips from the currently available tags. + * @brief Rebuilds the tag chips from the tags of the currently visible decks. */ void refreshTags(); void showEvent(QShowEvent *event) override; diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp index bd167ec5b..da7ddc368 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp @@ -48,12 +48,6 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare refreshButton->setFixedSize(32, 32); connect(refreshButton, &QPushButton::clicked, this, &VisualDeckStorageWidget::refreshIfPossible); - shareButton = new QToolButton(this); - shareButton->setIcon(themePixmap(QStringLiteral("icons/share"))); - shareButton->setFixedSize(32, 32); - shareButton->setVisible(false); - connect(shareButton, &QPushButton::clicked, this, &VisualDeckStorageWidget::shareRequested); - quickSettingsWidget = new VisualDeckStorageQuickSettingsWidget(this); connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::showFoldersChanged, this, &VisualDeckStorageWidget::updateShowFolders); @@ -64,14 +58,10 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare searchAndSortLayout->addWidget(sortWidget); searchAndSortLayout->addWidget(searchWidget); searchAndSortLayout->addWidget(refreshButton); - searchAndSortLayout->addWidget(shareButton); searchAndSortLayout->addWidget(quickSettingsWidget); // tag filter box tagFilterWidget = new VisualDeckStorageTagFilterWidget(this); - tagFilterWidget->setAllTagsProvider([this] { return gatherVisibleTags(); }); - connect(tagFilterWidget, &VisualDeckStorageTagFilterWidget::filterChanged, this, - &VisualDeckStorageWidget::updateTagFilter); updateTagsVisibility(SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowTagFilter()); deckPreviewSelectionAnimationEnabled = @@ -165,71 +155,11 @@ void VisualDeckStorageWidget::retranslateUi() databaseLoadIndicator->setText(tr("Loading database ...")); refreshButton->setToolTip(tr("Refresh loaded files")); - shareButton->setToolTip(tr("Select decks to share")); quickSettingsWidget->setToolTip(tr("Visual Deck Storage Settings")); sortWidget->retranslateUi(); } -void VisualDeckStorageWidget::setShareSelectable(bool selectable) -{ - if (shareSelectable == selectable) { - return; - } - shareSelectable = selectable; - if (folderWidget != nullptr) { - folderWidget->setShareSelectable(selectable); - } - emit shareSelectionChanged(); -} - -bool VisualDeckStorageWidget::isShareSelectable() const -{ - return shareSelectable; -} - -QStringList VisualDeckStorageWidget::selectedFilePaths() const -{ - QStringList selectedPaths; - if (folderWidget != nullptr) { - const auto previews = folderWidget->findChildren(); - for (DeckPreviewWidget *preview : previews) { - // Filtered-out previews stay alive hidden in their sorted place, so only decks the - // user can actually see are part of the share. - if (preview->isVisible() && preview->isShareSelected()) { - selectedPaths.append(preview->filePath); - } - } - } - return selectedPaths; -} - -void VisualDeckStorageWidget::clearShareSelection() -{ - if (folderWidget != nullptr) { - const auto previews = folderWidget->findChildren(); - for (DeckPreviewWidget *preview : previews) { - preview->setShareSelected(false); - } - } -} - -void VisualDeckStorageWidget::setShareAvailable(bool available) -{ - shareButton->setVisible(available); - shareButton->setEnabled(available); -} - -void VisualDeckStorageWidget::setShareSelectedFiles(const QStringList &paths) -{ - if (folderWidget != nullptr) { - const auto previews = folderWidget->findChildren(); - for (DeckPreviewWidget *preview : previews) { - preview->setShareSelected(paths.contains(preview->filePath)); - } - } -} - /** * Gets a const pointer to the quick settings so that the values can be accessed. */ @@ -287,25 +217,6 @@ void VisualDeckStorageWidget::updateTagFilter() tagFilterWidget->refreshTags(); } -/** - * @brief The tags of all decks currently accepted by the proxy model. - */ -QSet VisualDeckStorageWidget::gatherVisibleTags() const -{ - QSet allTags; - for (int proxyRow = 0; proxyRow < storageProxyModel->rowCount(); ++proxyRow) { - const QModelIndex index = storageProxyModel->index(proxyRow, 0); - if (!index.data(VisualDeckStorageRoles::FilterMatchRole).toBool()) { - continue; - } - const QStringList deckTags = index.data(VisualDeckStorageRoles::TagsRole).toStringList(); - for (const QString &tag : deckTags) { - allTags.insert(tag); - } - } - return allTags; -} - /** * Pushes the color identity filter widget's state into the proxy model. */ diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h index dfb715bea..fe6389414 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.h @@ -33,12 +33,6 @@ public: explicit VisualDeckStorageWidget(QWidget *parent); void refreshIfPossible(); void retranslateUi(); - void setShareSelectable(bool selectable); - [[nodiscard]] bool isShareSelectable() const; - [[nodiscard]] QStringList selectedFilePaths() const; - void setShareSelectedFiles(const QStringList &paths); - void clearShareSelection(); - void setShareAvailable(bool available); VisualDeckStorageTagFilterWidget *tagFilterWidget; bool deckPreviewSelectionAnimationEnabled; @@ -69,9 +63,6 @@ public slots: signals: void deckLoadRequested(const QString &filePath); void openDeckEditor(const LoadedDeck &deck); - void shareDeckRequested(const QString &filePath); - void shareSelectionChanged(); - void shareRequested(); protected: void resizeEvent(QResizeEvent *event) override; @@ -79,7 +70,6 @@ protected: private: void reapplySortAndFilters(); - [[nodiscard]] QSet gatherVisibleTags() const; private: QVBoxLayout *layout; @@ -90,14 +80,12 @@ private: VisualDeckStorageSearchWidget *searchWidget; DeckPreviewColorIdentityFilterWidget *deckPreviewColorIdentityFilterWidget; QToolButton *refreshButton; - QToolButton *shareButton; VisualDeckStorageQuickSettingsWidget *quickSettingsWidget; QScrollArea *scrollArea; VisualDeckStorageFolderDisplayWidget *folderWidget = nullptr; VisualDeckStorageModel *storageModel = nullptr; VisualDeckStorageSortFilterProxyModel *storageProxyModel = nullptr; QTimer *refreshTimer = nullptr; ///< Coalesces the re-apply/refresh burst following a batch of deck loads. - bool shareSelectable = false; }; #endif // VISUAL_DECK_STORAGE_WIDGET_H diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index 595d38d5b..3daaeb8d3 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -33,7 +33,6 @@ #include "../interface/widgets/dialogs/dlg_update.h" #include "../interface/widgets/dialogs/dlg_view_log.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_server.h" #include "../interface/widgets/tabs/tab_supervisor.h" @@ -247,8 +246,6 @@ void MainWindow::actFullScreen(bool checked) void MainWindow::actSettings() { DlgSettings dlg(this); - auto *generalPage = qobject_cast(dlg.page(DlgSettings::GeneralPage)); - connect(generalPage, &GeneralSettingsPage::cardDatabaseUpdateRequested, this, &MainWindow::actCheckCardUpdates); dlg.exec(); } @@ -512,7 +509,6 @@ MainWindow::MainWindow(QWidget *parent) connectionController = new ConnectionController(this, this); urlParser = new IntentUrlParser(this, this); - connect(urlParser, &IntentUrlParser::urlChainFinished, this, &MainWindow::onUrlChainFinished); createActions(); createMenus(); @@ -708,12 +704,6 @@ void MainWindow::applyStartupDestination() return; } - // A cockatrice:// link owns the startup connection while its chain runs; - // connecting here would race (and tear down) the link's own connection. - if (skipStartupAutoConnect) { - return; - } - const int destination = SettingsCache::instance().tabs().getStartupTabIndex(); if (destination != StartupTab::StartupTabServer && destination != StartupTab::StartupTabServerRoom) { return; @@ -735,7 +725,6 @@ void MainWindow::applyStartupDestination() connect(credentials, &Intent::finished, connector, &Intent::execute); connect(credentials, &Intent::failed, this, &MainWindow::startupDestinationFailed); - connect(credentials, &Intent::cancelled, this, [this]() { startupDestinationFailed(tr("Sign-in cancelled")); }); connect(connector, &Intent::finished, this, [this, destination, serverContext]() { onStartupDestinationConnected(destination, *serverContext); }); connect(connector, &Intent::failed, this, &MainWindow::startupDestinationFailed); @@ -887,7 +876,18 @@ void MainWindow::changeEvent(QEvent *event) } else if (event->type() == QEvent::ActivationChange) { if (isActiveWindow() && !bHasActivated) { bHasActivated = true; - attemptStartupAutoConnect(); + if (!connectTo.isEmpty()) { + qCInfo(WindowMainStartupAutoconnectLog) << "Command line connect to " << connectTo; + connectionController->connectToServerDirect(connectTo.host(), connectTo.port(), connectTo.userName(), + connectTo.password()); + } else if (SettingsCache::instance().servers().getAutoConnect() && + !SettingsCache::instance().debug().getLocalGameOnStartup() && + !startupDestinationConnectsToServer()) { + qCInfo(WindowMainStartupAutoconnectLog) << "Attempting auto-connect..."; + DlgConnect dlg(this); + connectionController->connectToServerDirect(dlg.getHost(), static_cast(dlg.getPort()), + dlg.getPlayerName(), dlg.getPassword()); + } } } @@ -913,59 +913,6 @@ void MainWindow::handleCockatriceLink(const QString &url) urlParser->handle(url); } -void MainWindow::attemptStartupAutoConnect() -{ - if (startupAutoConnectAttempted || skipStartupAutoConnect) { - return; - } - startupAutoConnectAttempted = true; - - if (!connectTo.isEmpty()) { - qCInfo(WindowMainStartupAutoconnectLog) << "Command line connect to " << connectTo; - connectionController->connectToServerDirect(connectTo.host(), connectTo.port(), connectTo.userName(), - connectTo.password()); - } else if (SettingsCache::instance().servers().getAutoConnect() && - !SettingsCache::instance().debug().getLocalGameOnStartup() && !startupDestinationConnectsToServer()) { - qCInfo(WindowMainStartupAutoconnectLog) << "Attempting auto-connect..."; - DlgConnect dlg(this); - connectionController->connectToServerDirect(dlg.getHost(), static_cast(dlg.getPort()), - dlg.getPlayerName(), dlg.getPassword()); - } -} - -void MainWindow::onUrlChainFinished(bool connected) -{ - // A cockatrice:// link owns the startup connection while it runs. When its - // chain ended without connecting (declined, invalid, offline), fall back to - // the startup connection so the activation launch still behaves like a - // normal launch. - if (connected) { - // The launch link connected, so the startup fallback has served its - // purpose: drop the skip so a later mid-session link that ends declined - // or offline cannot silently fire auto-connect or applyStartupDestination - // again. - skipStartupAutoConnect = false; - return; - } - - if (!skipStartupAutoConnect || getRemoteClient()->getStatus() != StatusDisconnected) { - return; - } - - if (startupDestinationConnectsToServer()) { - // Users whose startup tab is a Server / Server Room connect through the - // startup destination, not through auto-connect; retry that instead. - qCInfo(WindowMainStartupAutoconnectLog) << "URL chain ended without a connection; retrying startup destination"; - skipStartupAutoConnect = false; - applyStartupDestination(); - return; - } - - qCInfo(WindowMainStartupAutoconnectLog) << "URL chain ended without a connection; retrying startup connect"; - skipStartupAutoConnect = false; - attemptStartupAutoConnect(); -} - void MainWindow::cardDatabaseLoadingFailed() { if (askedForDbUpdater) { diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h index 9e45a4c3e..08481fd36 100644 --- a/cockatrice/src/interface/window_main.h +++ b/cockatrice/src/interface/window_main.h @@ -80,7 +80,6 @@ public slots: void actCheckClientUpdates(); void actConnect(); void actExit(); - void handleCockatriceLink(const QString &url); private slots: void updateTabMenu(const QList &newMenuList); void statusChanged(ClientStatus _status); @@ -98,7 +97,7 @@ private slots: void actOpenSettingsFolder(); void actShow(); void showWindowIfHidden(); - void onUrlChainFinished(bool connected); + void handleCockatriceLink(const QString &url); void cardUpdateError(QProcess::ProcessError err); void cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus); @@ -127,8 +126,6 @@ private slots: void startupDestinationFailed(const QString &reason); [[nodiscard]] bool startupDestinationConnectsToServer() const; - void attemptStartupAutoConnect(); - private: static const QString appName; static const QStringList fileNameFilters; @@ -167,8 +164,6 @@ private: LagMonitor lagMonitor; ///< watches the main thread for event loop stalls LatencyStatusWidget *latencyStatus = nullptr; ///< status bar widget with live round-trip stats and history graph bool bHasActivated, askedForDbUpdater; - bool skipStartupAutoConnect = false; - bool startupAutoConnectAttempted = false; QProcess *cardUpdateProcess; QByteArray cardUpdateOutputBuffer; DlgViewLog *logviewDialog; @@ -182,16 +177,6 @@ public: { connectTo = QUrl(QString("cockatrice://%1").arg(url)); } - // When set, the window's own startup connection (--connect or auto-connect - // on first activation) is skipped. Used for activation launches: the intent - // chain triggered by a cockatrice:// URL owns the connection, and letting - // auto-connect race against it caused two connectToServer calls to tear - // each other down. onUrlChainFinished() clears this and retries the startup - // connection when the link's chain ended without connecting. - void setSkipStartupAutoConnect(bool skip) - { - skipStartupAutoConnect = skip; - } ~MainWindow() override; RemoteClient *getRemoteClient() const diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index b9c30d1ad..829e08742 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -26,6 +26,7 @@ #include "client/url_scheme_event_filter.h" #include "database/interface/settings_card_preference_provider.h" #include "interface/intents/intent_open_local_deck.h" +#include "interface/intents/url_parser.h" #include "interface/logger.h" #include "interface/pixel_map_generator.h" #include "interface/theme_manager.h" @@ -44,8 +45,6 @@ #include #include #include -#include -#include #include #include #include @@ -178,18 +177,6 @@ QString const generateClientID() return strClientID; } -static QString redactActivationUrl(const QString &url) -{ - // Activation URLs carry secrets in their query string (e.g. the deck share - // token); log only the scheme and the action (cockatrice://opendeck), never - // the parameters. - if (!url.startsWith(QStringLiteral("cockatrice://"))) { - return url; - } - const QUrl parsed(url); - return parsed.scheme() + "://" + parsed.host(); -} - int main(int argc, char *argv[]) { #ifdef Q_OS_WIN @@ -286,17 +273,10 @@ int main(int argc, char *argv[]) SingleInstanceManager instance; if (hasActivationFiles) { - QStringList redactedFiles; - redactedFiles.reserve(startupFiles.size()); - for (const QString &file : startupFiles) { - redactedFiles.append(redactActivationUrl(file)); - } - qCInfo(MainLog) << "Activation launch, files:" << redactedFiles; // Activation launch: hand off to the primary instance if one is // running, otherwise become the primary ourselves. Do this before // constructing the main window so a hand-off exits cheaply. if (!instance.tryRun(startupFiles)) { - qInfo() << "Handed off to a running instance, exiting"; // Sent successfully → exit return 0; } @@ -345,30 +325,10 @@ int main(int argc, char *argv[]) MainWindow ui; - // A URL launch must own the connection: the intent chain triggered by the - // URL connects to the server named in the URL, so the window's own startup - // auto-connect must not race against it (two connectToServer calls tear - // each other down via doDisconnectFromServer). - bool hasUrlActivation = std::any_of(startupFiles.begin(), startupFiles.end(), [](const QString &file) { - return file.startsWith(QStringLiteral("cockatrice://")); - }); -#ifdef Q_OS_MAC - // On macOS the launch can arrive through the URL scheme instead of as a - // positional argument (captured in pendingMacUrls); count those too or the - // window would auto-connect into the link's own connection attempt. - hasUrlActivation = hasUrlActivation || - std::any_of(pendingMacUrls.cbegin(), pendingMacUrls.cend(), - [](const QString &url) { return url.startsWith(QStringLiteral("cockatrice://")); }); -#endif - ui.setSkipStartupAutoConnect(hasUrlActivation); - auto handleActivation = [&ui](const QString &file) { if (file.startsWith("cockatrice://")) { - qCInfo(MainLog) << "Handling URL activation:" << redactActivationUrl(file); - // Route through the window's persistent url parser: it serializes - // link chains so activations handed over while another chain is - // still connecting do not connect concurrently. - ui.handleCockatriceLink(file); + auto urlParser = new IntentUrlParser(&ui, &ui); + urlParser->handle(file); } else if (QFileInfo(file).exists()) { auto openDeckIntent = new IntentOpenLocalDeck(ui.getTabSupervisor(), file); QObject::connect(openDeckIntent, &Intent::failed, &ui, [&ui](const QString &reason) { diff --git a/cockatrice/src/single_instance_manager.cpp b/cockatrice/src/single_instance_manager.cpp index eff0e6586..aca23160c 100644 --- a/cockatrice/src/single_instance_manager.cpp +++ b/cockatrice/src/single_instance_manager.cpp @@ -2,14 +2,6 @@ #include -namespace -{ -// Sent by the primary instance after it has read a forwarded payload. Without -// an acknowledgment, a second instance cannot tell a live primary apart from a -// stale socket left behind by a process that is still shutting down. -const QByteArray ACK_MESSAGE = QByteArrayLiteral("COCKATRICE_ACK"); -} // namespace - SingleInstanceManager::SingleInstanceManager(QObject *parent) : QObject(parent) { } @@ -28,15 +20,9 @@ bool SingleInstanceManager::tryRun(const QStringList &filesToSend) } serverName = QStringLiteral("CockatriceSingleInstance-%1").arg(userName); - // Hand off to an already-running primary instance if one exists. Never steal - // the socket of a busy primary: it is alive and will act on the payload. - switch (forwardToPrimary(filesToSend)) { - case ForwardResult::Delivered: - return false; - case ForwardResult::PrimaryBusy: - return false; - case ForwardResult::NoPrimary: - break; + // Hand off to an already-running primary instance if one exists. + if (forwardToPrimary(filesToSend)) { + return false; } // No primary instance is currently reachable, so become the primary. @@ -49,18 +35,12 @@ bool SingleInstanceManager::tryRun(const QStringList &filesToSend) // Another instance may have started while we were probing; hand off to it // instead of stealing its socket. - switch (forwardToPrimary(filesToSend)) { - case ForwardResult::Delivered: - return false; - case ForwardResult::PrimaryBusy: - return false; - case ForwardResult::NoPrimary: - break; + if (forwardToPrimary(filesToSend)) { + return false; } - // The socket is stale (left over by a crashed instance), so no primary is - // holding it: remove it and retry. If that still fails, another instance - // just took the name. + // The socket is stale (left over by a crashed instance): remove it and + // retry. If that still fails, another instance just took the name. QLocalServer::removeServer(serverName); if (server->listen(serverName)) { return true; @@ -70,12 +50,12 @@ bool SingleInstanceManager::tryRun(const QStringList &filesToSend) return false; } -SingleInstanceManager::ForwardResult SingleInstanceManager::forwardToPrimary(const QStringList &filesToSend) +bool SingleInstanceManager::forwardToPrimary(const QStringList &filesToSend) { QLocalSocket socket; socket.connectToServer(serverName); if (!socket.waitForConnected(200)) { - return ForwardResult::NoPrimary; + return false; } // Serialize payload with length prefix @@ -92,23 +72,7 @@ SingleInstanceManager::ForwardResult SingleInstanceManager::forwardToPrimary(con socket.flush(); socket.waitForBytesWritten(1000); - // A plain launch has nothing for the primary to act on, so there is nothing - // to acknowledge. Waiting here would block the new instance for seconds if - // the primary is busy in a modal dialog, so only the activation path (which - // needs the ACK to avoid stealing a live primary's socket) waits below. - if (filesToSend.isEmpty()) { - return ForwardResult::Delivered; - } - - // Only report a successful hand-off once the primary has acknowledged that - // it actually read the payload. A socket that connects but is still working - // on an earlier payload is alive but busy, not dead: give it more room - // before giving up, so a slow handler does not make a live primary look - // dead (which would lead to stealing its socket). - if (!socket.waitForReadyRead(1000) && !socket.waitForReadyRead(4000)) { - return ForwardResult::PrimaryBusy; - } - return socket.readAll() == ACK_MESSAGE ? ForwardResult::Delivered : ForwardResult::PrimaryBusy; + return true; } void SingleInstanceManager::handleNewConnection() @@ -147,23 +111,12 @@ void SingleInstanceManager::handleNewConnection() QStringList files; payloadStream >> files; - // Acknowledge receipt as soon as the payload is parsed, before the - // primary starts handling it. The handlers run synchronously and can - // take longer than the sender's readiness timeout (e.g. a modal - // confirmation box), which would otherwise make a live primary look - // dead and cause duplicate handling. - socket->write(ACK_MESSAGE); - socket->flush(); + emit filesReceived(files); - // Drop the payload from the buffer before handling it: the handlers - // run synchronously and can spin a nested event loop (e.g. a modal - // dialog) that re-reads this socket, which would re-parse and re-emit - // the same files. + // Reset buffer (single message use-case) buffer->clear(); *expectedSize = 0; - emit filesReceived(files); - socket->disconnectFromServer(); return; } diff --git a/cockatrice/src/single_instance_manager.h b/cockatrice/src/single_instance_manager.h index d3884a60f..55bff0e80 100644 --- a/cockatrice/src/single_instance_manager.h +++ b/cockatrice/src/single_instance_manager.h @@ -23,14 +23,7 @@ private slots: void handleNewConnection(); private: - enum class ForwardResult - { - Delivered, // a live primary acknowledged the payload - NoPrimary, // no connectable primary socket exists - PrimaryBusy // a primary exists but did not acknowledge in time - }; - - ForwardResult forwardToPrimary(const QStringList &filesToSend); + bool forwardToPrimary(const QStringList &filesToSend); QString serverName; QLocalServer *server = nullptr; diff --git a/cockatrice/themes/CMakeLists.txt b/cockatrice/themes/CMakeLists.txt index 70344d95a..577977551 100644 --- a/cockatrice/themes/CMakeLists.txt +++ b/cockatrice/themes/CMakeLists.txt @@ -2,7 +2,7 @@ # # add themes subfolders -set(defthemes Fabric Fusion Leather Plasma VelvetMarble System) +set(defthemes Default Fabric Fusion Leather Plasma VelvetMarble) if(UNIX) if(APPLE) diff --git a/cockatrice/themes/System/palette-default-dark.toml b/cockatrice/themes/Default/palette-default-dark.toml similarity index 96% rename from cockatrice/themes/System/palette-default-dark.toml rename to cockatrice/themes/Default/palette-default-dark.toml index e101935b4..3ee174a2f 100644 --- a/cockatrice/themes/System/palette-default-dark.toml +++ b/cockatrice/themes/Default/palette-default-dark.toml @@ -61,7 +61,3 @@ ToolTipBase = #ffffffdc ToolTipText = #ff000000 PlaceholderText = #6effffff - -[AppColors] -AccentStrong = #ff148c3c -AccentSoft = #ff78c850 diff --git a/cockatrice/themes/System/theme.cfg b/cockatrice/themes/Default/theme.cfg similarity index 73% rename from cockatrice/themes/System/theme.cfg rename to cockatrice/themes/Default/theme.cfg index 4b756a5e8..d2016a238 100644 --- a/cockatrice/themes/System/theme.cfg +++ b/cockatrice/themes/Default/theme.cfg @@ -2,4 +2,4 @@ ColorScheme = Light [Style] -Name = System +Name = Default diff --git a/cockatrice/themes/Fabric/backgrounds/home-dark.png b/cockatrice/themes/Fabric/backgrounds/home-dark.png deleted file mode 100644 index 8d9514626..000000000 Binary files a/cockatrice/themes/Fabric/backgrounds/home-dark.png and /dev/null differ diff --git a/cockatrice/themes/Fabric/backgrounds/home-light.png b/cockatrice/themes/Fabric/backgrounds/home-light.png deleted file mode 100644 index 78f51a686..000000000 Binary files a/cockatrice/themes/Fabric/backgrounds/home-light.png and /dev/null differ diff --git a/cockatrice/themes/Fabric/palette-default-dark.toml b/cockatrice/themes/Fabric/palette-default-dark.toml deleted file mode 100644 index cad18921a..000000000 --- a/cockatrice/themes/Fabric/palette-default-dark.toml +++ /dev/null @@ -1,74 +0,0 @@ -# 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 - diff --git a/cockatrice/themes/Fabric/palette-default-light.toml b/cockatrice/themes/Fabric/palette-default-light.toml deleted file mode 100644 index f5151af3e..000000000 --- a/cockatrice/themes/Fabric/palette-default-light.toml +++ /dev/null @@ -1,74 +0,0 @@ -# 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 - diff --git a/cockatrice/themes/Fabric/theme.cfg b/cockatrice/themes/Fabric/theme.cfg deleted file mode 100644 index 55b916e71..000000000 --- a/cockatrice/themes/Fabric/theme.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[Appearance] -ColorScheme = System - -[Style] -Name = Fusion \ No newline at end of file diff --git a/cockatrice/themes/Fabric/zones/handzone-light.png b/cockatrice/themes/Fabric/zones/handzone-light.png deleted file mode 100644 index a87f9e292..000000000 Binary files a/cockatrice/themes/Fabric/zones/handzone-light.png and /dev/null differ diff --git a/cockatrice/themes/Fabric/zones/handzone.png b/cockatrice/themes/Fabric/zones/handzone.png index a5b72286c..25d22b070 100644 Binary files a/cockatrice/themes/Fabric/zones/handzone.png and b/cockatrice/themes/Fabric/zones/handzone.png differ diff --git a/cockatrice/themes/Fabric/zones/playerzone-light.png b/cockatrice/themes/Fabric/zones/playerzone-light.png deleted file mode 100644 index 05da3ea9b..000000000 Binary files a/cockatrice/themes/Fabric/zones/playerzone-light.png and /dev/null differ diff --git a/cockatrice/themes/Fabric/zones/playerzone.png b/cockatrice/themes/Fabric/zones/playerzone.png index 7fa977249..728fccdfa 100644 Binary files a/cockatrice/themes/Fabric/zones/playerzone.png and b/cockatrice/themes/Fabric/zones/playerzone.png differ diff --git a/cockatrice/themes/Fabric/zones/stackzone-light.png b/cockatrice/themes/Fabric/zones/stackzone-light.png deleted file mode 100644 index dcd6dc045..000000000 Binary files a/cockatrice/themes/Fabric/zones/stackzone-light.png and /dev/null differ diff --git a/cockatrice/themes/Fabric/zones/stackzone.png b/cockatrice/themes/Fabric/zones/stackzone.png index 9046aa049..89e2080f8 100644 Binary files a/cockatrice/themes/Fabric/zones/stackzone.png and b/cockatrice/themes/Fabric/zones/stackzone.png differ diff --git a/cockatrice/themes/Fabric/zones/tablezone-light.png b/cockatrice/themes/Fabric/zones/tablezone-light.png deleted file mode 100644 index cb297e7e1..000000000 Binary files a/cockatrice/themes/Fabric/zones/tablezone-light.png and /dev/null differ diff --git a/cockatrice/themes/Fabric/zones/tablezone.png b/cockatrice/themes/Fabric/zones/tablezone.png index 8048a4cc1..6a2ce68e1 100644 Binary files a/cockatrice/themes/Fabric/zones/tablezone.png and b/cockatrice/themes/Fabric/zones/tablezone.png differ diff --git a/cockatrice/themes/Fusion/palette-default-dark.toml b/cockatrice/themes/Fusion/palette-default-dark.toml index b180e9b63..c1d83a4cd 100644 --- a/cockatrice/themes/Fusion/palette-default-dark.toml +++ b/cockatrice/themes/Fusion/palette-default-dark.toml @@ -11,15 +11,15 @@ ButtonText = #ffffffff Base = #ff2d2d2d Window = #ff1e1e1e Shadow = #ff000000 -Highlight = #ff139740 +Highlight = #ff148c3c HighlightedText = #ffffffff -Link = #ffc9fd62 -LinkVisited = #ff9ad43e +Link = #ff00f652 +LinkVisited = #ff00d346 AlternateBase = #ff353535 ToolTipBase = #ff3c3c3c ToolTipText = #ffd4d4d4 PlaceholderText = #80ffffff -Accent = #ffc9fd62 +Accent = #ff00d346 [Palette.Disabled] WindowText = #ff9d9d9d @@ -34,7 +34,7 @@ ButtonText = #ff9d9d9d Base = #ff1e1e1e Window = #ff1e1e1e Shadow = #ff000000 -Highlight = #ff139740 +Highlight = #ff148c3c HighlightedText = #ffffffff Link = #ff308cc6 LinkVisited = #ffff00ff @@ -59,15 +59,11 @@ Window = #ff1e1e1e Shadow = #ff000000 Highlight = #ff1e1e1e HighlightedText = #ffffffff -Link = #ffc9fd62 -LinkVisited = #ff9ad43e +Link = #ff00f652 +LinkVisited = #ff00d346 AlternateBase = #ff353535 ToolTipBase = #ff3c3c3c ToolTipText = #ffd4d4d4 PlaceholderText = #80ffffff Accent = #ff1e1e1e - -[AppColors] -AccentStrong = #ff139740 -AccentSoft = #ffc9fd62 diff --git a/cockatrice/themes/Fusion/palette-default-light.toml b/cockatrice/themes/Fusion/palette-default-light.toml index 5a1163a2b..86c41be78 100644 --- a/cockatrice/themes/Fusion/palette-default-light.toml +++ b/cockatrice/themes/Fusion/palette-default-light.toml @@ -11,15 +11,15 @@ ButtonText = #ff000000 Base = #ffffffff Window = #fff0f0f0 Shadow = #ff696969 -Highlight = #ff139740 +Highlight = #ff148c3c HighlightedText = #ffffffff -Link = #ff0e6b31 -LinkVisited = #ff0a521f +Link = #ff0d5f28 +LinkVisited = #ff08401b AlternateBase = #ffe9e7e3 ToolTipBase = #ffffffdc ToolTipText = #ff000000 PlaceholderText = #80000000 -Accent = #ff139740 +Accent = #ff107532 [Palette.Disabled] WindowText = #ff787878 @@ -34,7 +34,7 @@ ButtonText = #ff787878 Base = #fff0f0f0 Window = #fff0f0f0 Shadow = #ff000000 -Highlight = #ff139740 +Highlight = #ff148c3c HighlightedText = #ffffffff Link = #ff0000ff LinkVisited = #ffff00ff @@ -59,15 +59,11 @@ Window = #fff0f0f0 Shadow = #ff696969 Highlight = #fff0f0f0 HighlightedText = #ff000000 -Link = #ff0e6b31 -LinkVisited = #ff0a521f +Link = #ff0d5f28 +LinkVisited = #ff08401b AlternateBase = #ffe9e7e3 ToolTipBase = #ffffffdc ToolTipText = #ff000000 PlaceholderText = #80000000 Accent = #fff0f0f0 - -[AppColors] -AccentStrong = #ff139740 -AccentSoft = #ffc9fd62 diff --git a/cockatrice/themes/Leather/backgrounds/home-dark.png b/cockatrice/themes/Leather/backgrounds/home-dark.png deleted file mode 100644 index 20d16a27f..000000000 Binary files a/cockatrice/themes/Leather/backgrounds/home-dark.png and /dev/null differ diff --git a/cockatrice/themes/Leather/backgrounds/home-light.png b/cockatrice/themes/Leather/backgrounds/home-light.png deleted file mode 100644 index ee1da361d..000000000 Binary files a/cockatrice/themes/Leather/backgrounds/home-light.png and /dev/null differ diff --git a/cockatrice/themes/Leather/palette-default-dark.toml b/cockatrice/themes/Leather/palette-default-dark.toml deleted file mode 100644 index b0db0e90a..000000000 --- a/cockatrice/themes/Leather/palette-default-dark.toml +++ /dev/null @@ -1,74 +0,0 @@ -# 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 - diff --git a/cockatrice/themes/Leather/palette-default-light.toml b/cockatrice/themes/Leather/palette-default-light.toml deleted file mode 100644 index ff122db76..000000000 --- a/cockatrice/themes/Leather/palette-default-light.toml +++ /dev/null @@ -1,74 +0,0 @@ -# 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 - diff --git a/cockatrice/themes/Leather/theme.cfg b/cockatrice/themes/Leather/theme.cfg deleted file mode 100644 index 55b916e71..000000000 --- a/cockatrice/themes/Leather/theme.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[Appearance] -ColorScheme = System - -[Style] -Name = Fusion \ No newline at end of file diff --git a/cockatrice/themes/Leather/zones/handzone-light.png b/cockatrice/themes/Leather/zones/handzone-light.png deleted file mode 100644 index b2c9bba34..000000000 Binary files a/cockatrice/themes/Leather/zones/handzone-light.png and /dev/null differ diff --git a/cockatrice/themes/Leather/zones/handzone.png b/cockatrice/themes/Leather/zones/handzone.png index 5f803df74..aba879683 100644 Binary files a/cockatrice/themes/Leather/zones/handzone.png and b/cockatrice/themes/Leather/zones/handzone.png differ diff --git a/cockatrice/themes/Leather/zones/playerzone-light.png b/cockatrice/themes/Leather/zones/playerzone-light.png deleted file mode 100644 index a26f96a5e..000000000 Binary files a/cockatrice/themes/Leather/zones/playerzone-light.png and /dev/null differ diff --git a/cockatrice/themes/Leather/zones/playerzone.png b/cockatrice/themes/Leather/zones/playerzone.png index cccbb3fd3..4f42ee41d 100644 Binary files a/cockatrice/themes/Leather/zones/playerzone.png and b/cockatrice/themes/Leather/zones/playerzone.png differ diff --git a/cockatrice/themes/Leather/zones/stackzone-light.png b/cockatrice/themes/Leather/zones/stackzone-light.png deleted file mode 100644 index 117c738f7..000000000 Binary files a/cockatrice/themes/Leather/zones/stackzone-light.png and /dev/null differ diff --git a/cockatrice/themes/Leather/zones/stackzone.png b/cockatrice/themes/Leather/zones/stackzone.png index d9cff892b..c02e2284a 100644 Binary files a/cockatrice/themes/Leather/zones/stackzone.png and b/cockatrice/themes/Leather/zones/stackzone.png differ diff --git a/cockatrice/themes/Leather/zones/tablezone-light.png b/cockatrice/themes/Leather/zones/tablezone-light.png deleted file mode 100644 index 912cbfb94..000000000 Binary files a/cockatrice/themes/Leather/zones/tablezone-light.png and /dev/null differ diff --git a/cockatrice/themes/Leather/zones/tablezone.png b/cockatrice/themes/Leather/zones/tablezone.png index 8af4c853d..152f4f7e9 100644 Binary files a/cockatrice/themes/Leather/zones/tablezone.png and b/cockatrice/themes/Leather/zones/tablezone.png differ diff --git a/cockatrice/themes/Plasma/backgrounds/home-dark.png b/cockatrice/themes/Plasma/backgrounds/home-dark.png deleted file mode 100644 index ce1469e2b..000000000 Binary files a/cockatrice/themes/Plasma/backgrounds/home-dark.png and /dev/null differ diff --git a/cockatrice/themes/Plasma/backgrounds/home-light.png b/cockatrice/themes/Plasma/backgrounds/home-light.png deleted file mode 100644 index 2e9000bd6..000000000 Binary files a/cockatrice/themes/Plasma/backgrounds/home-light.png and /dev/null differ diff --git a/cockatrice/themes/Plasma/palette-default-dark.toml b/cockatrice/themes/Plasma/palette-default-dark.toml deleted file mode 100644 index 222456f4f..000000000 --- a/cockatrice/themes/Plasma/palette-default-dark.toml +++ /dev/null @@ -1,74 +0,0 @@ -# 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 - diff --git a/cockatrice/themes/Plasma/palette-default-light.toml b/cockatrice/themes/Plasma/palette-default-light.toml deleted file mode 100644 index d54db31d2..000000000 --- a/cockatrice/themes/Plasma/palette-default-light.toml +++ /dev/null @@ -1,74 +0,0 @@ -# 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 - diff --git a/cockatrice/themes/Plasma/theme.cfg b/cockatrice/themes/Plasma/theme.cfg deleted file mode 100644 index 55b916e71..000000000 --- a/cockatrice/themes/Plasma/theme.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[Appearance] -ColorScheme = System - -[Style] -Name = Fusion \ No newline at end of file diff --git a/cockatrice/themes/Plasma/zones/handzone-light.png b/cockatrice/themes/Plasma/zones/handzone-light.png deleted file mode 100644 index 89fa7e6d4..000000000 Binary files a/cockatrice/themes/Plasma/zones/handzone-light.png and /dev/null differ diff --git a/cockatrice/themes/Plasma/zones/handzone.png b/cockatrice/themes/Plasma/zones/handzone.png index 82debb405..543571d8a 100644 Binary files a/cockatrice/themes/Plasma/zones/handzone.png and b/cockatrice/themes/Plasma/zones/handzone.png differ diff --git a/cockatrice/themes/Plasma/zones/playerzone-light.png b/cockatrice/themes/Plasma/zones/playerzone-light.png deleted file mode 100644 index 9034818f2..000000000 Binary files a/cockatrice/themes/Plasma/zones/playerzone-light.png and /dev/null differ diff --git a/cockatrice/themes/Plasma/zones/playerzone.png b/cockatrice/themes/Plasma/zones/playerzone.png index 4623524f6..eebcd1f84 100644 Binary files a/cockatrice/themes/Plasma/zones/playerzone.png and b/cockatrice/themes/Plasma/zones/playerzone.png differ diff --git a/cockatrice/themes/Plasma/zones/stackzone-light.png b/cockatrice/themes/Plasma/zones/stackzone-light.png deleted file mode 100644 index 2607b3389..000000000 Binary files a/cockatrice/themes/Plasma/zones/stackzone-light.png and /dev/null differ diff --git a/cockatrice/themes/Plasma/zones/stackzone.png b/cockatrice/themes/Plasma/zones/stackzone.png index 144d991f3..1845c7091 100644 Binary files a/cockatrice/themes/Plasma/zones/stackzone.png and b/cockatrice/themes/Plasma/zones/stackzone.png differ diff --git a/cockatrice/themes/Plasma/zones/tablezone-light.png b/cockatrice/themes/Plasma/zones/tablezone-light.png deleted file mode 100644 index dcf2ef95e..000000000 Binary files a/cockatrice/themes/Plasma/zones/tablezone-light.png and /dev/null differ diff --git a/cockatrice/themes/Plasma/zones/tablezone.png b/cockatrice/themes/Plasma/zones/tablezone.png index 4dc36ec2d..8f998d4bb 100644 Binary files a/cockatrice/themes/Plasma/zones/tablezone.png and b/cockatrice/themes/Plasma/zones/tablezone.png differ diff --git a/cockatrice/themes/System/palette-default-light.toml b/cockatrice/themes/System/palette-default-light.toml deleted file mode 100644 index 14c215cdf..000000000 --- a/cockatrice/themes/System/palette-default-light.toml +++ /dev/null @@ -1,67 +0,0 @@ -[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 \ No newline at end of file diff --git a/cockatrice/themes/VelvetMarble/backgrounds/home-dark.png b/cockatrice/themes/VelvetMarble/backgrounds/home-dark.png deleted file mode 100644 index 34738090f..000000000 Binary files a/cockatrice/themes/VelvetMarble/backgrounds/home-dark.png and /dev/null differ diff --git a/cockatrice/themes/VelvetMarble/backgrounds/home-light.png b/cockatrice/themes/VelvetMarble/backgrounds/home-light.png deleted file mode 100644 index de783e086..000000000 Binary files a/cockatrice/themes/VelvetMarble/backgrounds/home-light.png and /dev/null differ diff --git a/cockatrice/themes/VelvetMarble/palette-default-dark.toml b/cockatrice/themes/VelvetMarble/palette-default-dark.toml deleted file mode 100644 index 496943931..000000000 --- a/cockatrice/themes/VelvetMarble/palette-default-dark.toml +++ /dev/null @@ -1,74 +0,0 @@ -# VelvetMarble — dark identity palette (charcoal-velvet chrome with slate marble) -[Palette] -WindowText = #e8eaee -Button = #262a31 -Light = #3a3f47 -Midlight = #31363d -Dark = #0e1013 -Mid = #1f2329 -Text = #e8eaee -BrightText = #c3d8f2 -ButtonText = #f2f3f6 -Base = #1b1e23 -Window = #131519 -Shadow = #08090b -Highlight = #8fa6c0 -HighlightedText = #101318 -Link = #a9c6e8 -LinkVisited = #b9a8d9 -AlternateBase = #171a1f -ToolTipBase = #2d323a -ToolTipText = #f2f3f6 -PlaceholderText = #6ee8eaee -Accent = #8fa6c0 - -[Palette.Disabled] -WindowText = #9d9d9d -Button = #131519 -Light = #3a3f47 -Midlight = #31363d -Dark = #0e1013 -Mid = #1f2329 -Text = #9d9d9d -BrightText = #c3d8f2 -ButtonText = #787878 -Base = #131519 -Window = #131519 -Shadow = #08090b -Highlight = #1f2229 -HighlightedText = #9d9d9d -Link = #308cc6 -LinkVisited = #b450ff -AlternateBase = #171a1f -ToolTipBase = #2d323a -ToolTipText = #f2f3f6 -PlaceholderText = #6ee8eaee -Accent = #9d9d9d - -[Palette.Inactive] -WindowText = #e8eaee -Button = #262a31 -Light = #3a3f47 -Midlight = #31363d -Dark = #0e1013 -Mid = #1f2329 -Text = #e8eaee -BrightText = #c3d8f2 -ButtonText = #f2f3f6 -Base = #1b1e23 -Window = #131519 -Shadow = #08090b -Highlight = #1f2229 -HighlightedText = #ffffff -Link = #a9c6e8 -LinkVisited = #b9a8d9 -AlternateBase = #171a1f -ToolTipBase = #2d323a -ToolTipText = #f2f3f6 -PlaceholderText = #6ee8eaee -Accent = #8fa6c0 - -[AppColors] -AccentStrong = #54687e -AccentSoft = #9db0c4 - diff --git a/cockatrice/themes/VelvetMarble/palette-default-light.toml b/cockatrice/themes/VelvetMarble/palette-default-light.toml deleted file mode 100644 index 5323fd320..000000000 --- a/cockatrice/themes/VelvetMarble/palette-default-light.toml +++ /dev/null @@ -1,74 +0,0 @@ -# VelvetMarble — light identity palette (charcoal-velvet chrome with slate marble) -[Palette] -WindowText = #1b1d21 -Button = #c7d1dd -Light = #ffffff -Midlight = #b3becb -Dark = #93a0b1 -Mid = #a1adbc -Text = #1b1d21 -BrightText = #26465f -ButtonText = #121418 -Base = #f7f9fb -Window = #d9e0ea -Shadow = #57626f -Highlight = #54687e -HighlightedText = #ffffff -Link = #2e4d6b -LinkVisited = #5d4a78 -AlternateBase = #e6ebf2 -ToolTipBase = #f2f4f6 -ToolTipText = #121418 -PlaceholderText = #7a1b1d21 -Accent = #54687e - -[Palette.Disabled] -WindowText = #787878 -Button = #d9e0ea -Light = #ffffff -Midlight = #b3becb -Dark = #93a0b1 -Mid = #a1adbc -Text = #787878 -BrightText = #26465f -ButtonText = #969696 -Base = #d9e0ea -Window = #d9e0ea -Shadow = #57626f -Highlight = #ccd5e3 -HighlightedText = #787878 -Link = #0000ff -LinkVisited = #ff00ff -AlternateBase = #e6ebf2 -ToolTipBase = #f2f4f6 -ToolTipText = #121418 -PlaceholderText = #7a1b1d21 -Accent = #787878 - -[Palette.Inactive] -WindowText = #1b1d21 -Button = #c7d1dd -Light = #ffffff -Midlight = #b3becb -Dark = #93a0b1 -Mid = #a1adbc -Text = #1b1d21 -BrightText = #26465f -ButtonText = #121418 -Base = #f7f9fb -Window = #d9e0ea -Shadow = #57626f -Highlight = #ccd5e3 -HighlightedText = #000000 -Link = #2e4d6b -LinkVisited = #5d4a78 -AlternateBase = #e6ebf2 -ToolTipBase = #f2f4f6 -ToolTipText = #121418 -PlaceholderText = #7a1b1d21 -Accent = #54687e - -[AppColors] -AccentStrong = #54687e -AccentSoft = #9db0c4 - diff --git a/cockatrice/themes/VelvetMarble/theme.cfg b/cockatrice/themes/VelvetMarble/theme.cfg deleted file mode 100644 index 55b916e71..000000000 --- a/cockatrice/themes/VelvetMarble/theme.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[Appearance] -ColorScheme = System - -[Style] -Name = Fusion \ No newline at end of file diff --git a/cockatrice/themes/VelvetMarble/zones/handzone-light.png b/cockatrice/themes/VelvetMarble/zones/handzone-light.png deleted file mode 100644 index b21e78e61..000000000 Binary files a/cockatrice/themes/VelvetMarble/zones/handzone-light.png and /dev/null differ diff --git a/cockatrice/themes/VelvetMarble/zones/handzone.jpg b/cockatrice/themes/VelvetMarble/zones/handzone.jpg new file mode 100644 index 000000000..2ec9b37fe Binary files /dev/null and b/cockatrice/themes/VelvetMarble/zones/handzone.jpg differ diff --git a/cockatrice/themes/VelvetMarble/zones/handzone.png b/cockatrice/themes/VelvetMarble/zones/handzone.png deleted file mode 100644 index 3e2b7ebea..000000000 Binary files a/cockatrice/themes/VelvetMarble/zones/handzone.png and /dev/null differ diff --git a/cockatrice/themes/VelvetMarble/zones/playerzone-light.png b/cockatrice/themes/VelvetMarble/zones/playerzone-light.png deleted file mode 100644 index 630aac6eb..000000000 Binary files a/cockatrice/themes/VelvetMarble/zones/playerzone-light.png and /dev/null differ diff --git a/cockatrice/themes/VelvetMarble/zones/playerzone.jpg b/cockatrice/themes/VelvetMarble/zones/playerzone.jpg new file mode 100644 index 000000000..dd13cab78 Binary files /dev/null and b/cockatrice/themes/VelvetMarble/zones/playerzone.jpg differ diff --git a/cockatrice/themes/VelvetMarble/zones/playerzone.png b/cockatrice/themes/VelvetMarble/zones/playerzone.png deleted file mode 100644 index dcf21ba38..000000000 Binary files a/cockatrice/themes/VelvetMarble/zones/playerzone.png and /dev/null differ diff --git a/cockatrice/themes/VelvetMarble/zones/stackzone-light.png b/cockatrice/themes/VelvetMarble/zones/stackzone-light.png deleted file mode 100644 index 62027827c..000000000 Binary files a/cockatrice/themes/VelvetMarble/zones/stackzone-light.png and /dev/null differ diff --git a/cockatrice/themes/VelvetMarble/zones/stackzone.jpg b/cockatrice/themes/VelvetMarble/zones/stackzone.jpg new file mode 100644 index 000000000..b63aa0902 Binary files /dev/null and b/cockatrice/themes/VelvetMarble/zones/stackzone.jpg differ diff --git a/cockatrice/themes/VelvetMarble/zones/stackzone.png b/cockatrice/themes/VelvetMarble/zones/stackzone.png deleted file mode 100644 index a47f7bf9d..000000000 Binary files a/cockatrice/themes/VelvetMarble/zones/stackzone.png and /dev/null differ diff --git a/cockatrice/themes/VelvetMarble/zones/tablezone-light.png b/cockatrice/themes/VelvetMarble/zones/tablezone-light.png deleted file mode 100644 index 5a9014f9d..000000000 Binary files a/cockatrice/themes/VelvetMarble/zones/tablezone-light.png and /dev/null differ diff --git a/cockatrice/themes/VelvetMarble/zones/tablezone.jpg b/cockatrice/themes/VelvetMarble/zones/tablezone.jpg new file mode 100644 index 000000000..9f511491a Binary files /dev/null and b/cockatrice/themes/VelvetMarble/zones/tablezone.jpg differ diff --git a/cockatrice/themes/VelvetMarble/zones/tablezone.png b/cockatrice/themes/VelvetMarble/zones/tablezone.png deleted file mode 100644 index 1523d0821..000000000 Binary files a/cockatrice/themes/VelvetMarble/zones/tablezone.png and /dev/null differ diff --git a/doc/carddatabase_v4/cards.xsd b/doc/carddatabase_v4/cards.xsd index 82aed32ab..59ca3e560 100644 --- a/doc/carddatabase_v4/cards.xsd +++ b/doc/carddatabase_v4/cards.xsd @@ -35,13 +35,6 @@ - - - - - - - @@ -56,13 +49,6 @@ - - - - - - - diff --git a/libcockatrice_card/CMakeLists.txt b/libcockatrice_card/CMakeLists.txt index 15388ecfc..081e6fd05 100644 --- a/libcockatrice_card/CMakeLists.txt +++ b/libcockatrice_card/CMakeLists.txt @@ -5,7 +5,6 @@ set(CMAKE_AUTORCC ON) set(HEADERS libcockatrice/card/card_info.h libcockatrice/card/card_info_comparator.h - libcockatrice/card/card_localization.h libcockatrice/card/lazy_properties_hash.h libcockatrice/card/database/card_database.h libcockatrice/card/database/card_database_loader.h @@ -28,7 +27,6 @@ add_library( ${MOC_SOURCES} libcockatrice/card/card_info.cpp libcockatrice/card/card_info_comparator.cpp - libcockatrice/card/card_localization.cpp libcockatrice/card/lazy_properties_hash.cpp libcockatrice/card/database/card_database.cpp libcockatrice/card/database/card_database_cache.cpp diff --git a/libcockatrice_card/libcockatrice/card/card_info.cpp b/libcockatrice_card/libcockatrice/card/card_info.cpp index 56c737793..786e17950 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.cpp +++ b/libcockatrice_card/libcockatrice/card/card_info.cpp @@ -40,11 +40,8 @@ CardInfo::CardInfo(const QString &_name, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, - const UiAttributes _uiAttributes, - QMap _localizedNames, - QMap _localizedTexts) - : name(_name), text(_text), isToken(_isToken), localizedNames(std::move(_localizedNames)), - localizedTexts(std::move(_localizedTexts)), properties(LazyPropertiesHash(_properties)), + const UiAttributes _uiAttributes) + : name(_name), text(_text), isToken(_isToken), properties(LazyPropertiesHash(_properties)), relatedCards(_relatedCards), reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), uiAttributes(_uiAttributes) { @@ -62,11 +59,8 @@ CardInfo::CardInfo(const QString &_name, SetToPrintingsMap _sets, const UiAttributes _uiAttributes, QString _simpleName, - QSet _altNames, - QMap _localizedNames, - QMap _localizedTexts) + QSet _altNames) : name(_name), simpleName(std::move(_simpleName)), text(_text), isToken(_isToken), - localizedNames(std::move(_localizedNames)), localizedTexts(std::move(_localizedTexts)), properties(LazyPropertiesHash(_propertiesBlob)), relatedCards(_relatedCards), reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), uiAttributes(_uiAttributes), altNames(std::move(_altNames)) @@ -89,12 +83,10 @@ CardInfoPtr CardInfo::newInstance(const QString &_name, const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, - const UiAttributes _uiAttributes, - QMap _localizedNames, - QMap _localizedTexts) + const UiAttributes _uiAttributes) { - CardInfoPtr ptr(new CardInfo(_name, _text, _isToken, _properties, _relatedCards, _reverseRelatedCards, _sets, - _uiAttributes, std::move(_localizedNames), std::move(_localizedTexts))); + CardInfoPtr ptr( + new CardInfo(_name, _text, _isToken, _properties, _relatedCards, _reverseRelatedCards, _sets, _uiAttributes)); ptr->setSmartPointer(ptr); for (const auto &printings : _sets) { @@ -117,13 +109,11 @@ CardInfoPtr CardInfo::newInstance(const QString &_name, const UiAttributes _uiAttributes, QString _simpleName, QSet _altNames, - bool _appendToSets, - QMap _localizedNames, - QMap _localizedTexts) + bool _appendToSets) { CardInfoPtr ptr(new CardInfo(_name, _text, _isToken, std::move(_propertiesBlob), _relatedCards, _reverseRelatedCards, _sets, _uiAttributes, std::move(_simpleName), - std::move(_altNames), std::move(_localizedNames), std::move(_localizedTexts))); + std::move(_altNames))); ptr->setSmartPointer(ptr); if (_appendToSets) { diff --git a/libcockatrice_card/libcockatrice/card/card_info.h b/libcockatrice_card/libcockatrice/card/card_info.h index 17894cec5..392dc3849 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.h +++ b/libcockatrice_card/libcockatrice/card/card_info.h @@ -77,9 +77,6 @@ private: QString text; ///< Text description or rules text of the card. bool isToken; ///< Whether this card is a token or not. - QMap localizedNames; ///< Localized card names, keyed by language code. - QMap localizedTexts; ///< Localized rules text, keyed by language code. - LazyPropertiesHash properties; ///< Key-value store of dynamic card properties. QList relatedCards; ///< Forward references to related cards. @@ -103,8 +100,6 @@ public: * @param _reverseRelatedCards Backward references to related cards. * @param _sets Map of set names to printing information. * @param _uiAttributes Attributes that affect display and game logic - * @param _localizedNames Localized card names, keyed by language code. - * @param _localizedTexts Localized rules text, keyed by language code. */ explicit CardInfo(const QString &_name, const QString &_text, @@ -113,9 +108,7 @@ public: const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, - UiAttributes _uiAttributes, - QMap _localizedNames = {}, - QMap _localizedTexts = {}); + UiAttributes _uiAttributes); /** * @brief Constructs a CardInfo from a cache snapshot with precomputed derived @@ -137,8 +130,6 @@ public: * @param _uiAttributes Attributes that affect display and game logic. * @param _simpleName Precomputed simplified name. * @param _altNames Precomputed alternate names. - * @param _localizedNames Localized card names, keyed by language code. - * @param _localizedTexts Localized rules text, keyed by language code. */ explicit CardInfo(const QString &_name, const QString &_text, @@ -149,9 +140,7 @@ public: SetToPrintingsMap _sets, UiAttributes _uiAttributes, QString _simpleName, - QSet _altNames, - QMap _localizedNames = {}, - QMap _localizedTexts = {}); + QSet _altNames); /** * @brief Copy constructor for CardInfo. @@ -162,8 +151,7 @@ public: */ CardInfo(const CardInfo &other) : QObject(other.parent()), name(other.name), simpleName(other.simpleName), text(other.text), - isToken(other.isToken), localizedNames(other.localizedNames), localizedTexts(other.localizedTexts), - properties(other.properties), relatedCards(other.relatedCards), + isToken(other.isToken), properties(other.properties), relatedCards(other.relatedCards), reverseRelatedCards(other.reverseRelatedCards), reverseRelatedCardsToMe(other.reverseRelatedCardsToMe), setsToPrintings(other.setsToPrintings), uiAttributes(other.uiAttributes), setsNames(other.setsNames), altNames(other.altNames) @@ -191,8 +179,6 @@ public: * @param _reverseRelatedCards Reverse relationships. * @param _sets Printing information per set. * @param _uiAttributes Attributes that affect display and game logic - * @param _localizedNames Localized card names, keyed by language code. - * @param _localizedTexts Localized rules text, keyed by language code. * @return Shared pointer to the new CardInfo instance. */ static CardInfoPtr newInstance(const QString &_name, @@ -202,9 +188,7 @@ public: const QList &_relatedCards, const QList &_reverseRelatedCards, SetToPrintingsMap _sets, - UiAttributes _uiAttributes, - QMap _localizedNames = {}, - QMap _localizedTexts = {}); + UiAttributes _uiAttributes); /** * @brief Creates a new instance from a cache snapshot with precomputed @@ -224,8 +208,6 @@ public: * its CardSets. Pass false when building cards in parallel so the * (non-thread-safe) set membership is populated in a later * single-threaded pass. - * @param _localizedNames Localized card names, keyed by language code. - * @param _localizedTexts Localized rules text, keyed by language code. * @return Shared pointer to the new CardInfo instance. */ static CardInfoPtr newInstance(const QString &_name, @@ -238,9 +220,7 @@ public: UiAttributes _uiAttributes, QString _simpleName, QSet _altNames, - bool _appendToSets = true, - QMap _localizedNames = {}, - QMap _localizedTexts = {}); + bool _appendToSets = true); /** * @brief Clones the current CardInfo instance. @@ -290,96 +270,6 @@ public: text = _text; emit cardInfoChanged(smartThis); } - - /** - * @brief Returns the card name in the given language, falling back to the - * English name when no localization is available. - * - * @param lang Language code (e.g. "de", "ja", "zhs"). - * @return The localized name, or the English name as fallback. - */ - [[nodiscard]] const QString &getLocalizedName(const QString &lang) const - { - const auto it = localizedNames.constFind(lang); - return it != localizedNames.constEnd() ? it.value() : name; - } - - /** - * @brief Returns the rules text in the given language, falling back to the - * English text when no localization is available. - * - * @param lang Language code (e.g. "de", "ja", "zhs"). - * @return The localized text, or the English text as fallback. - */ - [[nodiscard]] const QString &getLocalizedText(const QString &lang) const - { - const auto it = localizedTexts.constFind(lang); - return it != localizedTexts.constEnd() ? it.value() : text; - } - - /** - * @brief Returns the localized card names keyed by language code. - * - * Only languages that have an entry are present; there is no English - * fallback in this map. - */ - [[nodiscard]] const QMap &getLocalizedNames() const - { - return localizedNames; - } - - /** - * @brief Returns the localized rules text keyed by language code. - * - * Only languages that have an entry are present; there is no English - * fallback in this map. - */ - [[nodiscard]] const QMap &getLocalizedTexts() const - { - return localizedTexts; - } - - /** - * @brief Sets the card name for the given language. - * - * @param lang Language code. - * @param _localizedName The localized card name. - */ - void setLocalizedName(const QString &lang, const QString &_localizedName) - { - if (localizedNames.value(lang) == _localizedName) { - return; - } - localizedNames.insert(lang, _localizedName); - emit cardInfoChanged(smartThis); - } - - /** - * @brief Sets the rules text for the given language. - * - * @param lang Language code. - * @param _localizedText The localized rules text. - */ - void setLocalizedText(const QString &lang, const QString &_localizedText) - { - if (localizedTexts.value(lang) == _localizedText) { - return; - } - localizedTexts.insert(lang, _localizedText); - emit cardInfoChanged(smartThis); - } - - /** - * @brief Returns the language codes for which this card has a localized - * name or rules text. - */ - [[nodiscard]] QStringList localizationLanguages() const - { - QStringList languages = localizedNames.keys(); - languages.append(localizedTexts.keys()); - languages.removeDuplicates(); - return languages; - } [[nodiscard]] bool getIsToken() const { return isToken; diff --git a/libcockatrice_card/libcockatrice/card/card_localization.cpp b/libcockatrice_card/libcockatrice/card/card_localization.cpp deleted file mode 100644 index 03c6c659b..000000000 --- a/libcockatrice_card/libcockatrice/card/card_localization.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "card_localization.h" - -#include -#include -#include - -namespace CardLocalization -{ -const QStringList &supportedLanguages() -{ - static const QStringList languages = {"cs", "de", "es", "fr", "it", "ja", "ko", "pt", "ru", "zhs", "zht", "he"}; - return languages; -} - -QString languageDisplayName(const QString &lang) -{ - static const QHash displayNames = { - {"cs", "Česky (Czech)"}, - {"de", "Deutsch (German)"}, - {"es", "Español (Spanish)"}, - {"fr", "Français (French)"}, - {"it", "Italiano (Italian)"}, - {"ja", "日本語 (Japanese)"}, - {"ko", "한국어 (Korean)"}, - {"pt", "Português (Portuguese)"}, - {"ru", "Русский (Russian)"}, - {"he", "עברית (Hebrew)"}, - {"zhs", "简体中文 (Chinese Simplified)"}, - {"zht", "繁體中文 (Chinese Traditional)"}, - }; - const QString displayName = displayNames.value(lang); - if (!displayName.isEmpty()) { - return displayName; - } - const QString nativeName = QLocale(lang).nativeLanguageName(); - return nativeName.isEmpty() ? lang : nativeName; -} -} // namespace CardLocalization \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/card_localization.h b/libcockatrice_card/libcockatrice/card/card_localization.h deleted file mode 100644 index a9c8d28ea..000000000 --- a/libcockatrice_card/libcockatrice/card/card_localization.h +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef CARD_LOCALIZATION_H -#define CARD_LOCALIZATION_H - -#include -#include - -/** - * @namespace CardLocalization - * @ingroup Cards - * - * @brief Shared language metadata for localized card text and images. - * - * Lists the language codes Cockatrice can display localized card data for and - * provides human-readable names. The list is shared between Oracle (which - * imports the selected language's card data) and the client settings UI (which - * offers the language choice). - */ -namespace CardLocalization -{ -/** - * @brief Language codes for which localized card data can be imported/displayed. - * - * Matches the languages Scryfall can serve localized card images for. "en" is - * always available as the default/fallback and is not listed here. - * - * @return The list of supported language codes. - */ -[[nodiscard]] const QStringList &supportedLanguages(); - -/** - * @brief Human-readable name for a language code. - * - * Follows the same "native name (English name)" format the UI language list - * uses (e.g. "日本語 (Japanese)"), so the English fallback is always visible. - * - * @param lang Language code (e.g. "de", "ja", "zhs"). - * @return The language's native name with its English name in parentheses, or - * the code itself if it cannot be resolved. - */ -[[nodiscard]] QString languageDisplayName(const QString &lang); -} // namespace CardLocalization - -#endif // CARD_LOCALIZATION_H \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp index 3165ff871..2b27f50f8 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database_cache.cpp @@ -17,7 +17,7 @@ namespace { constexpr quint32 CACHE_MAGIC = 0x43445243; // "CDRC" -constexpr quint32 CACHE_VERSION = 3; +constexpr quint32 CACHE_VERSION = 2; // ---- Primitives ----------------------------------------------------------- @@ -71,35 +71,6 @@ QDate readDate(QDataStream &in) return d; } -void writeStringMap(QDataStream &out, const QMap &map) -{ - out << static_cast(map.size()); - for (auto it = map.constBegin(); it != map.constEnd(); ++it) { - writeString(out, it.key()); - writeString(out, it.value()); - } -} - -QMap readStringMap(QDataStream &in) -{ - QMap map; - quint32 count = 0; - in >> count; - if (in.status() != QDataStream::Ok) { - return map; - } - for (quint32 i = 0; i < count; ++i) { - QString key = readString(in); - QString value = readString(in); - if (in.status() != QDataStream::Ok) { - map.clear(); - return map; - } - map.insert(key, value); - } - return map; -} - // ---- CardRelation ---------------------------------------------------------- void writeRelation(QDataStream &out, const CardRelation *rel) @@ -224,10 +195,6 @@ void writeCard(QDataStream &out, const CardInfoPtr &card) for (const CardRelation *rel : reverse) { writeRelation(out, rel); } - - // localized card data - writeStringMap(out, card->getLocalizedNames()); - writeStringMap(out, card->getLocalizedTexts()); } CardInfoPtr readCard(QDataStream &in, const SetNameMap &sets) @@ -301,19 +268,8 @@ CardInfoPtr readCard(QDataStream &in, const SetNameMap &sets) reverse.append(readRelation(in)); } - const QMap localizedNames = readStringMap(in); - if (in.status() != QDataStream::Ok) { - return nullptr; - } - const QMap localizedTexts = readStringMap(in); - if (in.status() != QDataStream::Ok) { - return nullptr; - } - - CardInfoPtr card = CardInfo::newInstance(name, text, isToken, propertiesBlob, related, reverse, cardSets, ui, - simpleName, altNames, false, localizedNames, localizedTexts); - - return card; + return CardInfo::newInstance(name, text, isToken, propertiesBlob, related, reverse, cardSets, ui, simpleName, + altNames, false); } // ---- FormatRules ----------------------------------------------------------- diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp index 19e972a7a..ec460d685 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp @@ -273,8 +273,6 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) QString name = QString(""); QString text = QString(""); QHash properties; - QMap localizedNames; - QMap localizedTexts; QList relatedCards, reverseRelatedCards; auto _sets = SetToPrintingsMap(); int tableRow = 0; @@ -300,44 +298,6 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) // generic properties } else if (xmlName == "prop") { properties = loadCardPropertiesFromXml(xml); - // localized card data - } else if (xmlName == "localizations") { - while (!xml.atEnd()) { - if (xml.readNextStartElement()) { - const QString elementName = xml.name().toString(); - if (elementName == "localization") { - const QString lang = xml.attributes().value("lang").toString(); - QString localizedName; - QString localizedText; - while (!xml.atEnd()) { - if (xml.readNext() == QXmlStreamReader::EndElement) { - break; - } - if (xml.isStartElement()) { - const QString childName = xml.name().toString(); - QString value = xml.readElementText(QXmlStreamReader::IncludeChildElements); - if (childName == "name") { - localizedName = value; - } else if (childName == "text") { - localizedText = value; - } - } - } - if (!lang.isEmpty()) { - if (!localizedName.isEmpty()) { - localizedNames.insert(lang, localizedName); - } - if (!localizedText.isEmpty()) { - localizedTexts.insert(lang, localizedText); - } - } - } else { - xml.skipCurrentElement(); - } - } else { - break; - } - } // positioning info } else if (xmlName == "tablerow") { tableRow = xml.readElementText(QXmlStreamReader::IncludeChildElements).toInt(); @@ -439,9 +399,8 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) .landscapeOrientation = landscapeOrientation, .tableRow = tableRow, .upsideDownArt = upsideDown}; - CardInfoPtr newCard = - CardInfo::newInstance(name, text, isToken, properties, relatedCards, reverseRelatedCards, _sets, - attributes, std::move(localizedNames), std::move(localizedTexts)); + CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards, + reverseRelatedCards, _sets, attributes); if (targetData) { // Mirror CardDatabase::addCard: if a card with this name already // exists, merge the new printings into it instead of replacing. @@ -558,26 +517,6 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in } xml.writeEndElement(); - // localized card data - const QStringList localizedLanguages = info->localizationLanguages(); - if (!localizedLanguages.isEmpty()) { - xml.writeStartElement("localizations"); - const QMap &localizedNames = info->getLocalizedNames(); - const QMap &localizedTexts = info->getLocalizedTexts(); - for (const QString &lang : localizedLanguages) { - xml.writeStartElement("localization"); - xml.writeAttribute("lang", lang); - if (localizedNames.contains(lang)) { - xml.writeTextElement("name", localizedNames.value(lang)); - } - if (localizedTexts.contains(lang)) { - xml.writeTextElement("text", localizedTexts.value(lang)); - } - xml.writeEndElement(); - } - xml.writeEndElement(); - } - // sets for (const auto &printings : info->getSets()) { for (const PrintingInfo &set : printings) { diff --git a/libcockatrice_deck_list/CMakeLists.txt b/libcockatrice_deck_list/CMakeLists.txt index 0c487466c..c7a54a390 100644 --- a/libcockatrice_deck_list/CMakeLists.txt +++ b/libcockatrice_deck_list/CMakeLists.txt @@ -11,7 +11,6 @@ set(HEADERS libcockatrice/deck_list/deck_list_history_manager.h libcockatrice/deck_list/deck_list_node_tree.h libcockatrice/deck_list/deck_list_memento.h - libcockatrice/deck_list/deck_list_plain_text_parser.h libcockatrice/deck_list/playmat_resolver.h libcockatrice/deck_list/sideboard_plan.h ) @@ -28,7 +27,6 @@ add_library( libcockatrice/deck_list/deck_list.cpp libcockatrice/deck_list/deck_list_history_manager.cpp libcockatrice/deck_list/deck_list_node_tree.cpp - libcockatrice/deck_list/deck_list_plain_text_parser.cpp libcockatrice/deck_list/playmat_resolver.cpp libcockatrice/deck_list/sideboard_plan.cpp ) diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp index 90f63c09d..1a3876cd3 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp @@ -1,7 +1,6 @@ #include "deck_list.h" #include "deck_list_memento.h" -#include "deck_list_plain_text_parser.h" #include "tree/abstract_deck_list_node.h" #include "tree/deck_list_card_node.h" #include "tree/inner_deck_list_node.h" @@ -9,138 +8,26 @@ #include #include #include +#include #include #include #include +#if QT_VERSION < 0x050600 +// qHash on QRegularExpression was added in 5.6, FIX IT +uint qHash(const QRegularExpression &key, uint seed) noexcept +{ + return qHash(key.pattern(), seed); // call qHash on pattern QString instead +} +#endif + static const QString CURRENT_SIDEBOARD_PLAN_KEY = ""; -/** - * @brief Parses a floating point XML attribute into a clamped playmat parameter. - * - * Falls back to @p fallback when the attribute is missing or malformed, so - * malformed deck files cannot produce degenerate art rectangles (e.g. a zoom - * of 0 dividing by zero). - * - * @param valueString Raw attribute text. - * @param fallback Value used when the text cannot be parsed. - * @param min Lower clamp bound. - * @param max Upper clamp bound. - * @return The parsed value clamped to [min, max], or @p fallback. - */ -static double parseClampedParam(const QString &valueString, double fallback, double min, double max) -{ - bool ok = false; - const double value = valueString.toDouble(&ok); - if (!ok) { - return fallback; - } - return qBound(min, value, max); -} - -/** - * @brief Reads a `bannerCard` element from the XML stream. - * - * @param xml Reader positioned at the element. - * @return The referenced card. - */ -static CardRef readBannerCard(QXmlStreamReader *xml) -{ - QString providerId = xml->attributes().value("providerId").toString(); - QString cardName = xml->readElementText(); - return {cardName, providerId}; -} - -/** - * @brief Reads a `playmatCard` element from the XML stream. - * - * Attribute values are read before readElementText consumes the element, and - * the params are clamped to the same ranges as the settings dialog and the - * remote player-properties path so malformed deck files cannot produce - * degenerate art rectangles (e.g. a zoom of 0 dividing by zero). - * - * @param xml Reader positioned at the element. - * @return The referenced card plus its clamped positioning parameters. - */ -static PlaymatInfo readPlaymatCard(QXmlStreamReader *xml) -{ - QString providerId = xml->attributes().value("providerId").toString(); - QString marginLStr = xml->attributes().value("marginPctL").toString(); - QString marginRStr = xml->attributes().value("marginPctR").toString(); - QString vOffStr = xml->attributes().value("verticalOffset").toString(); - QString zoomStr = xml->attributes().value("zoom").toString(); - QString cardName = xml->readElementText(); - - return { - .card = {cardName, providerId}, - .params = {.marginPctL = parseClampedParam(marginLStr, 0.07, 0.0, 0.95), - .marginPctR = parseClampedParam(marginRStr, 0.07, 0.0, 0.95), - .verticalOffset = parseClampedParam(vOffStr, 0.33, 0.0, 1.0), - .zoom = parseClampedParam(zoomStr, 1.0, 0.1, 4.0)}, - }; -} - bool DeckList::Metadata::isEmpty() const { return name.isEmpty() && comments.isEmpty() && bannerCard.isEmpty() && tags.isEmpty() && playmat.card.isEmpty(); } -bool DeckList::Metadata::readElement(QXmlStreamReader *xml, const QString &childName) -{ - if (childName == "lastLoadedTimestamp") { - lastLoadedTimestamp = xml->readElementText(); - } else if (childName == "deckname") { - name = xml->readElementText(); - } else if (childName == "format") { - gameFormat = xml->readElementText(); - } else if (childName == "comments") { - comments = xml->readElementText(); - } else if (childName == "bannerCard") { - bannerCard = readBannerCard(xml); - } else if (childName == "playmatCard") { - playmat = readPlaymatCard(xml); - } else if (childName == "tags") { - tags.clear(); // Clear existing tags - while (xml->readNextStartElement()) { - if (xml->name().toString() == "tag") { - tags.append(xml->readElementText()); - } - } - } else { - return false; - } - return true; -} - -void DeckList::Metadata::write(QXmlStreamWriter *xml) const -{ - xml->writeTextElement("lastLoadedTimestamp", lastLoadedTimestamp); - xml->writeTextElement("deckname", name); - xml->writeTextElement("format", gameFormat); - xml->writeStartElement("bannerCard"); - xml->writeAttribute("providerId", bannerCard.providerId); - xml->writeCharacters(bannerCard.name); - xml->writeEndElement(); - if (!playmat.card.isEmpty()) { - xml->writeStartElement("playmatCard"); - xml->writeAttribute("providerId", playmat.card.providerId); - xml->writeAttribute("marginPctL", QString::number(playmat.params.marginPctL, 'f', 4)); - xml->writeAttribute("marginPctR", QString::number(playmat.params.marginPctR, 'f', 4)); - xml->writeAttribute("verticalOffset", QString::number(playmat.params.verticalOffset, 'f', 4)); - xml->writeAttribute("zoom", QString::number(playmat.params.zoom, 'f', 4)); - xml->writeCharacters(playmat.card.name); - xml->writeEndElement(); - } - xml->writeTextElement("comments", comments); - - // Write tags - xml->writeStartElement("tags"); - for (const QString &tag : tags) { - xml->writeTextElement("tag", tag); - } - xml->writeEndElement(); -} - DeckList::DeckList() { } @@ -175,10 +62,56 @@ bool DeckList::readElement(QXmlStreamReader *xml) { const QString childName = xml->name().toString(); if (xml->isStartElement()) { - if (metadata.readElement(xml, childName)) { - return true; - } - if (childName == "zone") { + if (childName == "lastLoadedTimestamp") { + metadata.lastLoadedTimestamp = xml->readElementText(); + } else if (childName == "deckname") { + metadata.name = xml->readElementText(); + } else if (childName == "format") { + metadata.gameFormat = xml->readElementText(); + } else if (childName == "comments") { + metadata.comments = xml->readElementText(); + } else if (childName == "bannerCard") { + QString providerId = xml->attributes().value("providerId").toString(); + QString cardName = xml->readElementText(); + metadata.bannerCard = {cardName, providerId}; + } else if (childName == "playmatCard") { + QString providerId = xml->attributes().value("providerId").toString(); + bool ok; + QString marginLStr = xml->attributes().value("marginPctL").toString(); + QString marginRStr = xml->attributes().value("marginPctR").toString(); + QString vOffStr = xml->attributes().value("verticalOffset").toString(); + QString zoomStr = xml->attributes().value("zoom").toString(); + QString cardName = xml->readElementText(); + PlaymatInfo playmat; + playmat.card = {cardName, providerId}; + // Clamp to the same ranges as the settings dialog and the remote + // player-properties path so malformed deck files cannot produce + // degenerate art rectangles (e.g. a zoom of 0 dividing by zero). + playmat.params.marginPctL = qBound(0.0, marginLStr.toDouble(&ok), 0.95); + if (!ok) { + playmat.params.marginPctL = 0.07; + } + playmat.params.marginPctR = qBound(0.0, marginRStr.toDouble(&ok), 0.95); + if (!ok) { + playmat.params.marginPctR = 0.07; + } + playmat.params.verticalOffset = qBound(0.0, vOffStr.toDouble(&ok), 1.0); + if (!ok) { + playmat.params.verticalOffset = 0.33; + } + playmat.params.zoom = qBound(0.1, zoomStr.toDouble(&ok), 4.0); + if (!ok) { + playmat.params.zoom = 1.0; + } + metadata.playmat = playmat; + } else if (childName == "tags") { + metadata.tags.clear(); // Clear existing tags + while (xml->readNextStartElement()) { + if (xml->name().toString() == "tag") { + metadata.tags.append(xml->readElementText()); + } + } + } else if (childName == "zone") { tree.readZoneElement(xml); } else if (childName == "sideboard_plan") { SideboardPlan newSideboardPlan; @@ -192,12 +125,41 @@ bool DeckList::readElement(QXmlStreamReader *xml) return true; } +static void writeMetadata(QXmlStreamWriter *xml, const DeckList::Metadata &metadata) +{ + xml->writeTextElement("lastLoadedTimestamp", metadata.lastLoadedTimestamp); + xml->writeTextElement("deckname", metadata.name); + xml->writeTextElement("format", metadata.gameFormat); + xml->writeStartElement("bannerCard"); + xml->writeAttribute("providerId", metadata.bannerCard.providerId); + xml->writeCharacters(metadata.bannerCard.name); + xml->writeEndElement(); + if (!metadata.playmat.card.isEmpty()) { + xml->writeStartElement("playmatCard"); + xml->writeAttribute("providerId", metadata.playmat.card.providerId); + xml->writeAttribute("marginPctL", QString::number(metadata.playmat.params.marginPctL, 'f', 4)); + xml->writeAttribute("marginPctR", QString::number(metadata.playmat.params.marginPctR, 'f', 4)); + xml->writeAttribute("verticalOffset", QString::number(metadata.playmat.params.verticalOffset, 'f', 4)); + xml->writeAttribute("zoom", QString::number(metadata.playmat.params.zoom, 'f', 4)); + xml->writeCharacters(metadata.playmat.card.name); + xml->writeEndElement(); + } + xml->writeTextElement("comments", metadata.comments); + + // Write tags + xml->writeStartElement("tags"); + for (const QString &tag : metadata.tags) { + xml->writeTextElement("tag", tag); + } + xml->writeEndElement(); +} + void DeckList::write(QXmlStreamWriter *xml) const { xml->writeStartElement("cockatrice_deck"); xml->writeAttribute("version", "1"); - metadata.write(xml); + writeMetadata(xml, metadata); // Write zones tree.write(xml); @@ -210,27 +172,6 @@ void DeckList::write(QXmlStreamWriter *xml) const xml->writeEndElement(); // Close "cockatrice_deck" } -bool DeckList::seekToNextElement(QXmlStreamReader *xml) -{ - while (!xml->atEnd()) { - xml->readNext(); - if (xml->isStartElement()) { - return true; - } - } - return false; -} - -void DeckList::readDeckBody(QXmlStreamReader *xml) -{ - while (!xml->atEnd()) { - xml->readNext(); - if (!readElement(xml)) { - break; - } - } -} - bool DeckList::loadFromXml(QXmlStreamReader *xml) { if (xml->error()) { @@ -239,11 +180,19 @@ bool DeckList::loadFromXml(QXmlStreamReader *xml) } cleanList(); - while (seekToNextElement(xml)) { - if (xml->name().toString() != "cockatrice_deck") { - return false; + while (!xml->atEnd()) { + xml->readNext(); + if (xml->isStartElement()) { + if (xml->name().toString() != "cockatrice_deck") { + return false; + } + while (!xml->atEnd()) { + xml->readNext(); + if (!readElement(xml)) { + break; + } + } } - readDeckBody(xml); } refreshDeckHash(); if (xml->error()) { @@ -299,12 +248,160 @@ bool DeckList::loadFromStream_Plain(QTextStream &in, bool preserveMetadata, const std::function &cardNameNormalizer) { - if (!preserveMetadata) { - metadata = {}; + const QRegularExpression reCardLine(R"(^\s*[\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption); + const QRegularExpression reEmpty("^\\s*$"); + const QRegularExpression reComment(R"([\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption); + const QRegularExpression reSBMark("^\\s*sb:\\s*(.+)", QRegularExpression::CaseInsensitiveOption); + const QRegularExpression reSBComment("^sideboard\\b.*$", QRegularExpression::CaseInsensitiveOption); + const QRegularExpression reDeckComment("^((main)?deck(list)?|mainboard)\\b", + QRegularExpression::CaseInsensitiveOption); + + // Regex for advanced card parsing + const QRegularExpression reMultiplier(R"(^[xX\(\[]*(\d+)[xX\*\)\]]* ?(.+))"); + + // Regex for extracting set code and collector number with attached symbols + const QRegularExpression reHyphenFormat(R"(\((\w{3,})\)\s+(\w{3,})-(\d+[^\w\s]*))"); + const QRegularExpression reRegularFormat(R"(\((\w{3,})\)\s+(\d+[^\w\s]*))"); + + cleanList(preserveMetadata); + + auto inputs = in.readAll().trimmed().split('\n'); + auto max_line = inputs.size(); + + // Start at the first empty line before the first card line + auto deckStart = inputs.indexOf(reCardLine); + if (deckStart == -1) { + if (inputs.indexOf(reComment) == -1) { + return false; // Input is empty + } + deckStart = max_line; + } else { + deckStart = inputs.lastIndexOf(reEmpty, deckStart); + if (deckStart == -1) { + deckStart = 0; + } } - bool ok = DeckListPlainText::parse(in, cardNameNormalizer, metadata, tree); + + // find sideboard position, if marks are used this won't be needed + int sBStart = -1; + if (inputs.indexOf(reSBMark, deckStart) == -1) { + sBStart = inputs.indexOf(reSBComment, deckStart); + if (sBStart == -1) { + sBStart = inputs.indexOf(reEmpty, deckStart + 1); + if (sBStart == -1) { + sBStart = max_line; + } + auto nextCard = inputs.indexOf(reCardLine, sBStart + 1); + if (inputs.indexOf(reEmpty, nextCard + 1) != -1) { + sBStart = max_line; + } + } + } + + int index = 0; + QRegularExpressionMatch match; + + // Parse name and comments + while (index < deckStart) { + const auto ¤t = inputs.at(index++); + if (!current.contains(reEmpty)) { + match = reComment.match(current); + metadata.name = match.captured(); + break; + } + } + while (index < deckStart) { + const auto ¤t = inputs.at(index++); + if (!current.contains(reEmpty)) { + match = reComment.match(current); + metadata.comments += match.captured() + '\n'; + } + } + metadata.comments.chop(1); + + // Discard empty lines + while (index < max_line && inputs.at(index).contains(reEmpty)) { + ++index; + } + + // Discard line if it starts with deck or mainboard, all cards until the sideboard starts are in the mainboard + if (inputs.at(index).contains(reDeckComment)) { + ++index; + } + + // Parse decklist + for (; index < max_line; ++index) { + // check if line is a card + match = reCardLine.match(inputs.at(index)); + if (!match.hasMatch()) { + continue; + } + + QString cardName = match.captured().simplified(); + bool sideboard = false; + + // Sideboard detection + if (sBStart < 0) { + match = reSBMark.match(cardName); + if (match.hasMatch()) { + sideboard = true; + cardName = match.captured(1); + } + } else { + if (index == sBStart) { + continue; + } + sideboard = index > sBStart; + } + + // Extract set code, collector number, and foil + QString setCode; + QString collectorNumber; + bool isFoil = false; + + // Check for foil status at the end of the card name + if (cardName.endsWith("*F*", Qt::CaseInsensitive)) { + isFoil = true; + cardName.chop(3); // Remove the "*F*" from the card name + } + Q_UNUSED(isFoil); + + // Attempt to match the hyphen-separated format (PLST-2094) + match = reHyphenFormat.match(cardName); + if (match.hasMatch()) { + setCode = match.captured(2).toUpper(); + collectorNumber = match.captured(3); + cardName = cardName.left(match.capturedStart()).trimmed(); + } else { + // Attempt to match the regular format (PLST) 2094 + match = reRegularFormat.match(cardName); + if (match.hasMatch()) { + setCode = match.captured(1).toUpper(); + collectorNumber = match.captured(2); + cardName = cardName.left(match.capturedStart()).trimmed(); + } + } + + // check if a specific amount is mentioned + int amount = 1; + match = reMultiplier.match(cardName); + if (match.hasMatch()) { + amount = match.captured(1).toInt(); + cardName = match.captured(2); + } + + // Normalize the card name + cardName = cardNameNormalizer(cardName); + + // Determine the zone (mainboard/sideboard) + QString zoneName = sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN; + + // make new entry in decklist + tree.addCard(cardName, amount, zoneName, -1, setCode, collectorNumber); + } + refreshDeckHash(); - return ok; + return true; } bool DeckList::loadFromFile_Plain(QIODevice *device, const std::function &cardNameNormalizer) diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h index 199d3a9a8..475d99560 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h @@ -76,23 +76,6 @@ public: * @brief Checks if all values (except for lastLoadedTimestamp) in the metadata is empty. */ bool isEmpty() const; - - /** - * @brief Reads a single deck metadata element from a Cockatrice deck XML stream. - * - * @param xml Reader positioned at the element. - * @param childName Name of the current element. - * @return true if a metadata element was consumed, false if @p childName is - * not a metadata element. - */ - bool readElement(QXmlStreamReader *xml, const QString &childName); - - /** - * @brief Writes the deck metadata section of a Cockatrice deck XML file. - * - * @param xml Writer to append the metadata elements to. - */ - void write(QXmlStreamWriter *xml) const; }; private: @@ -106,22 +89,6 @@ private: */ mutable QString cachedDeckHash; - /** @name XML load helpers */ - ///@{ - /** - * @brief Advances to the next element in the XML stream. - * @param xml Reader to advance past non-element tokens. - * @return true when a start element was reached, false at end of stream. - */ - bool seekToNextElement(QXmlStreamReader *xml); - - /** - * @brief Reads the contents of a `cockatrice_deck` element into this deck. - * @param xml Reader positioned at the deck element, stopped at its end. - */ - void readDeckBody(QXmlStreamReader *xml); - ///@} - public: /** @name Metadata setters */ ///@{ diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.cpp index b6d0687b4..acf4707ab 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.cpp @@ -14,32 +14,42 @@ void DeckListHistoryManager::clear() emit undoRedoStateChanged(); } -void DeckListHistoryManager::restoreAndSwap(QStack &source, - QStack &target, - DeckList *deck) +void DeckListHistoryManager::undo(DeckList *deck) { - if (source.isEmpty()) { + if (undoStack.isEmpty()) { return; } - // The reason is read before the source is popped. - const QString reason = source.top().getReason(); + // Peek at the memento we are going to restore + const DeckListMemento &mementoToRestore = undoStack.top(); - // Save the current state so the opposite direction can return to it. - target.push(deck->createMemento(reason)); + // Save current state for redo + DeckListMemento currentState = deck->createMemento(mementoToRestore.getReason()); + redoStack.push(currentState); - // Apply the state we are moving to. - deck->restoreMemento(source.pop()); + // Pop the last state from undo stack and restore it + DeckListMemento memento = undoStack.pop(); + deck->restoreMemento(memento); emit undoRedoStateChanged(); } -void DeckListHistoryManager::undo(DeckList *deck) -{ - restoreAndSwap(undoStack, redoStack, deck); -} - void DeckListHistoryManager::redo(DeckList *deck) { - restoreAndSwap(redoStack, undoStack, deck); + if (redoStack.isEmpty()) { + return; + } + + // Peek at the memento we are going to restore + const DeckListMemento &mementoToRestore = redoStack.top(); + + // Save current state for undo + DeckListMemento currentState = deck->createMemento(mementoToRestore.getReason()); + undoStack.push(currentState); + + // Pop the next state from redo stack and restore it + DeckListMemento memento = redoStack.pop(); + deck->restoreMemento(memento); + + emit undoRedoStateChanged(); } diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.h index 6acdb199e..e6bd27e2d 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.h @@ -47,13 +47,6 @@ public: } private: - /** - * @brief Moves one state from @p source to @p target, applying it to @p deck. - * - * Used by both undo (undoStack -> redoStack) and redo (redoStack -> undoStack). - */ - void restoreAndSwap(QStack &source, QStack &target, DeckList *deck); - QStack undoStack; QStack redoStack; }; diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.cpp index 66f228d19..91fe1874b 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.cpp @@ -7,84 +7,6 @@ static constexpr int MAX_DECK_SIZE = 1e5; -namespace -{ - -/** - * @brief Expands card nodes into one lowercase name entry per copy. - * - * @param nodes The card nodes to expand. - * @param prefix Optional prefix prepended to every entry (e.g. "SB:" for the sideboard). - * @return One entry per copy, in node order. - */ -QStringList cardNodesToCopies(const QList &nodes, const QString &prefix = {}) -{ - QStringList result; - for (auto node : nodes) { - for (int i = 0; i < node->getNumber(); ++i) { - result.append(prefix + node->getName().toLower()); - } - } - return result; -} - -/** - * @brief Packs the first five bytes of a SHA-1 digest into a compact base-32 number. - * - * The bytes are placed in most-significant-byte-first order (byte 0 shifted by 32, - * byte 4 unshifted) so decks that only differ in their low-order hash bytes still - * produce distinct identifiers. - * - * @return The 8-character base-32 representation of the packed number. - */ -QString encodeDeckHash(const QByteArray &digest) -{ - quint64 number = 0; - for (int i = 0; i < 5; ++i) { - number |= static_cast(static_cast(digest[i])) << (32 - 8 * i); - } - return QString::number(number, 32).rightJustified(8, '0'); -} - -/** - * @brief Collects every card node in @p node's subtree, in tree order. - * - * @return The collected card nodes. - */ -QList collectCardsRecursive(const InnerDecklistNode *node) -{ - QList result; - for (int i = 0; i < node->size(); i++) { - if (auto *card = dynamic_cast(node->at(i))) { - result.append(card); - } else if (auto *inner = dynamic_cast(node->at(i))) { - result.append(collectCardsRecursive(inner)); - } - } - return result; -} - -/** - * @brief Invokes @p func on every card in @p node's subtree. - * - * Cards nested in custom zones are reported with their top-level @p boardZone - * so that callers can classify cards by board (main/side/maybeboard/tokens). - */ -void forEachCardInNode(InnerDecklistNode *boardZone, - InnerDecklistNode *node, - const std::function &func) -{ - for (int i = 0; i < node->size(); i++) { - if (auto *card = dynamic_cast(node->at(i))) { - func(boardZone, card); - } else if (auto *inner = dynamic_cast(node->at(i))) { - forEachCardInNode(boardZone, inner, func); - } - } -} - -} // namespace - DecklistNodeTree::DecklistNodeTree() : root(new InnerDecklistNode()) { } @@ -121,8 +43,19 @@ QList DecklistNodeTree::getCardNodes(const QSet result; + std::function collectCards = [&collectCards, + &result](const InnerDecklistNode *node) { + for (int i = 0; i < node->size(); i++) { + if (auto *card = dynamic_cast(node->at(i))) { + result.append(card); + } else if (auto *inner = dynamic_cast(node->at(i))) { + collectCards(inner); + } + } + }; + for (auto *zoneNode : getZoneNodes(restrictToZones)) { - result.append(collectCardsRecursive(zoneNode)); + collectCards(zoneNode); } return result; @@ -150,11 +83,25 @@ QString DecklistNodeTree::computeDeckHash() const auto mainDeckNodes = getCardNodes({DECK_ZONE_MAIN}); auto sideDeckNodes = getCardNodes({DECK_ZONE_SIDE}); - QStringList cardList = cardNodesToCopies(mainDeckNodes) + cardNodesToCopies(sideDeckNodes, "SB:"); + static auto nodesToCardList = [](const QList &nodes, const QString &prefix = {}) { + QStringList result; + for (auto node : nodes) { + for (int i = 0; i < node->getNumber(); ++i) { + result.append(prefix + node->getName().toLower()); + } + } + return result; + }; + + QStringList cardList = nodesToCardList(mainDeckNodes) + nodesToCardList(sideDeckNodes, "SB:"); cardList.sort(); QByteArray deckHashArray = QCryptographicHash::hash(cardList.join(";").toUtf8(), QCryptographicHash::Sha1); - return encodeDeckHash(deckHashArray); + quint64 number = (((quint64)(unsigned char)deckHashArray[0]) << 32) + + (((quint64)(unsigned char)deckHashArray[1]) << 24) + + (((quint64)(unsigned char)deckHashArray[2] << 16)) + + (((quint64)(unsigned char)deckHashArray[3]) << 8) + (quint64)(unsigned char)deckHashArray[4]; + return QString::number(number, 32).rightJustified(8, '0'); } void DecklistNodeTree::write(QXmlStreamWriter *xml) const @@ -201,7 +148,12 @@ bool DecklistNodeTree::deleteNode(AbstractDecklistNode *node, InnerDecklistNode int index = rootNode->indexOf(node); if (index != -1) { delete rootNode->takeAt(index); - pruneEmptyBoardZone(rootNode); + + // Empty custom zones are kept while empty board zones get pruned. + if (rootNode->empty() && rootNode->getParent() == root) { + deleteNode(rootNode, rootNode->getParent()); + } + return true; } @@ -217,18 +169,24 @@ bool DecklistNodeTree::deleteNode(AbstractDecklistNode *node, InnerDecklistNode return false; } -void DecklistNodeTree::pruneEmptyBoardZone(InnerDecklistNode *container) -{ - if (container->isEmpty() && container->getParent() == root) { - deleteNode(container, container->getParent()); - } -} - void DecklistNodeTree::forEachCard(const std::function &func) const { + // Cards nested in custom zones are reported with their top-level board zone + // so that callers can classify cards by board (main/side/maybeboard/tokens). + std::function walk = [&func, &walk](InnerDecklistNode *boardZone, + InnerDecklistNode *node) { + for (int i = 0; i < node->size(); i++) { + if (auto *card = dynamic_cast(node->at(i))) { + func(boardZone, card); + } else if (auto *inner = dynamic_cast(node->at(i))) { + walk(boardZone, inner); + } + } + }; + for (int i = 0; i < root->size(); i++) { if (auto *zone = dynamic_cast(root->at(i))) { - forEachCardInNode(zone, zone, func); + walk(zone, zone); } } } @@ -239,7 +197,7 @@ void DecklistNodeTree::forEachCard(const std::functionsize(); i++) { auto *node = dynamic_cast(root->at(i)); diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h index 2c66b34ff..5d91cd233 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h @@ -144,17 +144,9 @@ public: private: // Helpers for traversing the tree - InnerDecklistNode *getZoneObjFromName(const QString &zoneName); + InnerDecklistNode *getZoneObjFromName(const QString &zoneName) const; InnerDecklistNode *findBoardZone(const QString &boardZoneName) const; InnerDecklistNode *findOrCreateBoardZone(const QString &boardZoneName); - - /** - * @brief Recursively removes @p container when it is an empty board zone. - * - * Empty custom zones are kept while empty board zones get pruned, so a - * board zone disappears once its last card or custom zone goes away. - */ - void pruneEmptyBoardZone(InnerDecklistNode *container); }; #endif // COCKATRICE_DECKLIST_NODE_TREE_H diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_plain_text_parser.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_plain_text_parser.cpp deleted file mode 100644 index de50d743b..000000000 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_plain_text_parser.cpp +++ /dev/null @@ -1,172 +0,0 @@ -#include "deck_list_plain_text_parser.h" - -#include "deck_list_node_tree.h" -#include "tree/inner_deck_list_node.h" - -#include -#include - -namespace DeckListPlainText -{ - -bool parse(QTextStream &in, - const std::function &cardNameNormalizer, - DeckList::Metadata &metadata, - DecklistNodeTree &tree) -{ - tree.clear(); - - static const QRegularExpression reCardLine(R"(^\s*[\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption); - static const QRegularExpression reEmpty("^\\s*$"); - static const QRegularExpression reComment(R"([\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption); - static const QRegularExpression reSBMark("^\\s*sb:\\s*(.+)", QRegularExpression::CaseInsensitiveOption); - static const QRegularExpression reSBComment("^sideboard\\b.*$", QRegularExpression::CaseInsensitiveOption); - static const QRegularExpression reDeckComment("^((main)?deck(list)?|mainboard)\\b", - QRegularExpression::CaseInsensitiveOption); - - // Regex for advanced card parsing - static const QRegularExpression reMultiplier(R"(^[xX\(\[]*(\d+)[xX\*\)\]]* ?(.+))"); - - // Regex for extracting set code and collector number with attached symbols - static const QRegularExpression reHyphenFormat(R"(\((\w{3,})\)\s+(\w{3,})-(\d+[^\w\s]*))"); - static const QRegularExpression reRegularFormat(R"(\((\w{3,})\)\s+(\d+[^\w\s]*))"); - - auto inputs = in.readAll().trimmed().split('\n'); - auto max_line = inputs.size(); - - // Start at the first empty line before the first card line - auto deckStart = inputs.indexOf(reCardLine); - if (deckStart == -1) { - if (inputs.indexOf(reComment) == -1) { - return false; // Input is empty - } - deckStart = max_line; - } else { - deckStart = inputs.lastIndexOf(reEmpty, deckStart); - if (deckStart == -1) { - deckStart = 0; - } - } - - // find sideboard position, if marks are used this won't be needed - int sBStart = -1; - if (inputs.indexOf(reSBMark, deckStart) == -1) { - sBStart = inputs.indexOf(reSBComment, deckStart); - if (sBStart == -1) { - sBStart = inputs.indexOf(reEmpty, deckStart + 1); - if (sBStart == -1) { - sBStart = max_line; - } - auto nextCard = inputs.indexOf(reCardLine, sBStart + 1); - if (inputs.indexOf(reEmpty, nextCard + 1) != -1) { - sBStart = max_line; - } - } - } - - int index = 0; - QRegularExpressionMatch match; - - // Parse name and comments - while (index < deckStart) { - const auto ¤t = inputs.at(index++); - if (!current.contains(reEmpty)) { - match = reComment.match(current); - metadata.name = match.captured(); - break; - } - } - while (index < deckStart) { - const auto ¤t = inputs.at(index++); - if (!current.contains(reEmpty)) { - match = reComment.match(current); - metadata.comments += match.captured() + '\n'; - } - } - metadata.comments.chop(1); - - // Discard empty lines - while (index < max_line && inputs.at(index).contains(reEmpty)) { - ++index; - } - - // Discard line if it starts with deck or mainboard, all cards until the sideboard starts are in the mainboard - if (inputs.at(index).contains(reDeckComment)) { - ++index; - } - - // Parse decklist - for (; index < max_line; ++index) { - // check if line is a card - match = reCardLine.match(inputs.at(index)); - if (!match.hasMatch()) { - continue; - } - - QString cardName = match.captured().simplified(); - bool sideboard = false; - - // Sideboard detection - if (sBStart < 0) { - match = reSBMark.match(cardName); - if (match.hasMatch()) { - sideboard = true; - cardName = match.captured(1); - } - } else { - if (index == sBStart) { - continue; - } - sideboard = index > sBStart; - } - - // Extract set code, collector number, and foil - QString setCode; - QString collectorNumber; - bool isFoil = false; - - // Check for foil status at the end of the card name - if (cardName.endsWith("*F*", Qt::CaseInsensitive)) { - isFoil = true; - cardName.chop(3); // Remove the "*F*" from the card name - } - Q_UNUSED(isFoil); - - // Attempt to match the hyphen-separated format (PLST-2094) - match = reHyphenFormat.match(cardName); - if (match.hasMatch()) { - setCode = match.captured(2).toUpper(); - collectorNumber = match.captured(3); - cardName = cardName.left(match.capturedStart()).trimmed(); - } else { - // Attempt to match the regular format (PLST) 2094 - match = reRegularFormat.match(cardName); - if (match.hasMatch()) { - setCode = match.captured(1).toUpper(); - collectorNumber = match.captured(2); - cardName = cardName.left(match.capturedStart()).trimmed(); - } - } - - // check if a specific amount is mentioned - int amount = 1; - match = reMultiplier.match(cardName); - if (match.hasMatch()) { - amount = match.captured(1).toInt(); - cardName = match.captured(2); - } - - // Normalize the card name - cardName = cardNameNormalizer(cardName); - - // Determine the zone (mainboard/sideboard) - QString zoneName = sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN; - - // make new entry in decklist - tree.addCard(cardName, amount, zoneName, -1, setCode, collectorNumber); - } - - return true; -} - -} // namespace DeckListPlainText \ No newline at end of file diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_plain_text_parser.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_plain_text_parser.h deleted file mode 100644 index e0f456a18..000000000 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_plain_text_parser.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef COCKATRICE_DECK_LIST_PLAIN_TEXT_PARSER_H -#define COCKATRICE_DECK_LIST_PLAIN_TEXT_PARSER_H - -#include "deck_list.h" - -#include -#include - -class QTextStream; - -namespace DeckListPlainText -{ - -/** - * @brief Parses a plain-text deck list into a tree and its metadata. - * - * Clears the tree first, then fills both from the text. - * - * @param in The text to load - * @param cardNameNormalizer Function that takes the parsed card name string - * in the text and returns the name to store - * @param metadata Deck metadata written by the parser - * @param tree Deck tree the parser adds cards to - * @return False if the input was empty, true otherwise. - */ -bool parse(QTextStream &in, - const std::function &cardNameNormalizer, - DeckList::Metadata &metadata, - DecklistNodeTree &tree); - -} // namespace DeckListPlainText - -#endif // COCKATRICE_DECK_LIST_PLAIN_TEXT_PARSER_H \ No newline at end of file diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp index 855062c18..a76fed619 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp @@ -2,32 +2,6 @@ #include -namespace -{ - -void readMoveCardToZone(QXmlStreamReader *xml, QList &moveList) -{ - MoveCard_ToZone move; - while (!xml->atEnd()) { - xml->readNext(); - const QString childName = xml->name().toString(); - if (xml->isStartElement()) { - if (childName == "card_name") { - move.set_card_name(xml->readElementText().toStdString()); - } else if (childName == "start_zone") { - move.set_start_zone(xml->readElementText().toStdString()); - } else if (childName == "target_zone") { - move.set_target_zone(xml->readElementText().toStdString()); - } - } else if (xml->isEndElement() && (childName == "move_card_to_zone")) { - moveList.append(move); - return; - } - } -} - -} // namespace - SideboardPlan::SideboardPlan(const QString &_name, const QList &_moveList) : name(_name), moveList(_moveList) { @@ -47,7 +21,23 @@ bool SideboardPlan::readElement(QXmlStreamReader *xml) if (childName == "name") { name = xml->readElementText(); } else if (childName == "move_card_to_zone") { - readMoveCardToZone(xml, moveList); + MoveCard_ToZone m; + while (!xml->atEnd()) { + xml->readNext(); + const QString childName2 = xml->name().toString(); + if (xml->isStartElement()) { + if (childName2 == "card_name") { + m.set_card_name(xml->readElementText().toStdString()); + } else if (childName2 == "start_zone") { + m.set_start_zone(xml->readElementText().toStdString()); + } else if (childName2 == "target_zone") { + m.set_target_zone(xml->readElementText().toStdString()); + } + } else if (xml->isEndElement() && (childName2 == "move_card_to_zone")) { + moveList.append(m); + break; + } + } } } else if (xml->isEndElement() && (childName == "sideboard_plan")) { return true; diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.cpp index 685238d53..7200ede5f 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.cpp @@ -34,6 +34,17 @@ bool AbstractDecklistCardNode::compareName(AbstractDecklistNode *other) const } } +int AbstractDecklistCardNode::readElement(QXmlStreamReader *xml, int /* limit */) +{ + while (!xml->atEnd()) { + xml->readNext(); + if (xml->isEndElement() && xml->name().toString() == "card") { + return 0; + } + } + return 0; +} + void AbstractDecklistCardNode::writeElement(QXmlStreamWriter *xml) { xml->writeEmptyElement("card"); diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h index 0942a4601..52dd56529 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h @@ -134,6 +134,15 @@ public: */ bool compareName(AbstractDecklistNode *other) const; + /** + * @brief Deserialize this node’s properties from XML. + * @param xml QXmlStreamReader positioned at the element. + * @return true if parsing succeeded. + * + * This supports loading deck files from Cockatrice’s XML format. + */ + int readElement(QXmlStreamReader *xml, int limit) override; + /** * @brief Serialize this node’s properties to XML. * @param xml Writer to append this node’s XML element. diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_node.h b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_node.h index 38d1d44b8..9c4290db0 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_node.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_node.h @@ -179,10 +179,11 @@ public: /** * @name XML serialization - * This method supports writing this node and its children to the + * These methods support reading and writing decks from/to * Cockatrice deck XML format. * @{ */ + virtual int readElement(QXmlStreamReader *xml, int limit) = 0; virtual void writeElement(QXmlStreamWriter *xml) = 0; /// @} }; diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp index 86f7f5363..d082b3cca 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp @@ -151,29 +151,25 @@ bool InnerDecklistNode::compareName(AbstractDecklistNode *other) const } } -int InnerDecklistNode::readCardElement(QXmlStreamReader *xml, int remainingBudget) -{ - const int amount = qMin(xml->attributes().value("number").toString().toInt(), remainingBudget); - new DecklistCardNode(xml->attributes().value("name").toString(), amount, this, -1, - xml->attributes().value("setShortName").toString(), - xml->attributes().value("collectorNumber").toString(), - xml->attributes().value("uuid").toString()); - return amount; -} - int InnerDecklistNode::readElement(QXmlStreamReader *xml, int limit) { int totalCards = 0; while (!xml->atEnd()) { xml->readNext(); const QString childName = xml->name().toString(); - const int remainingBudget = limit - totalCards; if (xml->isStartElement()) { if (childName == "zone") { auto *newZone = new InnerDecklistNode(xml->attributes().value("name").toString(), this); - totalCards += newZone->readElement(xml, remainingBudget); + totalCards += newZone->readElement(xml, limit - totalCards); } else if (childName == "card") { - totalCards += readCardElement(xml, remainingBudget); + int amount = xml->attributes().value("number").toString().toInt(); + amount = qMin(amount, limit - totalCards); + auto *newCard = new DecklistCardNode(xml->attributes().value("name").toString(), amount, this, -1, + xml->attributes().value("setShortName").toString(), + xml->attributes().value("collectorNumber").toString(), + xml->attributes().value("uuid").toString()); + totalCards += amount; + totalCards += newCard->readElement(xml, limit - totalCards); } } else if (xml->isEndElement() && (childName == "zone")) { return totalCards; @@ -192,35 +188,31 @@ void InnerDecklistNode::writeElement(QXmlStreamWriter *xml) xml->writeEndElement(); // zone } -QVector> InnerDecklistNode::indexedSnapshot() const -{ - QVector> snapshot(size()); - for (int i = size() - 1; i >= 0; --i) { - snapshot[i].first = i; - snapshot[i].second = at(i); - } - return snapshot; -} - -QVector> InnerDecklistNode::applySortedOrder(const QVector> &sorted) -{ - QVector> result(size()); - for (int i = size() - 1; i >= 0; --i) { - result[i].first = sorted[i].first; - result[i].second = i; - replace(i, sorted[i].second); - } - return result; -} - QVector> InnerDecklistNode::sort(Qt::SortOrder order) { - auto snapshot = indexedSnapshot(); + QVector> result(size()); + // Initialize temporary list with contents of current list + QVector> tempList(size()); + for (int i = size() - 1; i >= 0; --i) { + tempList[i].first = i; + tempList[i].second = at(i); + } + + // Sort temporary list auto cmp = [order](const auto &a, const auto &b) { return (order == Qt::AscendingOrder) ? (b.second->compare(a.second)) : (a.second->compare(b.second)); }; - std::sort(snapshot.begin(), snapshot.end(), cmp); - return applySortedOrder(snapshot); + std::sort(tempList.begin(), tempList.end(), cmp); + + // Map old indexes to new indexes and + // copy temporary list to the current one + for (int i = size() - 1; i >= 0; --i) { + result[i].first = tempList[i].first; + result[i].second = i; + replace(i, tempList[i].second); + } + + return result; } diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h index 8404d7116..0d454c11e 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h @@ -223,45 +223,19 @@ public: */ QVector> sort(Qt::SortOrder order = Qt::AscendingOrder); -private: - /** - * @brief Snapshots the current children as (old index, node) pairs. - */ - QVector> indexedSnapshot() const; - - /** - * @brief Replaces this node's children with @p sorted and maps old indexes to new ones. - * - * @return A list of (old index, new index) pairs for each reordered child. - */ - QVector> applySortedOrder(const QVector> &sorted); - -public: /** * @brief Deserialize this node and its children from XML. * @param xml Reader positioned at this element. * @param limit The maximum amount of cards to read * @return the amount of cards found */ - int readElement(QXmlStreamReader *xml, int limit); + int readElement(QXmlStreamReader *xml, int limit) override; /** * @brief Serialize this node and its children to XML. * @param xml Writer to append elements to. */ void writeElement(QXmlStreamWriter *xml) override; - -private: - /** - * @brief Reads a single `card` element and appends it to this node. - * - * The card's quantity is capped at @p remainingBudget so a malicious or - * oversized deck file cannot push the total card count past the deck size - * limit. - * - * @return The amount of cards actually added. - */ - int readCardElement(QXmlStreamReader *xml, int remainingBudget); }; #endif // COCKATRICE_INNER_DECK_LIST_NODE_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h index 304ab3cb7..3f2cbbe8e 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h @@ -1,8 +1,6 @@ #ifndef COCKATRICE_INTERFACE_CARDS_DISPLAY_SETTINGS_PROVIDER_H #define COCKATRICE_INTERFACE_CARDS_DISPLAY_SETTINGS_PROVIDER_H -#include - class ICardsDisplaySettingsProvider { public: @@ -28,7 +26,6 @@ public: [[nodiscard]] virtual int getEDHRecCardSize() const = 0; [[nodiscard]] virtual int getArchidektPreviewSize() const = 0; [[nodiscard]] virtual int getSampleHandSize() const = 0; - [[nodiscard]] virtual QString getCardLang() const = 0; }; #endif // COCKATRICE_INTERFACE_CARDS_DISPLAY_SETTINGS_PROVIDER_H diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp index a5628a844..76afca0c4 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -28,16 +28,6 @@ DeckListModel::~DeckListModel() delete root; } -void DeckListModel::setDisplayLanguage(const QString &lang) -{ - if (displayLang == lang) { - return; - } - displayLang = lang; - emit layoutAboutToBeChanged(); - emit layoutChanged(); -} - /** * @brief Extract the value from the card that is used for the group criteria. * @param info Pointer to card information. @@ -191,15 +181,8 @@ QVariant DeckListModel::data(const QModelIndex &index, int role) const switch (index.column()) { case DeckListModelColumns::CARD_AMOUNT: return card->getNumber(); - case DeckListModelColumns::CARD_NAME: { - if (role == Qt::DisplayRole) { - CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(card->getName()); - if (info) { - return info->getLocalizedName(displayLang); - } - } + case DeckListModelColumns::CARD_NAME: return card->getName(); - } case DeckListModelColumns::CARD_SET: return card->getCardSetShortName(); case DeckListModelColumns::CARD_COLLECTOR_NUMBER: diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h index ce6d7f8cb..09600ca67 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h @@ -287,16 +287,6 @@ public: explicit DeckListModel(QObject *parent, const QSharedPointer &deckList); ~DeckListModel() override; - /** - * @brief Selects the language code for localized card names in the display role. - * - * The model never reads global settings itself; callers wire this to the card - * language setting (including reacting to its changes) rather than the model - * querying it. - * @param lang Language code; "en" shows the canonical English names. - */ - void setDisplayLanguage(const QString &lang); - /** * @brief Returns the root index of the model. * @return QModelIndex representing the root node. @@ -418,7 +408,6 @@ private: DeckListModelGroupCriteria::Type activeGroupCriteria = DeckListModelGroupCriteria::MAIN_TYPE; int lastKnownColumn; /**< Last column used for sorting. */ Qt::SortOrder lastKnownOrder; /**< Last known sort order. */ - QString displayLang = "en"; /**< Language code for localized card names in the display role. */ InnerDecklistNode *createNodeIfNeeded(const QString &name, InnerDecklistNode *parent); QModelIndex nodeToIndex(AbstractDecklistNode *node) const; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp index 131ff1077..799b1e7ee 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -465,7 +465,7 @@ Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user, if (asJudge && !(user->user_level() & ServerInfo_User::IsJudge)) { return Response::RespUserLevelTooLow; } - if (!(overrideRestrictions && (user->user_level() & (ServerInfo_User::IsModerator | ServerInfo_User::IsJudge)))) { + if (!(overrideRestrictions && (user->user_level() & ServerInfo_User::IsModerator))) { if ((_password != password) && !(spectator && !spectatorsNeedPassword)) { return Response::RespWrongPassword; } diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt index bc4814d5e..0791f8c14 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt @@ -15,17 +15,9 @@ set(PROTO_FILES command_deck_del.proto command_deck_del_dir.proto command_deck_download.proto - command_deck_download_public.proto command_deck_list.proto - command_deck_list_other_user.proto command_deck_new_dir.proto command_deck_select.proto - command_deck_set_visibility.proto - command_deck_share_create.proto - command_deck_share_download.proto - command_deck_share_list.proto - command_deck_share_list_mine.proto - command_deck_share_remove.proto command_deck_upload.proto command_del_counter.proto command_delete_arrow.proto @@ -145,10 +137,6 @@ set(PROTO_FILES response_card_art_rule_entry.proto response_deck_download.proto response_deck_list.proto - response_deck_share_create.proto - response_deck_share_download.proto - response_deck_share_list.proto - response_deck_share_list_mine.proto response_deck_upload.proto response_dump_zone.proto response_forgotpasswordrequest.proto @@ -187,8 +175,6 @@ set(PROTO_FILES serverinfo_cardcounter.proto serverinfo_chat_message.proto serverinfo_counter.proto - serverinfo_deck_share_item.proto - serverinfo_deck_share_summary.proto serverinfo_deckstorage.proto serverinfo_game.proto serverinfo_gametype.proto diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_download_public.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_download_public.proto deleted file mode 100644 index a5592ef51..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_download_public.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto2"; -import "session_commands.proto"; - -message Command_DeckDownloadPublic { - extend SessionCommand { - optional Command_DeckDownloadPublic ext = 1031; - } - optional uint32 deck_id = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_list_other_user.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_list_other_user.proto deleted file mode 100644 index 2459608e2..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_list_other_user.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto2"; -import "session_commands.proto"; - -message Command_DeckListOtherUser { - extend SessionCommand { - optional Command_DeckListOtherUser ext = 1029; - } - optional string user_name = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_set_visibility.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_set_visibility.proto deleted file mode 100644 index 3ada87973..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_set_visibility.proto +++ /dev/null @@ -1,13 +0,0 @@ -syntax = "proto2"; -import "session_commands.proto"; - -message Command_DeckSetVisibility { - extend SessionCommand { - optional Command_DeckSetVisibility ext = 1030; - } - // Set the public visibility of a single deck (mutually exclusive with folder_path). - optional uint32 deck_id = 1; - // Set the public visibility of a folder (all decks under it inherit). - optional string folder_path = 2; - optional bool is_public = 3; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_create.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_create.proto deleted file mode 100644 index 9cebf8eae..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_create.proto +++ /dev/null @@ -1,24 +0,0 @@ -syntax = "proto2"; -import "session_commands.proto"; - -message DeckShareItem { - // Reference an existing deck in the sharer's personal deck storage. - // Mutually exclusive with deck_list. - optional uint32 deck_id = 1; - // Inline deck content in the native format. - // Mutually exclusive with deck_id. - optional string deck_list = 2; - // Color identity of the deck (e.g. "WUBRG"), computed by the sharing client. - optional string color_identity = 3; -} - -message Command_DeckShareCreate { - extend SessionCommand { - optional Command_DeckShareCreate ext = 1026; - } - optional string name = 1; - repeated DeckShareItem items = 2; - // Path of a folder in the sharer's personal deck storage. When set, all - // decks in that folder are shared (resolved by the server). - optional string folder_path = 3; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_download.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_download.proto deleted file mode 100644 index 251a662b3..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_download.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto2"; -import "session_commands.proto"; - -message Command_DeckShareDownload { - extend SessionCommand { - optional Command_DeckShareDownload ext = 1028; - } - optional string token = 1; - optional uint32 item_id = 2; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_list.proto deleted file mode 100644 index b75fd65f9..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_list.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto2"; -import "session_commands.proto"; - -message Command_DeckShareList { - extend SessionCommand { - optional Command_DeckShareList ext = 1027; - } - optional string token = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_list_mine.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_list_mine.proto deleted file mode 100644 index 75bc30714..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_list_mine.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto2"; -import "session_commands.proto"; - -// Requests the list of share bundles created by the calling user, so they can -// be reviewed and revoked before they expire. -message Command_DeckShareListMine { - extend SessionCommand { - optional Command_DeckShareListMine ext = 1032; - } -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_remove.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_remove.proto deleted file mode 100644 index 348996da9..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_share_remove.proto +++ /dev/null @@ -1,11 +0,0 @@ -syntax = "proto2"; -import "session_commands.proto"; - -// Revokes one of the calling user's own share bundles. The referenced items -// are removed by cascade. -message Command_DeckShareRemove { - extend SessionCommand { - optional Command_DeckShareRemove ext = 1033; - } - optional uint32 share_id = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_upload.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_upload.proto index 1a5d44e9e..63d9c80ef 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_upload.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_upload.proto @@ -8,10 +8,4 @@ message Command_DeckUpload { optional string path = 1; // to upload a new deck optional uint32 deck_id = 2; // to replace an existing deck optional string deck_list = 3; - optional bool is_public = 4; // mark the deck public on upload (publish) - // The server derives the banner card and tags from deck_list, so clients only - // need to send the color identity, which cannot be computed server-side. - reserved 5, 6, 8; - reserved "banner_card_name", "banner_card_provider", "tags"; - optional string color_identity = 7; } diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto index caf9febde..42a42fcc0 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto @@ -81,10 +81,6 @@ message Response { REPLAY_LIST = 1100; // Response listing replays REPLAY_DOWNLOAD = 1101; // Response for replay download REPLAY_GET_CODE = 1102; // Response containing replay code - DECK_SHARE_CREATE = 1103; // Response to deck share creation - DECK_SHARE_LIST = 1104; // Response listing shared decks - DECK_SHARE_DOWNLOAD = 1105; // Response for shared deck download - DECK_SHARE_LIST_MINE = 1106; // Response listing the caller's own shares CARD_ART_RULE_LIST = 1200; // Response containing a list of card art rules } diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_create.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_create.proto deleted file mode 100644 index 574c02eb6..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_create.proto +++ /dev/null @@ -1,11 +0,0 @@ -syntax = "proto2"; -import "response.proto"; - -message Response_DeckShareCreate { - extend Response { - optional Response_DeckShareCreate ext = 1103; - } - optional string token = 1; - optional uint64 expires_at = 2; - optional uint32 item_count = 3; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_download.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_download.proto deleted file mode 100644 index def0ccbe6..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_download.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto2"; -import "response.proto"; - -message Response_DeckShareDownload { - extend Response { - optional Response_DeckShareDownload ext = 1105; - } - optional string deck = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_list.proto deleted file mode 100644 index 3edffa8ac..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_list.proto +++ /dev/null @@ -1,12 +0,0 @@ -syntax = "proto2"; -import "response.proto"; -import "serverinfo_deck_share_item.proto"; - -message Response_DeckShareList { - extend Response { - optional Response_DeckShareList ext = 1104; - } - optional string name = 1; - optional uint64 expires_at = 2; - repeated ServerInfo_DeckShareItem items = 3; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_list_mine.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_list_mine.proto deleted file mode 100644 index e3cbf83b9..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_share_list_mine.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto2"; -import "response.proto"; -import "serverinfo_deck_share_summary.proto"; - -message Response_DeckShareListMine { - extend Response { - optional Response_DeckShareListMine ext = 1106; - } - repeated ServerInfo_DeckShareSummary shares = 1; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deck_share_item.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deck_share_item.proto deleted file mode 100644 index bac025419..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deck_share_item.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto2"; - -message ServerInfo_DeckShareItem { - optional uint32 id = 1; - optional string name = 2; - repeated string tags = 3; - optional string banner_card = 4; - optional string game_format = 5; - optional string color_identity = 6; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deck_share_summary.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deck_share_summary.proto deleted file mode 100644 index 43273a54f..000000000 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deck_share_summary.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto2"; - -// A share bundle created by a user, as reported by a "list my shares" query. -message ServerInfo_DeckShareSummary { - optional uint32 id = 1; - optional string name = 2; - optional uint64 creation_time = 3; - optional uint64 expires_at = 4; - optional uint32 item_count = 5; -} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deckstorage.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deckstorage.proto index b04d676d8..16e3f28e3 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deckstorage.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deckstorage.proto @@ -1,21 +1,10 @@ syntax = "proto2"; message ServerInfo_DeckStorage_File { optional uint32 creation_time = 1; - optional bool is_public = 2; - // Preview metadata computed by the uploading client, so other clients can - // render this deck (e.g. in a visual storage grid) without downloading the - // full deck list. Empty for decks uploaded before the metadata columns. - optional string banner_card_name = 3; - optional string banner_card_provider = 4; - optional string color_identity = 5; - // Tag names associated with the deck. Empty for decks uploaded before the - // tags column existed. - repeated string tags = 6; } message ServerInfo_DeckStorage_Folder { repeated ServerInfo_DeckStorage_TreeItem items = 1; - optional bool is_public = 2; } message ServerInfo_DeckStorage_TreeItem { diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto index 4b7fe9c85..fee8c36a8 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto @@ -28,14 +28,6 @@ message SessionCommand { FORGOT_PASSWORD_CHALLENGE = 1023; REQUEST_PASSWORD_SALT = 1024; SET_CARD_ART_PARAMS = 1025; - DECK_SHARE_CREATE = 1026; - DECK_SHARE_LIST = 1027; - DECK_SHARE_DOWNLOAD = 1028; - DECK_LIST_OTHER_USER = 1029; - DECK_SET_VISIBILITY = 1030; - DECK_DOWNLOAD_PUBLIC = 1031; - DECK_SHARE_LIST_MINE = 1032; - DECK_SHARE_REMOVE = 1033; REPLAY_LIST = 1100; REPLAY_DOWNLOAD = 1101; REPLAY_MODIFY_MATCH = 1102; diff --git a/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp b/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp index 839e33be4..2f19d6224 100644 --- a/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/appearance_settings.cpp @@ -70,17 +70,6 @@ void AppearanceSettings::setHomeTabDisplayCardName(bool _displayCardName) emit homeTabDisplayCardNameChanged(); } -bool AppearanceSettings::getHomeTabBackgroundDim() const -{ - return getValue("homeTabBackgroundDim", QString(), QString(), true).toBool(); -} - -void AppearanceSettings::setHomeTabBackgroundDim(bool _dimBackground) -{ - setValue(_dimBackground, "homeTabBackgroundDim"); - emit homeTabBackgroundDimChanged(); -} - int AppearanceSettings::getHomeTabButtonColorSourceIndex() const { return getValue("homeTabButtonColorSource", "", "", 0).toInt(); diff --git a/libcockatrice_settings/libcockatrice/settings/appearance_settings.h b/libcockatrice_settings/libcockatrice/settings/appearance_settings.h index a4504798e..3a63f0df0 100644 --- a/libcockatrice_settings/libcockatrice/settings/appearance_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/appearance_settings.h @@ -27,8 +27,6 @@ public: void setHomeTabBackgroundShuffleFrequency(int _frequency); [[nodiscard]] bool getHomeTabDisplayCardName() const; void setHomeTabDisplayCardName(bool _displayCardName); - [[nodiscard]] bool getHomeTabBackgroundDim() const; - void setHomeTabBackgroundDim(bool _dimBackground); [[nodiscard]] int getHomeTabButtonColorSourceIndex() const; void setHomeTabButtonColorSourceIndex(int index); @@ -38,7 +36,6 @@ signals: void homeTabBackgroundSourceChanged(); void homeTabBackgroundShuffleFrequencyChanged(); void homeTabDisplayCardNameChanged(); - void homeTabBackgroundDimChanged(); void homeTabButtonColorChanged(); public: diff --git a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp index b8eafca6c..f528a7c4b 100644 --- a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp @@ -105,11 +105,6 @@ int CardsDisplaySettings::getSampleHandSize() const return getValue("sampleHandSize", "cards", "cardSize", 7).toInt(); } -QString CardsDisplaySettings::getCardLang() const -{ - return getValue("cardLang", QString(), QString(), "en").toString(); -} - void CardsDisplaySettings::setDisplayCardNames(bool _displayCardNames) { setValue(_displayCardNames, "displayCardNames"); @@ -229,16 +224,3 @@ void CardsDisplaySettings::setSampleHandSize(int _sampleHandSize) setValue(_sampleHandSize, "sampleHandSize", "cards", "cardSize"); emit sampleHandSizeChanged(_sampleHandSize); } - -void CardsDisplaySettings::setCardLang(const QString &_cardLang) -{ - if (_cardLang == getCardLang()) { - return; - } - setValue(_cardLang, "cardLang"); - // Flush to disk immediately: the Oracle tool is a separate process that - // reads this value to decide which foreignData to import, so it must not - // observe a stale (pre-change) value. - sync(); - emit cardLangChanged(_cardLang); -} diff --git a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h index 0b47ce490..dbafa32ae 100644 --- a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h @@ -31,7 +31,6 @@ public: [[nodiscard]] int getEDHRecCardSize() const override; [[nodiscard]] int getArchidektPreviewSize() const override; [[nodiscard]] int getSampleHandSize() const override; - [[nodiscard]] QString getCardLang() const override; void setDisplayCardNames(bool _displayCardNames); void setRoundCardCorners(bool _roundCardCorners); @@ -53,7 +52,6 @@ public: void setEDHRecCardSize(int _edhrecCardSize); void setArchidektPreviewCardSize(int _archidektPreviewCardSize); void setSampleHandSize(int _sampleHandSize); - void setCardLang(const QString &_cardLang); signals: void displayCardNamesChanged(); @@ -70,7 +68,6 @@ signals: void edhRecCardSizeChanged(); void archidektPreviewSizeChanged(); void sampleHandSizeChanged(int amount); - void cardLangChanged(const QString &lang); public: explicit CardsDisplaySettings(const QString &settingPath, QObject *parent = nullptr); diff --git a/libcockatrice_settings/libcockatrice/settings/download_settings.cpp b/libcockatrice_settings/libcockatrice/settings/download_settings.cpp index eb73e58ee..cfa1c054e 100644 --- a/libcockatrice_settings/libcockatrice/settings/download_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/download_settings.cpp @@ -4,14 +4,11 @@ const QStringList DownloadSettings::DEFAULT_DOWNLOAD_URLS = { "https://cards.scryfall.io/large/!prop:side!/!set:uuid_substr_0_1!/!set:uuid_substr_1_1!/!set:uuid!.jpg", - "https://api.scryfall.com/cards/!set:uuid!?format=image&face=!prop:side!&lang=!sflang!", - "https://api.scryfall.com/cards/multiverse/!set:muid!?format=image&lang=!sflang!", + "https://api.scryfall.com/cards/!set:uuid!?format=image&face=!prop:side!", + "https://api.scryfall.com/cards/multiverse/!set:muid!?format=image", "https://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!set:muid!&type=card", "https://gatherer.wizards.com/Handlers/Image.ashx?name=!name!&type=card"}; -const QString DownloadSettings::SCRYFALL_NAMED_LOCALIZED_URL = - "https://api.scryfall.com/cards/named?fuzzy=!localizedName!&lang=!sflang!&format=image&face=!prop:side!"; - DownloadSettings::DownloadSettings(const QString &settingPath, QObject *parent = nullptr) : SettingsManager(settingPath + "downloads.ini", "downloads", QString(), parent) { @@ -32,18 +29,6 @@ void DownloadSettings::resetToDefaultURLs() setValue(QVariant::fromValue(DEFAULT_DOWNLOAD_URLS), "urls"); } -bool DownloadSettings::addLocalizedScryfallUrl() -{ - const QStringList urls = getAllURLs(); - if (urls.contains(SCRYFALL_NAMED_LOCALIZED_URL)) { - return false; - } - QStringList updated = urls; - updated.prepend(SCRYFALL_NAMED_LOCALIZED_URL); - setDownloadUrls(updated); - return true; -} - bool DownloadSettings::getPicDownload() const { return getValue("pictureDownload", QString(), QString(), true).toBool(); diff --git a/libcockatrice_settings/libcockatrice/settings/download_settings.h b/libcockatrice_settings/libcockatrice/settings/download_settings.h index ae49884f0..a3a6f4ca9 100644 --- a/libcockatrice_settings/libcockatrice/settings/download_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/download_settings.h @@ -15,7 +15,6 @@ class DownloadSettings : public SettingsManager friend class SettingsCache; static const QStringList DEFAULT_DOWNLOAD_URLS; - static const QString SCRYFALL_NAMED_LOCALIZED_URL; public: explicit DownloadSettings(const QString &, QObject *); @@ -23,7 +22,6 @@ public: QStringList getAllURLs() const; void setDownloadUrls(const QStringList &downloadURLs); void resetToDefaultURLs(); - [[nodiscard]] bool addLocalizedScryfallUrl(); [[nodiscard]] bool getPicDownload() const; void setPicDownload(bool _picDownload); [[nodiscard]] bool getDownloadSpoilersStatus() const; diff --git a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp index 436e260c5..811b0c842 100644 --- a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp @@ -171,12 +171,7 @@ void ServersSettings::addNewServer(const QString &saveName, bool savePassword, const QString &site) { - // Match the exact host-plus-port server the caller is adding, so a link or - // public-server list entry cannot clobber the port (and credentials) of an - // unrelated entry that happens to share the same hostname. - const int existingIndex = findServerIndex(serv, port); - if (existingIndex >= 0) { - updateServerFields(existingIndex, saveName, username, password, savePassword, site); + if (updateExistingServer(saveName, serv, port, username, password, savePassword, site)) { return; } @@ -276,7 +271,22 @@ bool ServersSettings::updateExistingServer(QString saveName, for (int i = 0; i <= size; ++i) { if (serv == getValue(QString("server%1").arg(i), "server", "server_details").toString()) { setValue(port, QString("port%1").arg(i), "server", "server_details"); - updateServerFields(i, saveName, username, password, savePassword, site); + if (!username.isEmpty()) { + setValue(username, QString("username%1").arg(i), "server", "server_details"); + } + + if (savePassword && !password.isEmpty()) { + setValue(password, QString("password%1").arg(i), "server", "server_details"); + } else { + setValue(QString(), QString("password%1").arg(i), "server", "server_details"); + } + + if (!site.isEmpty()) { + setValue(site, QString("site%1").arg(i), "server", "server_details"); + } + + setValue(savePassword, QString("savePassword%1").arg(i), "server", "server_details"); + setValue(saveName, QString("saveName%1").arg(i), "server", "server_details"); return true; } @@ -284,31 +294,6 @@ bool ServersSettings::updateExistingServer(QString saveName, return false; } -void ServersSettings::updateServerFields(int index, - const QString &saveName, - const QString &username, - const QString &password, - bool savePassword, - const QString &site) -{ - if (!username.isEmpty()) { - setValue(username, QString("username%1").arg(index), "server", "server_details"); - } - - if (savePassword && !password.isEmpty()) { - setValue(password, QString("password%1").arg(index), "server", "server_details"); - } else { - setValue(QString(), QString("password%1").arg(index), "server", "server_details"); - } - - if (!site.isEmpty()) { - setValue(site, QString("site%1").arg(index), "server", "server_details"); - } - - setValue(savePassword, QString("savePassword%1").arg(index), "server", "server_details"); - setValue(saveName, QString("saveName%1").arg(index), "server", "server_details"); -} - int ServersSettings::findServerIndex(const QString &host, const QString &port) const { int size = getValue("totalServers", "server", "server_details").toInt(); @@ -325,21 +310,6 @@ int ServersSettings::findServerIndex(const QString &host, const QString &port) c return -1; } -int ServersSettings::findHostIndex(const QString &host) const -{ - int size = getValue("totalServers", "server", "server_details").toInt(); - - for (int i = 0; i <= size; ++i) { - QString storedHost = getValue(QString("server%1").arg(i), "server", "server_details").toString(); - - if (storedHost.compare(host, Qt::CaseInsensitive) == 0) { - return i; - } - } - - return -1; -} - bool ServersSettings::hasUsername(const QString &host, const QString &port) const { int index = findServerIndex(host, port); diff --git a/libcockatrice_settings/libcockatrice/settings/servers_settings.h b/libcockatrice_settings/libcockatrice/settings/servers_settings.h index 93651c813..f9803a158 100644 --- a/libcockatrice_settings/libcockatrice/settings/servers_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.h @@ -61,14 +61,7 @@ public: QString password, bool savePassword, QString site = QString()); - void updateServerFields(int index, - const QString &saveName, - const QString &username, - const QString &password, - bool savePassword, - const QString &site); int findServerIndex(const QString &host, const QString &port) const; - int findHostIndex(const QString &host) const; bool hasUsername(const QString &host, const QString &port) const; bool hasCredentials(const QString &host, const QString &port) const; bool hasLoginData(const QString &host, const QString &port) const; diff --git a/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp b/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp index f6e107dad..37dc9a0a0 100644 --- a/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp +++ b/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp @@ -374,11 +374,7 @@ static void migrateAppearanceSettings(const QString &settingsPath, QSettings &gl QSettings appearanceIni(settingsPath + "appearance.ini", QSettings::IniFormat); for (auto it = appearanceKeyMap.constBegin(); it != appearanceKeyMap.constEnd(); ++it) { if (globalIni.contains(it.key())) { - QVariant value = globalIni.value(it.key()); - if (it.key() == "theme/name" && value.toString() == "Default") { - value = "System"; - } - appearanceIni.setValue(it.value(), value); + appearanceIni.setValue(it.value(), globalIni.value(it.key())); } } } diff --git a/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.cpp b/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.cpp index 3320598d8..1b21af58e 100644 --- a/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.cpp @@ -130,11 +130,6 @@ bool VisualDeckStorageSettings::getVisualDeckStorageShowTagsOnDeckPreviews() con return getValue("showTagsOnDeckPreviews", "interface", "visualDeckStorage", true).toBool(); } -bool VisualDeckStorageSettings::getVisualDeckStorageShowUploadTime() const -{ - return getValue("showUploadTime", "interface", "visualDeckStorage", true).toBool(); -} - bool VisualDeckStorageSettings::getVisualDeckStorageDrawUnusedColorIdentities() const { return getValue("drawUnusedColorIdentities", "interface", "visualDeckStorage", true).toBool(); @@ -225,12 +220,6 @@ void VisualDeckStorageSettings::setVisualDeckStorageShowTagsOnDeckPreviews(bool emit visualDeckStorageShowTagsOnDeckPreviewsChanged(_showTags); } -void VisualDeckStorageSettings::setVisualDeckStorageShowUploadTime(bool value) -{ - setValue(value, "showUploadTime", "interface", "visualDeckStorage"); - emit visualDeckStorageShowUploadTimeChanged(value); -} - void VisualDeckStorageSettings::setVisualDeckStorageDrawUnusedColorIdentities(bool _draw) { setValue(_draw, "drawUnusedColorIdentities", "interface", "visualDeckStorage"); diff --git a/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.h b/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.h index 9bc9d4172..fd2a76663 100644 --- a/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/visual_deck_storage_settings.h @@ -20,7 +20,6 @@ public: [[nodiscard]] bool getVisualDeckStorageShowColorIdentity() const override; [[nodiscard]] bool getVisualDeckStorageShowBannerCardComboBox() const override; [[nodiscard]] bool getVisualDeckStorageShowTagsOnDeckPreviews() const override; - [[nodiscard]] bool getVisualDeckStorageShowUploadTime() const; [[nodiscard]] bool getVisualDeckStorageDrawUnusedColorIdentities() const override; [[nodiscard]] int getVisualDeckStorageUnusedColorIdentitiesOpacity() const override; [[nodiscard]] int getVisualDeckStorageTooltipType() const override; @@ -39,7 +38,6 @@ public: void setVisualDeckStorageShowColorIdentity(bool value); void setVisualDeckStorageShowBannerCardComboBox(bool _showBannerCardComboBox); void setVisualDeckStorageShowTagsOnDeckPreviews(bool _showTags); - void setVisualDeckStorageShowUploadTime(bool value); void setVisualDeckStorageDrawUnusedColorIdentities(bool _draw); void setVisualDeckStorageUnusedColorIdentitiesOpacity(int _opacity); void setVisualDeckStorageTooltipType(int value); @@ -56,7 +54,6 @@ signals: void visualDeckStorageShowColorIdentityChanged(bool _visible); void visualDeckStorageShowBannerCardComboBoxChanged(bool _visible); void visualDeckStorageShowTagsOnDeckPreviewsChanged(bool _visible); - void visualDeckStorageShowUploadTimeChanged(bool _visible); void visualDeckStorageDrawUnusedColorIdentitiesChanged(bool _visible); void visualDeckStorageUnusedColorIdentitiesOpacityChanged(bool value); void visualDeckStorageInGameChanged(bool enabled); diff --git a/oracle/CMakeLists.txt b/oracle/CMakeLists.txt index a942870b7..392184b6e 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -213,19 +213,10 @@ if(WIN32) list(APPEND libSearchDirs ${QT_LIBRARY_DIR}) install( - DIRECTORY "$/" + DIRECTORY "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/${CMAKE_BUILD_TYPE}/" DESTINATION ./ FILES_MATCHING PATTERN "*.dll" - PATTERN "*.pdb" EXCLUDE - PATTERN "*.dir*" EXCLUDE - PATTERN "*_autogen*" EXCLUDE - PATTERN "*.tlog*" EXCLUDE - PATTERN "CMakeFiles*" EXCLUDE - PATTERN "x64*" EXCLUDE - PATTERN ".qt*" EXCLUDE - PATTERN ".qsb*" EXCLUDE - PATTERN ".lupdate*" EXCLUDE ) # Qt plugins: iconengines, platforms, styles, tls (Qt6) diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index 88b522197..fdf11ef02 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include @@ -23,9 +22,8 @@ static const QList kSingletonCounts = {{1, "legal"}, {0, "banned"} SplitCardPart::SplitCardPart(const QString &_name, const QString &_text, const QHash &_properties, - const PrintingInfo &_printingInfo, - const QString &_localizedText) - : name(_name), text(_text), localizedText(_localizedText), properties(_properties), printingInfo(_printingInfo) + const PrintingInfo &_printingInfo) + : name(_name), text(_text), properties(_properties), printingInfo(_printingInfo) { } @@ -35,42 +33,6 @@ OracleImporter::OracleImporter(QObject *parent) : QObject(parent) { } -void OracleImporter::setCardLang(const QString &lang) -{ - cardLang = lang.trimmed().toLower(); - localizationEnabled = cardLang != "en" && CardLocalization::supportedLanguages().contains(cardLang); -} - -/** - * @brief Maps the MTGJSON foreignData language names to the short codes used by - * Scryfall and stored in cards.xml (e.g. "German" -> "de"). - * @param language The language name found in the MTGJSON foreignData entries. - * @return The short language code, or an empty string if unknown. - */ -static QString mtgjsonLanguageToCode(const QString &language) -{ - static const QHash map = { - {"Chinese Simplified", "zhs"}, - {"Chinese Traditional", "zht"}, - {"English", "en"}, - {"French", "fr"}, - {"German", "de"}, - {"Greek", "grc"}, - {"Ancient Greek", "grc"}, - {"Hebrew", "he"}, - {"Italian", "it"}, - {"Japanese", "ja"}, - {"Korean", "ko"}, - {"Latin", "la"}, - {"Phyrexian", "ph"}, - {"Portuguese (Brazil)", "pt"}, - {"Russian", "ru"}, - {"Sanskrit", "sa"}, - {"Spanish", "es"}, - }; - return map.value(language); -} - static CardSet::Priority getSetPriority(const QString &setType, const QString &shortName) { if (!setTypePriorities.contains(setType.toLower())) { @@ -292,101 +254,6 @@ static QString getJsonString(const QJsonObject &obj, const QString &key) return obj.value(key).toVariant().toString(); } -static QString normalizeCardName(QString name) -{ - // Mirror of the name cleanup applied in addCard(), so collected localization - // keys line up with the card map keys (Æ → AE, curly apostrophe → straight). - name = name.replace("Æ", "AE"); - name = name.replace("’", "'"); - return name; -} - -static QString matchingForeignEntryText(const QJsonObject &card, const QString &cardLang) -{ - // Multi-face cards (split/aftermath/adventure/prepare) expose each face as a - // separate card object, each with its own foreignData entry carrying that - // face's rules text; single-face cards carry the full text in one entry. - const QJsonArray foreignData = card.value("foreignData").toArray(); - for (const QJsonValue &entryValue : foreignData) { - const QJsonObject entry = entryValue.toObject(); - // MTGJSON reports languages by long-form name ("German"); match the - // short code ("de") that Scryfall and cards.xml use. - if (mtgjsonLanguageToCode(getJsonString(entry, "language")) == cardLang) { - return getJsonString(entry, "text"); - } - } - return QString(); -} - -void OracleImporter::collectForeignData(const QString &cardKey, - const CardSetPtr ¤tSet, - const QJsonObject &card, - bool collectText) -{ - if (!localizationEnabled) { - return; - } - - LocalizedCardEntry incoming; - bool found = false; - const QJsonArray foreignData = card.value("foreignData").toArray(); - for (const QJsonValue &entryValue : foreignData) { - const QJsonObject entry = entryValue.toObject(); - // MTGJSON reports languages by long-form name ("German"); match the - // short code ("de") that Scryfall and cards.xml use. - if (mtgjsonLanguageToCode(getJsonString(entry, "language")) != cardLang) { - continue; - } - incoming.name = getJsonString(entry, "name"); - incoming.text = collectText ? getJsonString(entry, "text") : QString(); - found = true; - break; - } - if (!found) { - return; - } - - // Prefer the entry from the highest-priority set (lower enum value = more - // authoritative); printings of equal priority keep the first one seen. - incoming.priority = currentSet->getPriority(); - const auto existing = localizedEntries.constFind(cardKey); - if (existing == localizedEntries.constEnd() || incoming.priority < existing->priority) { - localizedEntries.insert(cardKey, incoming); - } -} - -void OracleImporter::applyLocalizedData() -{ - if (!localizationEnabled) { - return; - } - for (auto it = localizedEntries.constBegin(); it != localizedEntries.constEnd(); ++it) { - CardInfoPtr card = cards.value(it.key()); - if (card.isNull()) { - continue; - } - const LocalizedCardEntry &entry = it.value(); - if (!entry.name.isEmpty()) { - card->setLocalizedName(cardLang, entry.name); - } - if (!entry.text.isEmpty()) { - card->setLocalizedText(cardLang, entry.text); - } - } - localizedEntries.clear(); - - for (auto it = splitLocalizedTexts.constBegin(); it != splitLocalizedTexts.constEnd(); ++it) { - CardInfoPtr card = cards.value(it.key()); - if (card.isNull()) { - continue; - } - if (!it.value().text.isEmpty()) { - card->setLocalizedText(cardLang, it.value().text); - } - } - splitLocalizedTexts.clear(); -} - int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJsonArray &cardsList) { // mtgjson name => xml name @@ -530,21 +397,13 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson // split cards are considered a single card, enqueue for later merging if (layout == "split" || layout == "aftermath" || layout == "adventure" || layout == "prepare") { auto _faceName = getJsonString(card, "faceName"); - // MTGJSON exposes each face as a separate card object, each with its - // own foreignData entry holding that face's rules text; collect it so - // the per-face texts can be joined the same way as the English text. - const QString faceLocalizedText = - localizationEnabled ? matchingForeignEntryText(card, cardLang) : QString(); - SplitCardPart split(_faceName, text, properties, printingInfo, faceLocalizedText); + SplitCardPart split(_faceName, text, properties, printingInfo); auto found_iter = splitCards.find(name + numProperty); if (found_iter == splitCards.end()) { splitCards.insert(name + numProperty, {{split}, name}); } else { found_iter->first.append(split); } - // MTGJSON's foreignData name is the joined name present on every - // face, so collect the name once. - collectForeignData(normalizeCardName(name), currentSet, card, false); } else { // relations QList relatedCards; @@ -587,8 +446,6 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson } } - collectForeignData(normalizeCardName(name + numComponent), currentSet, card); - CardInfoPtr newCard = addCard(name + numComponent, text, isToken, std::move(properties), relatedCards, printingInfo); numCards++; @@ -602,8 +459,6 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson QList, QString>> partsAndNames = splitCards.values(); for (auto [splitCardParts, name] : partsAndNames) { QString text; - QString localizedText; - bool localizedTextComplete = true; QHash properties; PrintingInfo printingInfo; @@ -613,21 +468,6 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson } text.append(tmp.getText()); - // Build the cardLang text by joining each face's translated text with - // the same separator as the English text. Any face missing a complete - // translation abandons the whole join, falling back to the English text. - if (localizedTextComplete) { - const QString partLocalizedText = tmp.getLocalizedText(); - if (partLocalizedText.isEmpty()) { - localizedTextComplete = false; - } else { - if (!localizedText.isEmpty()) { - localizedText.append(splitCardTextSeparator); - } - localizedText.append(partLocalizedText); - } - } - if (properties.isEmpty()) { properties = tmp.getProperties(); printingInfo = tmp.getPrintingInfo(); @@ -660,18 +500,6 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson } } CardInfoPtr newCard = addCard(name, text, isToken, std::move(properties), {}, printingInfo); - if (localizationEnabled && localizedTextComplete && !localizedText.isEmpty()) { - // Same priority policy as collectForeignData(): the joined text from the - // highest-priority set seen so far wins, applied once all printings are in. - LocalizedCardEntry entry; - entry.text = localizedText; - entry.priority = currentSet->getPriority(); - const QString entryKey = normalizeCardName(name); - const auto existing = splitLocalizedTexts.constFind(entryKey); - if (existing == splitLocalizedTexts.constEnd() || entry.priority < existing->priority) { - splitLocalizedTexts.insert(entryKey, entry); - } - } numCards++; } @@ -826,8 +654,6 @@ int OracleImporter::startImport() emit setIndexChanged(numCardsInSet, setIndex, curSetToParse.getLongName()); } - applyLocalizedData(); - emit setIndexChanged(0, setIndex, QString()); // total number of sets @@ -853,6 +679,4 @@ void OracleImporter::clear() cards.clear(); allSets.clear(); rawSetsData.clear(); - localizedEntries.clear(); - splitLocalizedTexts.clear(); } diff --git a/oracle/src/oracleimporter.h b/oracle/src/oracleimporter.h index c8d3a19bf..748056772 100644 --- a/oracle/src/oracleimporter.h +++ b/oracle/src/oracleimporter.h @@ -107,8 +107,7 @@ public: SplitCardPart(const QString &_name, const QString &_text, const QHash &_properties, - const PrintingInfo &_printingInfo, - const QString &_localizedText = QString()); + const PrintingInfo &_printingInfo); inline const QString &getName() const { return name; @@ -117,13 +116,6 @@ public: { return text; } - /** - * @brief The cardLang rules text of this face's foreignData entry, if any. - */ - inline const QString &getLocalizedText() const - { - return localizedText; - } inline const QHash &getProperties() const { return properties; @@ -136,18 +128,10 @@ public: private: QString name; QString text; - QString localizedText; QHash properties; PrintingInfo printingInfo; }; -struct LocalizedCardEntry -{ - QString name; - QString text; - CardSet::Priority priority = CardSet::PriorityLowest; -}; - class OracleImporter : public QObject { Q_OBJECT @@ -187,53 +171,12 @@ private: */ QAtomicInt importCancelled; - /** - * The ISO-639 language code whose foreignData is imported; "en" by default. - */ - QString cardLang = "en"; - - /** - * Whether cardLang is a supported language other than English, so per-card - * foreignData scanning can be skipped entirely when disabled. - */ - bool localizationEnabled = false; - - /** - * Localized name/text collected per imported card key while parsing sets, - * applied to the CardInfo objects by applyLocalizedData() once all - * printings have been seen so the best-priority one wins. - */ - QMap localizedEntries; - - /** - * cardLang rules text collected for split-card names while parsing sets, - * applied by applyLocalizedData(). Kept apart from localizedEntries because - * MTGJSON emits each split face as its own card object with the joined name - * on every foreignData entry: names and the per-face text join have different - * completeness and must not overwrite each other under the same key. - */ - QMap splitLocalizedTexts; - CardInfoPtr addCard(QString name, const QString &text, bool isToken, QHash properties, const QList &relatedCards, const PrintingInfo &printingInfo); - - /** - * Records the first foreignData entry matching cardLang for the given card - * key, keeping the entry from the highest-priority set seen so far. - * - * Multi-face cards (split, adventure, aftermath, prepare) pass collectText = - * false: MTGJSON emits one foreignData entry per face with the same joined - * name but only that face's text, so the name is collected here while the - * per-face texts are joined during the split-card merge. - */ - void collectForeignData(const QString &cardKey, - const CardSetPtr ¤tSet, - const QJsonObject &card, - bool collectText = true); signals: void setIndexChanged(int cardsImported, int setIndex, const QString &setName); void dataReadProgress(int bytesRead, int totalBytes); @@ -252,15 +195,6 @@ public: { progressReporting = enabled; } - /** - * Selects the ISO-639 language code whose foreignData is imported. - * English (the default) and unsupported codes disable localization. - */ - void setCardLang(const QString &lang); - const QString &getCardLang() const - { - return cardLang; - } /** * Scans the given JSON document for set metadata. Takes the data by value so * the wizard can hand over its decompressed buffer without copying it. @@ -278,12 +212,6 @@ public: { importCancelled.storeRelease(1); } - /** - * Applies the collected localized names/texts to the imported cards. - * Called automatically at the end of startImport(); exposed separately so - * tests can drive it after importing sets directly. - */ - void applyLocalizedData(); bool saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion); int importCardsFromSet(const CardSetPtr ¤tSet, const QJsonArray &cardsList); /** diff --git a/oracle/src/oraclewizard.cpp b/oracle/src/oraclewizard.cpp index 9ce0d7b51..dd3ac6459 100644 --- a/oracle/src/oraclewizard.cpp +++ b/oracle/src/oraclewizard.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include OracleWizard::OracleWizard(QWidget *parent) : QWizard(parent) @@ -38,9 +37,6 @@ OracleWizard::OracleWizard(QWidget *parent) : QWizard(parent) connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this, &OracleWizard::updateLanguage); importer = new OracleImporter(this); - // Import card text in the language the client displays, if supported: - // foreignData for any other language is never imported. - importer->setCardLang(SettingsCache::instance().cardsDisplay().getCardLang()); nam = new QNetworkAccessManager(this); diff --git a/oracle/src/pagetemplates.cpp b/oracle/src/pagetemplates.cpp index 0ffeabbb3..4a27fef30 100644 --- a/oracle/src/pagetemplates.cpp +++ b/oracle/src/pagetemplates.cpp @@ -112,7 +112,7 @@ bool SimpleDownloadFilePage::validatePage() return false; } - progressLabel->setText(tr("Downloading (0 MB)")); + progressLabel->setText(tr("Downloading (0MB)")); // show an infinite progressbar progressBar->setMaximum(0); progressBar->setMinimum(0); diff --git a/servatrice/CMakeLists.txt b/servatrice/CMakeLists.txt index 21f71a908..68e422d8c 100644 --- a/servatrice/CMakeLists.txt +++ b/servatrice/CMakeLists.txt @@ -184,19 +184,10 @@ if(WIN32) set(qtconf_dest_dir .) install( - DIRECTORY "$/" + DIRECTORY "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/${CMAKE_BUILD_TYPE}/" DESTINATION ./ FILES_MATCHING PATTERN "*.dll" - PATTERN "*.pdb" EXCLUDE - PATTERN "*.dir*" EXCLUDE - PATTERN "*_autogen*" EXCLUDE - PATTERN "*.tlog*" EXCLUDE - PATTERN "CMakeFiles*" EXCLUDE - PATTERN "x64*" EXCLUDE - PATTERN ".qt*" EXCLUDE - PATTERN ".qsb*" EXCLUDE - PATTERN ".lupdate*" EXCLUDE ) # Qt plugins: platforms, sqldrivers, tls (Qt6) diff --git a/servatrice/migrations/servatrice_0036_to_0037.sql b/servatrice/migrations/servatrice_0036_to_0037.sql deleted file mode 100644 index 576ab53b4..000000000 --- a/servatrice/migrations/servatrice_0036_to_0037.sql +++ /dev/null @@ -1,71 +0,0 @@ --- Servatrice db migration from version 36 to version 37 - --- Deck sharing (temporary share links + permanent public decks). --- --- This feature was developed behind several intermediate migrations that have --- never shipped, so they are folded into this single 36 -> 37 migration: --- temporary share links, permanent public-deck visibility, preview metadata, --- and per-deck tags. - --- 1. Temporary deck shares: a named bundle of decks that can be fetched by --- anyone who knows the (unguessable) token, until the share expires. -CREATE TABLE IF NOT EXISTS `cockatrice_deck_share` ( - `id` int(7) unsigned zerofill NOT NULL auto_increment, - `token` varchar(64) COLLATE utf8mb4_bin NOT NULL, - `name` varchar(64) NOT NULL, - `created_by` int(7) unsigned NULL, - `created_at` datetime NOT NULL, - `expires_at` datetime NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `token` (`token`), - KEY `expires_at` (`expires_at`), - FOREIGN KEY(`created_by`) REFERENCES `cockatrice_users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; - --- Individual decks inside a share bundle. Content is materialized at share --- time so expiring/deleting a share can cascade cleanly. The metadata columns --- are nullable because Qt's MySQL driver binds empty QStrings as NULL when --- using prepared statements. -CREATE TABLE IF NOT EXISTS `cockatrice_deck_share_item` ( - `id` int(7) unsigned zerofill NOT NULL auto_increment, - `share_id` int(7) unsigned zerofill NOT NULL, - `name` varchar(50) NOT NULL, - `tags` text NULL, - `banner_card` varchar(255) NULL, - `game_format` varchar(50) NULL, - `color_identity` varchar(5) NULL, - `content` text NOT NULL, - `position` int(7) NOT NULL, - PRIMARY KEY (`id`), - KEY `share_id` (`share_id`), - FOREIGN KEY(`share_id`) REFERENCES `cockatrice_deck_share`(`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; - --- 2. Permanent deck sharing: add public visibility flags to the deck storage --- tables. A deck is visible to other users if it is marked public, or if any --- ancestor folder is marked public (inherited). Existing decks default to --- private, so the upgrade does not expose any data. -ALTER TABLE `cockatrice_decklist_files` - ADD COLUMN `is_public` tinyint(1) NOT NULL DEFAULT 0 AFTER `content`; - -ALTER TABLE `cockatrice_decklist_folders` - ADD COLUMN `is_public` tinyint(1) NOT NULL DEFAULT 0 AFTER `name`; - --- 3. Per-deck preview metadata so clients can render another user's public --- decks (e.g. in a visual deck storage grid) without downloading each deck --- list. The metadata is derived by the server from the deck content (the color --- identity is supplied by the uploading client); decks uploaded before this --- migration have empty values until they are re-uploaded. -ALTER TABLE `cockatrice_decklist_files` - ADD COLUMN `banner_card_name` varchar(255) NULL AFTER `is_public`, - ADD COLUMN `banner_card_provider` varchar(32) NULL AFTER `banner_card_name`, - ADD COLUMN `color_identity` varchar(5) NULL AFTER `banner_card_provider`; - --- 4. Per-deck tags for public decks. The server renders the deck's own tags --- into a JSON array, so another user's public decks can filter by tag without --- downloading each deck list. Decks uploaded before this migration have NULL --- tags until they are re-uploaded. -ALTER TABLE `cockatrice_decklist_files` - ADD COLUMN `tags` text NULL AFTER `color_identity`; - -UPDATE cockatrice_schema_version SET version=37 WHERE version=36; diff --git a/servatrice/servatrice.ini.example b/servatrice/servatrice.ini.example index ccd4f3c3f..23eca3f1b 100644 --- a/servatrice/servatrice.ini.example +++ b/servatrice/servatrice.ini.example @@ -452,24 +452,3 @@ ssl_cert=ssl_cert.pem ; Filename of the private key for the server-to-server certificate ssl_key=ssl_key.pem - - -[deck_share] - -; How many days a created deck share link remains valid before it expires. -; Default: 7 -expiry_days=7 - -; How often (in minutes) the server checks for and removes expired deck shares. -; A value of 0 disables the automatic cleanup. -; Default: 60 -cleanup_interval=60 - -; Maximum number of decks a single share link can contain. -; Default: 50 -max_decks_per_share=50 - -; Maximum number of share links a single user may create per day. -; A value of 0 disables the limit. -; Default: 50 -max_shares_per_day=50 diff --git a/servatrice/servatrice.sql b/servatrice/servatrice.sql index 4fbc1d8bd..cfb1ef5d8 100644 --- a/servatrice/servatrice.sql +++ b/servatrice/servatrice.sql @@ -20,7 +20,7 @@ CREATE TABLE IF NOT EXISTS `cockatrice_schema_version` ( PRIMARY KEY (`version`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; -INSERT INTO cockatrice_schema_version VALUES(37); +INSERT INTO cockatrice_schema_version VALUES(36); -- users and user data tables CREATE TABLE IF NOT EXISTS `cockatrice_users` ( @@ -66,56 +66,16 @@ CREATE TABLE IF NOT EXISTS `cockatrice_decklist_files` ( `name` varchar(50) NOT NULL, `upload_time` datetime NOT NULL, `content` text NOT NULL, - `is_public` tinyint(1) NOT NULL DEFAULT 0, - `banner_card_name` varchar(255) NULL, - `banner_card_provider` varchar(32) NULL, - `color_identity` varchar(5) NULL, - `tags` text NULL, PRIMARY KEY (`id`), KEY `FolderPlusUser` (`id_folder`,`id_user`), FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; --- Temporary deck shares: a named bundle of decks that can be fetched by --- anyone who knows the (unguessable) token, until the share expires. -CREATE TABLE IF NOT EXISTS `cockatrice_deck_share` ( - `id` int(7) unsigned zerofill NOT NULL auto_increment, - `token` varchar(64) COLLATE utf8mb4_bin NOT NULL, - `name` varchar(64) NOT NULL, - `created_by` int(7) unsigned NULL, - `created_at` datetime NOT NULL, - `expires_at` datetime NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `token` (`token`), - KEY `expires_at` (`expires_at`), - FOREIGN KEY(`created_by`) REFERENCES `cockatrice_users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; - --- Individual decks inside a share bundle. Content is materialized at share --- time so expiring/deleting a share can cascade cleanly. The metadata columns --- are nullable because Qt's MySQL driver binds empty QStrings as NULL when --- using prepared statements. -CREATE TABLE IF NOT EXISTS `cockatrice_deck_share_item` ( - `id` int(7) unsigned zerofill NOT NULL auto_increment, - `share_id` int(7) unsigned zerofill NOT NULL, - `name` varchar(50) NOT NULL, - `tags` text NULL, - `banner_card` varchar(255) NULL, - `game_format` varchar(50) NULL, - `color_identity` varchar(5) NULL, - `content` text NOT NULL, - `position` int(7) NOT NULL, - PRIMARY KEY (`id`), - KEY `share_id` (`share_id`), - FOREIGN KEY(`share_id`) REFERENCES `cockatrice_deck_share`(`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; - CREATE TABLE IF NOT EXISTS `cockatrice_decklist_folders` ( `id` int(7) unsigned zerofill NOT NULL auto_increment, `id_parent` int(7) unsigned zerofill NOT NULL, `id_user` int(7) unsigned NULL, `name` varchar(30) NOT NULL, - `is_public` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`id`), KEY `ParentPlusUser` (`id_parent`,`id_user`), FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE diff --git a/servatrice/src/deck_tag_serialization.h b/servatrice/src/deck_tag_serialization.h deleted file mode 100644 index 162b0604d..000000000 --- a/servatrice/src/deck_tag_serialization.h +++ /dev/null @@ -1,39 +0,0 @@ -#ifndef DECK_TAG_SERIALIZATION_H -#define DECK_TAG_SERIALIZATION_H - -#include -#include -#include -#include -#include - -/** - * @brief Encodes deck tags as a compact JSON array for storage in a text column. - * - * Deck tags are stored as JSON (rather than a delimited string) so tag names may - * contain any character, and decoded uniformly everywhere they are read. - */ -inline QString serializeDeckTags(const QStringList &tags) -{ - QJsonArray array; - for (const QString &tag : tags) { - array.append(tag); - } - return QString::fromUtf8(QJsonDocument(array).toJson(QJsonDocument::Compact)); -} - -/** @brief Decodes deck tags previously written by serializeDeckTags. */ -inline QStringList deserializeDeckTags(const QString &serialized) -{ - QStringList tags; - if (serialized.isEmpty()) { - return tags; - } - const QJsonArray array = QJsonDocument::fromJson(serialized.toUtf8()).array(); - for (const QJsonValue &tag : array) { - tags.append(tag.toString()); - } - return tags; -} - -#endif // DECK_TAG_SERIALIZATION_H diff --git a/servatrice/src/servatrice.cpp b/servatrice/src/servatrice.cpp index 3dd7ab510..26352ccd7 100644 --- a/servatrice/src/servatrice.cpp +++ b/servatrice/src/servatrice.cpp @@ -437,14 +437,6 @@ bool Servatrice::initServer() statusUpdateClock->start(getServerStatusUpdateTime()); } - deckShareCleanupClock = new QTimer(this); - connect(deckShareCleanupClock, SIGNAL(timeout()), this, SLOT(cleanupExpiredDeckShares())); - const int deckShareCleanupInterval = getDeckShareCleanupInterval(); - if (deckShareCleanupInterval > 0) { - qDebug() << "Starting deck share cleanup clock, interval" << deckShareCleanupInterval << "ms"; - deckShareCleanupClock->start(deckShareCleanupInterval); - } - // SOCKET SERVER if (getNumberOfTCPPools() > 0) { gameServer = @@ -663,11 +655,6 @@ void Servatrice::setRequiredFeatures(const QString &featureList) qDebug() << "Set required client features to:" << serverRequiredFeatureList; } -void Servatrice::cleanupExpiredDeckShares() -{ - servatriceDatabaseInterface->cleanupExpiredDeckShares(); -} - void Servatrice::statusUpdate() { if (!servatriceDatabaseInterface->checkSql()) { @@ -1080,27 +1067,6 @@ int Servatrice::getServerStatusUpdateTime() const return settingsCache->value("server/statusupdate", 15000).toInt(); } -int Servatrice::getDeckShareExpiryDays() const -{ - return qMax(1, settingsCache->value("deck_share/expiry_days", 7).toInt()); -} - -int Servatrice::getDeckShareCleanupInterval() const -{ - // default: every 60 minutes - return settingsCache->value("deck_share/cleanup_interval", 60).toInt() * 60000; -} - -int Servatrice::getDeckShareMaxDecksPerShare() const -{ - return settingsCache->value("deck_share/max_decks_per_share", 50).toInt(); -} - -int Servatrice::getDeckShareMaxSharesPerDay() const -{ - return settingsCache->value("deck_share/max_shares_per_day", 50).toInt(); -} - int Servatrice::getNumberOfTCPPools() const { return settingsCache->value("server/number_pools", 1).toInt(); diff --git a/servatrice/src/servatrice.h b/servatrice/src/servatrice.h index f39a44d51..8d964a52b 100644 --- a/servatrice/src/servatrice.h +++ b/servatrice/src/servatrice.h @@ -146,7 +146,6 @@ public: private slots: void statusUpdate(); void shutdownTimeout(); - void cleanupExpiredDeckShares(); protected: void doSendIslMessage(const IslMessage &msg, int _serverId) override; @@ -160,7 +159,6 @@ private: AuthenticationMethod authenticationMethod; DatabaseType databaseType; QTimer *pingClock, *statusUpdateClock; - QTimer *deckShareCleanupClock; Servatrice_GameServer *gameServer; Servatrice_WebsocketGameServer *websocketGameServer; Servatrice_IslServer *islServer; @@ -278,10 +276,6 @@ public: int getMaxGameInactivityTime() const override; int getMaxPlayerInactivityTime() const override; int getClientKeepAlive() const override; - int getDeckShareExpiryDays() const; - int getDeckShareCleanupInterval() const; - int getDeckShareMaxDecksPerShare() const; - int getDeckShareMaxSharesPerDay() const; int getMaxUsersPerAddress() const; int getMessageCountingInterval() const override; int getMaxMessageCountPerInterval() const override; diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index bb67d78eb..c4c7046c3 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -1,6 +1,5 @@ #include "servatrice_database_interface.h" -#include "deck_tag_serialization.h" #include "servatrice.h" #include "serversocketinterface.h" #include "settingscache.h" @@ -8,13 +7,11 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -102,59 +99,12 @@ bool Servatrice_DatabaseInterface::openDatabase() return false; } - if (sqlDatabase.driverName() != "QMYSQL") { - qCCritical(DatabaseInterfaceLog) - << poolStr - << "Error opening database: connection is not a MySQL/MariaDB database, Servatrice only " - "supports the QMYSQL driver (actual driver:" - << sqlDatabase.driverName() << ")."; - return false; - } - - bool strictModeCheckOk = false; - const bool strictModeEnabled = isStrictModeEnabled(strictModeCheckOk); - if (!strictModeCheckOk) { - qCCritical(DatabaseInterfaceLog) << poolStr - << "Error opening database: unable to determine whether MySQL/MariaDB strict " - "mode is enabled"; - return false; - } - if (strictModeEnabled) { - qCCritical(DatabaseInterfaceLog) << poolStr - << "Error opening database: MySQL/MariaDB strict mode is enabled, which " - "breaks most Servatrice database operations. Please disable strict mode " - "by removing STRICT_TRANS_TABLES and STRICT_ALL_TABLES from sql_mode, " - "for example by adding 'sql_mode=NO_ENGINE_SUBSTITUTION' under [mysqld] " - "in your my.cnf (or my.ini on Windows) and restarting the database " - "server."; - return false; - } - // reset all prepared statements qDeleteAll(preparedStatements); preparedStatements.clear(); return true; } -bool Servatrice_DatabaseInterface::isStrictModeEnabled(bool &ok) const -{ - ok = true; - - QSqlQuery query(sqlDatabase); - if (!query.exec("SELECT @@GLOBAL.sql_mode")) { - ok = false; - return false; - } - - const QStringList modes = query.next() ? query.value(0).toString().split(',') : QStringList(); - for (const QString &mode : modes) { - if (mode.trimmed() == "STRICT_TRANS_TABLES" || mode.trimmed() == "STRICT_ALL_TABLES") { - return true; - } - } - return false; -} - bool Servatrice_DatabaseInterface::checkSql() { if (!sqlDatabase.isValid()) { @@ -1081,183 +1031,6 @@ DeckList *Servatrice_DatabaseInterface::getDeckFromDatabase(int deckId, int user return deck; } -bool Servatrice_DatabaseInterface::createDeckShare(const QString &token, - const QString &name, - int userId, - const QList &items, - int expiryDays, - qint64 &expiresAt) -{ - checkSql(); - - if (items.isEmpty()) { - return false; - } - - if (!sqlDatabase.transaction()) { - return false; - } - - QSqlQuery *query = prepareQuery("insert into {prefix}_deck_share (token, name, created_by, created_at, expires_at) " - "values (:token, :name, :created_by, NOW(), DATE_ADD(NOW(), INTERVAL :days DAY))"); - query->bindValue(":token", token); - query->bindValue(":name", name); - query->bindValue(":created_by", userId < 1 ? QVariant() : userId); - query->bindValue(":days", expiryDays); - if (!execSqlQuery(query)) { - // A failed execSqlQuery has already closed and reopened the connection, - // which implicitly discards the transaction; rollback below is a no-op. - sqlDatabase.rollback(); - return false; - } - - const int shareId = query->lastInsertId().toInt(); - - // Read the expiry back from the database so the value returned to the client - // matches the server clock rather than being approximated client-side. - QSqlQuery *expiryQuery = prepareQuery("select UNIX_TIMESTAMP(expires_at) from {prefix}_deck_share where id = :id"); - expiryQuery->bindValue(":id", shareId); - if (!execSqlQuery(expiryQuery) || !expiryQuery->next()) { - // See the note above: after a failed execSqlQuery the transaction is - // already gone because the connection was torn down. - sqlDatabase.rollback(); - return false; - } - expiresAt = expiryQuery->value(0).toLongLong(); - - for (int i = 0; i < items.size(); ++i) { - const DeckShareItemRecord &item = items.at(i); - QSqlQuery *itemQuery = prepareQuery("insert into {prefix}_deck_share_item (share_id, name, tags, banner_card, " - "game_format, color_identity, content, position) values (:share_id, :name, " - ":tags, :banner_card, :game_format, :color_identity, :content, :position)"); - itemQuery->bindValue(":share_id", shareId); - itemQuery->bindValue(":name", item.name); - itemQuery->bindValue(":tags", serializeDeckTags(item.tags)); - itemQuery->bindValue(":banner_card", item.bannerCard); - itemQuery->bindValue(":game_format", item.gameFormat); - itemQuery->bindValue(":color_identity", item.colorIdentity); - itemQuery->bindValue(":content", item.content); - itemQuery->bindValue(":position", i); - if (!execSqlQuery(itemQuery)) { - // See the note above: the transaction is already gone after the - // reconnect performed by a failed execSqlQuery. - sqlDatabase.rollback(); - return false; - } - } - - if (!sqlDatabase.commit()) { - sqlDatabase.rollback(); - return false; - } - return true; -} - -bool Servatrice_DatabaseInterface::getDeckShareList(const QString &token, - QString &name, - qint64 &expiresAt, - QList &items) -{ - checkSql(); - - QSqlQuery *query = - prepareQuery("select id, name, UNIX_TIMESTAMP(expires_at) from {prefix}_deck_share where token = " - ":token and expires_at > now()"); - query->bindValue(":token", token); - execSqlQuery(query); - if (!query->next()) { - return false; - } - - const int shareId = query->value(0).toInt(); - name = query->value(1).toString(); - expiresAt = query->value(2).toLongLong(); - items.clear(); - - QSqlQuery *itemQuery = - prepareQuery("select id, name, tags, banner_card, game_format, color_identity from {prefix}_deck_share_item " - "where share_id = :share_id order by position"); - itemQuery->bindValue(":share_id", shareId); - execSqlQuery(itemQuery); - while (itemQuery->next()) { - DeckShareItemRecord item; - item.id = itemQuery->value(0).toInt(); - item.name = itemQuery->value(1).toString(); - item.tags = deserializeDeckTags(itemQuery->value(2).toString()); - item.bannerCard = itemQuery->value(3).toString(); - item.gameFormat = itemQuery->value(4).toString(); - item.colorIdentity = itemQuery->value(5).toString(); - items.append(item); - } - - return true; -} - -bool Servatrice_DatabaseInterface::getDeckShareItem(const QString &token, int itemId, QString &content) -{ - checkSql(); - - QSqlQuery *query = prepareQuery("select i.content from {prefix}_deck_share_item i join {prefix}_deck_share s on " - "s.id = i.share_id where s.token = :token and s.expires_at > now() and i.id = :id"); - query->bindValue(":token", token); - query->bindValue(":id", itemId); - execSqlQuery(query); - if (!query->next()) { - return false; - } - - content = query->value(0).toString(); - return true; -} - -void Servatrice_DatabaseInterface::cleanupExpiredDeckShares() -{ - checkSql(); - - QSqlQuery *query = prepareQuery("delete from {prefix}_deck_share where expires_at < now()"); - execSqlQuery(query); -} - -bool Servatrice_DatabaseInterface::getDeckSharesForUser(int userId, QList &shares) -{ - checkSql(); - - QSqlQuery *query = prepareQuery("select s.id, s.name, UNIX_TIMESTAMP(s.created_at), " - "UNIX_TIMESTAMP(s.expires_at), count(i.id) from {prefix}_deck_share s left join " - "{prefix}_deck_share_item i on i.share_id = s.id " - "where s.created_by = :created_by and s.expires_at > now() " - "group by s.id, s.name, s.created_at, s.expires_at order by s.created_at desc"); - query->bindValue(":created_by", userId); - if (!execSqlQuery(query)) { - return false; - } - - shares.clear(); - while (query->next()) { - DeckShareSummaryRecord summary; - summary.id = query->value(0).toInt(); - summary.name = query->value(1).toString(); - summary.creationTime = query->value(2).toLongLong(); - summary.expiresAt = query->value(3).toLongLong(); - summary.itemCount = query->value(4).toInt(); - shares.append(summary); - } - return true; -} - -bool Servatrice_DatabaseInterface::deleteDeckShare(int shareId, int userId) -{ - checkSql(); - - QSqlQuery *query = prepareQuery("delete from {prefix}_deck_share where id = :id and created_by = :created_by"); - query->bindValue(":id", shareId); - query->bindValue(":created_by", userId); - if (!execSqlQuery(query)) { - return false; - } - return query->numRowsAffected() > 0; -} - void Servatrice_DatabaseInterface::logMessage(const int senderId, const QString &senderName, const QString &senderIp, diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index ce51a2474..a891c7a3d 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -13,32 +13,10 @@ #include #include -#define DATABASE_SCHEMA_VERSION 37 +#define DATABASE_SCHEMA_VERSION 36 class Servatrice; -/** @brief Metadata of a single deck inside a temporary deck share bundle. */ -struct DeckShareItemRecord -{ - int id = -1; ///< Database id, used for downloads. - QString name; ///< Deck name. - QStringList tags; ///< Deck tags. - QString bannerCard; ///< Banner card name (deck image). - QString gameFormat; ///< Game format the deck was built for. - QString colorIdentity; ///< Color identity, e.g. "WUBRG". - QString content; ///< Deck content (native format); empty in list queries. -}; - -/** @brief Summary of a share bundle owned by a user. */ -struct DeckShareSummaryRecord -{ - int id = -1; ///< Database id, used for revocation. - QString name; ///< Share name. - qint64 creationTime = 0; ///< Unix timestamp at which the share was created. - qint64 expiresAt = 0; ///< Unix timestamp at which the share expires. - int itemCount = 0; ///< Number of decks in the bundle. -}; - class Servatrice_DatabaseInterface : public Server_DatabaseInterface { Q_OBJECT @@ -54,7 +32,6 @@ private: bool checkUserIsIpBanned(const QString &ipAddress, QString &banReason, int &banSecondsRemaining); /** Must be called after checkSql and server is known to be in auth mode. */ bool checkUserIsNameBanned(QString const &userName, QString &banReason, int &banSecondsRemaining); - bool isStrictModeEnabled(bool &ok) const; protected: AuthenticationResult checkUserPassword(Server_ProtocolHandler *handler, @@ -102,37 +79,6 @@ public: const QList &replayList) override; DeckList *getDeckFromDatabase(int deckId, int userId) override; - /** - * @brief Creates a new temporary deck share bundle. - * @param expiresAt Receives the actual expiry read back from the database. - * @return false on failure. - */ - bool createDeckShare(const QString &token, - const QString &name, - int userId, - const QList &items, - int expiryDays, - qint64 &expiresAt); - /** @brief Lists the share bundles created by a user, newest first. */ - bool getDeckSharesForUser(int userId, QList &shares); - /** - * @brief Deletes one of a user's own share bundles (cascades to its items). - * @return false if no such bundle belongs to the user. - */ - bool deleteDeckShare(int shareId, int userId); - /** - * @brief Looks up a valid (non-expired) share bundle by token. - * @return false if the token is unknown or expired. - */ - bool getDeckShareList(const QString &token, QString &name, qint64 &expiresAt, QList &items); - /** - * @brief Fetches the content of one item of a valid share bundle. - * @return false if the token is unknown/expired or the item does not belong to the bundle. - */ - bool getDeckShareItem(const QString &token, int itemId, QString &content); - /** @brief Deletes all expired share bundles (cascades to their items). */ - void cleanupExpiredDeckShares(); - int getNextGameId() override; int getNextReplayId() override; int getActiveUserCount(QString connectionType = QString()) override; diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index c82a8dd73..aeffd7081 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -20,7 +20,6 @@ #include "serversocketinterface.h" -#include "deck_tag_serialization.h" #include "email_parser.h" #include "main.h" #include "servatrice.h" @@ -36,12 +35,10 @@ #include #include #include -#include #include #include #include #include -#include #include #include #include @@ -50,16 +47,8 @@ #include #include #include -#include #include -#include #include -#include -#include -#include -#include -#include -#include #include #include #include @@ -92,10 +81,6 @@ #include #include #include -#include -#include -#include -#include #include #include #include @@ -292,12 +277,6 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedSessionComm return cmdRemoveFromList(cmd.GetExtension(Command_RemoveFromList::ext), rc); case SessionCommand::DECK_LIST: return cmdDeckList(cmd.GetExtension(Command_DeckList::ext), rc); - case SessionCommand::DECK_LIST_OTHER_USER: - return cmdDeckListOtherUser(cmd.GetExtension(Command_DeckListOtherUser::ext), rc); - case SessionCommand::DECK_SET_VISIBILITY: - return cmdDeckSetVisibility(cmd.GetExtension(Command_DeckSetVisibility::ext), rc); - case SessionCommand::DECK_DOWNLOAD_PUBLIC: - return cmdDeckDownloadPublic(cmd.GetExtension(Command_DeckDownloadPublic::ext), rc); case SessionCommand::DECK_NEW_DIR: return cmdDeckNewDir(cmd.GetExtension(Command_DeckNewDir::ext), rc); case SessionCommand::DECK_DEL_DIR: @@ -341,16 +320,6 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedSessionComm return cmdAccountImage(cmd.GetExtension(Command_AccountImage::ext), rc); case SessionCommand::SET_CARD_ART_PARAMS: return cmdSetCardArtParams(cmd.GetExtension(Command_SetCardArtParams::ext), rc); - case SessionCommand::DECK_SHARE_CREATE: - return cmdDeckShareCreate(cmd.GetExtension(Command_DeckShareCreate::ext), rc); - case SessionCommand::DECK_SHARE_LIST: - return cmdDeckShareList(cmd.GetExtension(Command_DeckShareList::ext), rc); - case SessionCommand::DECK_SHARE_LIST_MINE: - return cmdDeckShareListMine(cmd.GetExtension(Command_DeckShareListMine::ext), rc); - case SessionCommand::DECK_SHARE_REMOVE: - return cmdDeckShareRemove(cmd.GetExtension(Command_DeckShareRemove::ext), rc); - case SessionCommand::DECK_SHARE_DOWNLOAD: - return cmdDeckShareDownload(cmd.GetExtension(Command_DeckShareDownload::ext), rc); case SessionCommand::ACCOUNT_PASSWORD: return cmdAccountPassword(cmd.GetExtension(Command_AccountPassword::ext), rc); case SessionCommand::REQUEST_PASSWORD_SALT: @@ -597,73 +566,46 @@ int AbstractServerSocketInterface::getDeckPathId(const QString &path) return getDeckPathId(0, path.split("/")); } -bool AbstractServerSocketInterface::deckListHelper(int folderId, - ServerInfo_DeckStorage_Folder *folder, - int userId, - bool inheritedPublic, - bool publicOnly) +bool AbstractServerSocketInterface::deckListHelper(int folderId, ServerInfo_DeckStorage_Folder *folder) { - QSqlQuery *query = sqlInterface->prepareQuery("select id, name, is_public from {prefix}_decklist_folders where " - "id_parent = :id_parent and id_user = :id_user"); + QSqlQuery *query = sqlInterface->prepareQuery( + "select id, name from {prefix}_decklist_folders where id_parent = :id_parent and id_user = :id_user"); query->bindValue(":id_parent", folderId); - query->bindValue(":id_user", userId); + query->bindValue(":id_user", userInfo->id()); if (!sqlInterface->execSqlQuery(query)) { return false; } - QList>> folderRows; + QMap results; while (query->next()) { - folderRows.append({query->value(0).toInt(), {query->value(1).toString(), query->value(2).toBool()}}); + results[query->value(0).toInt()] = query->value(1).toString(); } - std::sort(folderRows.begin(), folderRows.end(), [](const auto &a, const auto &b) { return a.first < b.first; }); - - for (const auto &[folderIdValue, folderInfo] : folderRows) { - const QString name = folderInfo.first; - const bool ownPublic = folderInfo.second; - const bool effectivePublic = inheritedPublic || ownPublic; + for (int key : results.keys()) { ServerInfo_DeckStorage_TreeItem *newItem = folder->add_items(); - newItem->set_id(folderIdValue); - newItem->set_name(name.toStdString()); - newItem->mutable_folder()->set_is_public(ownPublic); + newItem->set_id(key); + newItem->set_name(results.value(key).toStdString()); - if (!deckListHelper(newItem->id(), newItem->mutable_folder(), userId, effectivePublic, publicOnly)) { + if (!deckListHelper(newItem->id(), newItem->mutable_folder())) { return false; } - - if (publicOnly && !effectivePublic && newItem->mutable_folder()->items_size() == 0) { - folder->mutable_items()->RemoveLast(); - } } - query = sqlInterface->prepareQuery("select id, name, upload_time, is_public, banner_card_name, " - "banner_card_provider, color_identity, tags from {prefix}_decklist_files where " - "id_folder = :id_folder and id_user = :id_user"); + query = sqlInterface->prepareQuery("select id, name, upload_time from {prefix}_decklist_files where id_folder = " + ":id_folder and id_user = :id_user"); query->bindValue(":id_folder", folderId); - query->bindValue(":id_user", userId); + query->bindValue(":id_user", userInfo->id()); if (!sqlInterface->execSqlQuery(query)) { return false; } while (query->next()) { - const bool ownPublic = query->value(3).toBool(); - if (publicOnly && !(inheritedPublic || ownPublic)) { - continue; - } - ServerInfo_DeckStorage_TreeItem *newItem = folder->add_items(); newItem->set_id(query->value(0).toInt()); newItem->set_name(query->value(1).toString().toStdString()); ServerInfo_DeckStorage_File *newFile = newItem->mutable_file(); newFile->set_creation_time(query->value(2).toDateTime().toSecsSinceEpoch()); - newFile->set_is_public(ownPublic); - newFile->set_banner_card_name(query->value(4).toString().toStdString()); - newFile->set_banner_card_provider(query->value(5).toString().toStdString()); - newFile->set_color_identity(query->value(6).toString().toStdString()); - for (const QString &tag : deserializeDeckTags(query->value(7).toString())) { - newFile->add_tags(tag.toStdString()); - } } return true; @@ -684,7 +626,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckList(const Command_ Response_DeckList *re = new Response_DeckList; ServerInfo_DeckStorage_Folder *root = re->mutable_root(); - if (!deckListHelper(0, root, userInfo->id(), false, false)) { + if (!deckListHelper(0, root)) { return Response::RespContextError; } @@ -692,160 +634,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckList(const Command_ return Response::RespOk; } -Response::ResponseCode AbstractServerSocketInterface::cmdDeckListOtherUser(const Command_DeckListOtherUser &cmd, - ResponseContainer &rc) -{ - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - - sqlInterface->checkSql(); - - const QString userName = nameFromStdString(cmd.user_name()); - const int userId = sqlInterface->getUserIdInDB(userName); - if (userId == -1) { - return Response::RespNameNotFound; - } - - Response_DeckList *re = new Response_DeckList; - ServerInfo_DeckStorage_Folder *root = re->mutable_root(); - - if (!deckListHelper(0, root, userId, false, true)) { - return Response::RespContextError; - } - - rc.setResponseExtension(re); - return Response::RespOk; -} - -int AbstractServerSocketInterface::getDeckOwnerId(int deckId) -{ - QSqlQuery *query = sqlInterface->prepareQuery("select id_user from {prefix}_decklist_files where id = :id"); - query->bindValue(":id", deckId); - if (!sqlInterface->execSqlQuery(query)) { - return -1; - } - if (!query->next()) { - return -1; - } - return query->value(0).toInt(); -} - -bool AbstractServerSocketInterface::isDeckEffectivelyPublic(int deckId) -{ - QSqlQuery *query = - sqlInterface->prepareQuery("select is_public, id_folder from {prefix}_decklist_files where id = :id"); - query->bindValue(":id", deckId); - if (!sqlInterface->execSqlQuery(query)) { - return false; - } - if (!query->next()) { - return false; - } - if (query->value(0).toBool()) { - return true; - } - - int folderId = query->value(1).toInt(); - int guard = 0; - while (folderId != 0 && guard < 100) { - QSqlQuery *folderQuery = - sqlInterface->prepareQuery("select is_public, id_parent from {prefix}_decklist_folders where id = :id"); - folderQuery->bindValue(":id", folderId); - if (!sqlInterface->execSqlQuery(folderQuery)) { - return false; - } - if (!folderQuery->next()) { - return false; - } - if (folderQuery->value(0).toBool()) { - return true; - } - folderId = folderQuery->value(1).toInt(); - ++guard; - } - return false; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdDeckSetVisibility(const Command_DeckSetVisibility &cmd, - ResponseContainer & /*rc*/) -{ - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - - sqlInterface->checkSql(); - - if (cmd.has_deck_id()) { - QSqlQuery *query = - sqlInterface->prepareQuery("select 1 from {prefix}_decklist_files where id = :id and id_user = :id_user"); - query->bindValue(":id", cmd.deck_id()); - query->bindValue(":id_user", userInfo->id()); - sqlInterface->execSqlQuery(query); - if (!query->next()) { - return Response::RespNameNotFound; - } - - query = sqlInterface->prepareQuery("update {prefix}_decklist_files set is_public = :is_public where id = :id " - "and id_user = :id_user"); - query->bindValue(":is_public", cmd.is_public() ? 1 : 0); - query->bindValue(":id", cmd.deck_id()); - query->bindValue(":id_user", userInfo->id()); - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespContextError; - } - } else if (cmd.has_folder_path()) { - const int folderId = getDeckPathId(nameFromStdString(cmd.folder_path())); - if (folderId == -1 || folderId == 0) { - return Response::RespNameNotFound; - } - - QSqlQuery *query = - sqlInterface->prepareQuery("update {prefix}_decklist_folders set is_public = :is_public where id = :id " - "and id_user = :id_user"); - query->bindValue(":is_public", cmd.is_public() ? 1 : 0); - query->bindValue(":id", folderId); - query->bindValue(":id_user", userInfo->id()); - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespContextError; - } - } else { - return Response::RespInvalidData; - } - - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdDeckDownloadPublic(const Command_DeckDownloadPublic &cmd, - ResponseContainer &rc) -{ - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - - sqlInterface->checkSql(); - - const int deckId = cmd.deck_id(); - const int ownerId = getDeckOwnerId(deckId); - if (ownerId == -1 || !isDeckEffectivelyPublic(deckId)) { - return Response::RespNameNotFound; - } - - DeckList *deck; - try { - deck = sqlInterface->getDeckFromDatabase(deckId, ownerId); - } catch (Response::ResponseCode &r) { - return r; - } - - Response_DeckDownload *re = new Response_DeckDownload; - re->set_deck(deck->writeToString_Native().toStdString()); - rc.setResponseExtension(re); - delete deck; - - return Response::RespOk; -} - Response::ResponseCode AbstractServerSocketInterface::cmdDeckNewDir(const Command_DeckNewDir &cmd, ResponseContainer & /*rc*/) { @@ -954,22 +742,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckDel(const Command_D return Response::RespOk; } -namespace -{ -/** @brief Keeps only the WUBRG colors from a color identity string, deduplicated. */ -QString sanitizeColorIdentity(const QString &colorIdentity) -{ - QString sanitized; - for (const QChar &color : colorIdentity) { - const QChar upper = color.toUpper(); - if (QStringLiteral("WUBRG").contains(upper) && !sanitized.contains(upper)) { - sanitized.append(upper); - } - } - return sanitized; -} -} // namespace - Response::ResponseCode AbstractServerSocketInterface::cmdDeckUpload(const Command_DeckUpload &cmd, ResponseContainer &rc) { @@ -994,14 +766,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckUpload(const Comman deckName = "Unnamed deck"; } - // The server derives the banner card and tags from the deck itself. Only the - // color identity must come from the client, since the server has no card - // database to compute it. All values are bounded to the column sizes. - const QString bannerCardName = deck.getBannerCard().name.left(255); - const QString bannerCardProvider = deck.getBannerCard().providerId.left(32); - const QString tagsJson = serializeDeckTags(deck.getTags()); - const QString colorIdentity = sanitizeColorIdentity(nameFromStdString(cmd.color_identity())); - if (cmd.has_path()) { int folderId = getDeckPathId(nameFromStdString(cmd.path())); if (folderId == -1) { @@ -1010,74 +774,38 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckUpload(const Comman QSqlQuery *query = sqlInterface->prepareQuery("insert into {prefix}_decklist_files (id_folder, id_user, name, upload_time, " - "content, is_public, banner_card_name, banner_card_provider, color_identity, " - "tags) values(:id_folder, :id_user, :name, NOW(), :content, :is_public, " - ":banner_card_name, :banner_card_provider, :color_identity, :tags)"); + "content) values(:id_folder, :id_user, :name, NOW(), :content)"); query->bindValue(":id_folder", folderId); query->bindValue(":id_user", userInfo->id()); query->bindValue(":name", deckName); query->bindValue(":content", deckStr); - query->bindValue(":is_public", cmd.has_is_public() && cmd.is_public() ? 1 : 0); - query->bindValue(":banner_card_name", bannerCardName); - query->bindValue(":banner_card_provider", bannerCardProvider); - query->bindValue(":color_identity", colorIdentity); - query->bindValue(":tags", tagsJson); - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespContextError; - } + sqlInterface->execSqlQuery(query); Response_DeckUpload *re = new Response_DeckUpload; ServerInfo_DeckStorage_TreeItem *fileInfo = re->mutable_new_file(); fileInfo->set_id(query->lastInsertId().toInt()); fileInfo->set_name(deckName.toStdString()); fileInfo->mutable_file()->set_creation_time(QDateTime::currentDateTime().toSecsSinceEpoch()); - fileInfo->mutable_file()->set_is_public(cmd.has_is_public() && cmd.is_public()); rc.setResponseExtension(re); } else if (cmd.has_deck_id()) { - QString updateQuery = "update {prefix}_decklist_files set name=:name, upload_time=NOW(), content=:content, " - "banner_card_name=:banner_card_name, banner_card_provider=:banner_card_provider, " - "color_identity=:color_identity, tags=:tags"; - if (cmd.has_is_public()) { - updateQuery += ", is_public=:is_public"; - } - updateQuery += " where id = :id_deck and id_user = :id_user"; - - QSqlQuery *query = sqlInterface->prepareQuery(updateQuery); + QSqlQuery *query = + sqlInterface->prepareQuery("update {prefix}_decklist_files set name=:name, upload_time=NOW(), " + "content=:content where id = :id_deck and id_user = :id_user"); query->bindValue(":id_deck", cmd.deck_id()); query->bindValue(":id_user", userInfo->id()); query->bindValue(":name", deckName); query->bindValue(":content", deckStr); - query->bindValue(":banner_card_name", bannerCardName); - query->bindValue(":banner_card_provider", bannerCardProvider); - query->bindValue(":color_identity", colorIdentity); - query->bindValue(":tags", tagsJson); - if (cmd.has_is_public()) { - query->bindValue(":is_public", cmd.is_public() ? 1 : 0); - } - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespContextError; - } + sqlInterface->execSqlQuery(query); if (query->numRowsAffected() == 0) { return Response::RespNameNotFound; } - QSqlQuery *visibilityQuery = - sqlInterface->prepareQuery("select is_public from {prefix}_decklist_files where id = :id and " - "id_user = :id_user"); - visibilityQuery->bindValue(":id", cmd.deck_id()); - visibilityQuery->bindValue(":id_user", userInfo->id()); - if (!sqlInterface->execSqlQuery(visibilityQuery)) { - return Response::RespContextError; - } - const bool isPublic = visibilityQuery->next() && visibilityQuery->value(0).toBool(); - Response_DeckUpload *re = new Response_DeckUpload; ServerInfo_DeckStorage_TreeItem *fileInfo = re->mutable_new_file(); fileInfo->set_id(cmd.deck_id()); fileInfo->set_name(deckName.toStdString()); fileInfo->mutable_file()->set_creation_time(QDateTime::currentDateTime().toSecsSinceEpoch()); - fileInfo->mutable_file()->set_is_public(isPublic); rc.setResponseExtension(re); } else { return Response::RespInvalidData; @@ -1108,248 +836,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckDownload(const Comm return Response::RespOk; } -namespace -{ -/** @brief Builds a cryptographically random, URL-safe share token. */ -QString generateShareToken() -{ - QByteArray bytes(32, Qt::Uninitialized); - QRandomGenerator::system()->fillRange(reinterpret_cast(bytes.data()), bytes.size() / sizeof(quint32)); - return QString::fromLatin1(bytes.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)); -} - -/** @brief Extracts the share metadata for a deck, materializing its content. */ -DeckShareItemRecord makeShareItemFromDeck(const DeckList &deck, const QString &colorIdentity) -{ - DeckShareItemRecord item; - item.name = deck.getName(); - if (item.name.isEmpty()) { - item.name = "Unnamed deck"; - } - item.tags = deck.getTags(); - item.bannerCard = deck.getBannerCard().name; - item.gameFormat = deck.getGameFormat(); - item.colorIdentity = sanitizeColorIdentity(colorIdentity); - item.content = deck.writeToString_Native(); - return item; -} -} // namespace - -Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareCreate(const Command_DeckShareCreate &cmd, - ResponseContainer &rc) -{ - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - - sqlInterface->checkSql(); - - const int maxItems = servatrice->getDeckShareMaxDecksPerShare(); - if (maxItems > 0 && cmd.items_size() > maxItems) { - return Response::RespInvalidData; - } - - // Per-user rate limit so share links cannot be used to build an unbounded - // word-of-mouth leak of public decks. - const int maxSharesPerDay = servatrice->getDeckShareMaxSharesPerDay(); - if (maxSharesPerDay > 0) { - QSqlQuery *countQuery = sqlInterface->prepareQuery("select count(*) from {prefix}_deck_share where " - "created_by = :created_by and created_at >= " - "DATE_SUB(NOW(), INTERVAL 1 DAY)"); - countQuery->bindValue(":created_by", userInfo->id()); - if (!sqlInterface->execSqlQuery(countQuery) || !countQuery->next()) { - return Response::RespContextError; - } - if (countQuery->value(0).toInt() >= maxSharesPerDay) { - return Response::RespTooManyRequests; - } - } - - QList items; - if (cmd.items_size() > 0) { - for (const DeckShareItem &shareItem : cmd.items()) { - if (shareItem.has_deck_list()) { - DeckList deck; - if (!deck.loadFromString_Native(fileFromStdString(shareItem.deck_list()))) { - return Response::RespContextError; - } - items.append(makeShareItemFromDeck(deck, nameFromStdString(shareItem.color_identity()))); - } else if (shareItem.has_deck_id()) { - DeckList *deck; - try { - deck = sqlInterface->getDeckFromDatabase(shareItem.deck_id(), userInfo->id()); - } catch (Response::ResponseCode &r) { - return r; - } - items.append(makeShareItemFromDeck(*deck, nameFromStdString(shareItem.color_identity()))); - delete deck; - } else { - return Response::RespInvalidData; - } - } - } else if (cmd.has_folder_path()) { - const int folderId = getDeckPathId(nameFromStdString(cmd.folder_path())); - if (folderId == -1) { - return Response::RespNameNotFound; - } - - // Drain the deck list before resolving each deck: getDeckFromDatabase - // issues its own query on the same cached statement set. - QSqlQuery *query = - sqlInterface->prepareQuery("select id, color_identity from {prefix}_decklist_files where id_folder = " - ":id_folder and id_user = :id_user"); - query->bindValue(":id_folder", folderId); - query->bindValue(":id_user", userInfo->id()); - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespContextError; - } - QList> deckRows; - while (query->next()) { - deckRows.append({query->value(0).toInt(), query->value(1).toString()}); - } - for (const auto &[deckId, colorIdentity] : deckRows) { - DeckList *deck; - try { - deck = sqlInterface->getDeckFromDatabase(deckId, userInfo->id()); - } catch (Response::ResponseCode &r) { - return r; - } - items.append(makeShareItemFromDeck(*deck, colorIdentity)); - delete deck; - } - } else { - return Response::RespInvalidData; - } - - if (items.isEmpty() || (maxItems > 0 && items.size() > maxItems)) { - return Response::RespInvalidData; - } - - QString shareName = nameFromStdString(cmd.name()); - if (shareName.isEmpty()) { - shareName = "Shared decks"; - } - - const QString token = generateShareToken(); - qint64 expiresAt = 0; - if (!sqlInterface->createDeckShare(token, shareName, userInfo->id(), items, servatrice->getDeckShareExpiryDays(), - expiresAt)) { - return Response::RespInvalidData; - } - - Response_DeckShareCreate *re = new Response_DeckShareCreate; - re->set_token(token.toStdString()); - re->set_expires_at(expiresAt); - re->set_item_count(items.size()); - rc.setResponseExtension(re); - - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareListMine(const Command_DeckShareListMine & /*cmd*/, - ResponseContainer &rc) -{ - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - - sqlInterface->checkSql(); - - QList shares; - if (!sqlInterface->getDeckSharesForUser(userInfo->id(), shares)) { - return Response::RespContextError; - } - - Response_DeckShareListMine *re = new Response_DeckShareListMine; - for (const DeckShareSummaryRecord &share : shares) { - ServerInfo_DeckShareSummary *summary = re->add_shares(); - summary->set_id(share.id); - summary->set_name(share.name.toStdString()); - summary->set_creation_time(share.creationTime); - summary->set_expires_at(share.expiresAt); - summary->set_item_count(share.itemCount); - } - rc.setResponseExtension(re); - - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareRemove(const Command_DeckShareRemove &cmd, - ResponseContainer & /*rc*/) -{ - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - - if (!cmd.has_share_id()) { - return Response::RespInvalidData; - } - - sqlInterface->checkSql(); - - if (!sqlInterface->deleteDeckShare(cmd.share_id(), userInfo->id())) { - return Response::RespNameNotFound; - } - - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareList(const Command_DeckShareList &cmd, - ResponseContainer &rc) -{ - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - - sqlInterface->checkSql(); - - QString name; - qint64 expiresAt = 0; - QList items; - if (!sqlInterface->getDeckShareList(nameFromStdString(cmd.token()), name, expiresAt, items)) { - return Response::RespNameNotFound; - } - - Response_DeckShareList *re = new Response_DeckShareList; - re->set_name(name.toStdString()); - re->set_expires_at(expiresAt); - for (const DeckShareItemRecord &item : items) { - ServerInfo_DeckShareItem *itemInfo = re->add_items(); - itemInfo->set_id(item.id); - itemInfo->set_name(item.name.toStdString()); - for (const QString &tag : item.tags) { - itemInfo->add_tags(tag.toStdString()); - } - itemInfo->set_banner_card(item.bannerCard.toStdString()); - itemInfo->set_game_format(item.gameFormat.toStdString()); - itemInfo->set_color_identity(item.colorIdentity.toStdString()); - } - rc.setResponseExtension(re); - - return Response::RespOk; -} - -Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareDownload(const Command_DeckShareDownload &cmd, - ResponseContainer &rc) -{ - if (authState != PasswordRight) { - return Response::RespFunctionNotAllowed; - } - - sqlInterface->checkSql(); - - QString content; - if (!sqlInterface->getDeckShareItem(nameFromStdString(cmd.token()), cmd.item_id(), content)) { - return Response::RespNameNotFound; - } - - Response_DeckShareDownload *re = new Response_DeckShareDownload; - re->set_deck(content.toStdString()); - rc.setResponseExtension(re); - - return Response::RespOk; -} - Response::ResponseCode AbstractServerSocketInterface::cmdReplayList(const Command_ReplayList & /*cmd*/, ResponseContainer &rc) { diff --git a/servatrice/src/serversocketinterface.h b/servatrice/src/serversocketinterface.h index 36b900d28..b464e6a9b 100644 --- a/servatrice/src/serversocketinterface.h +++ b/servatrice/src/serversocketinterface.h @@ -45,19 +45,11 @@ class ServerInfo_DeckStorage_Folder; class Command_AddToList; class Command_RemoveFromList; class Command_DeckList; -class Command_DeckListOtherUser; class Command_DeckNewDir; class Command_DeckDelDir; class Command_DeckDel; class Command_DeckDownload; -class Command_DeckDownloadPublic; class Command_DeckUpload; -class Command_DeckSetVisibility; -class Command_DeckShareCreate; -class Command_DeckShareList; -class Command_DeckShareListMine; -class Command_DeckShareRemove; -class Command_DeckShareDownload; class Command_ReplayList; class Command_ReplayDownload; class Command_ReplayModifyMatch; @@ -105,16 +97,8 @@ private: Response::ResponseCode cmdRemoveFromList(const Command_RemoveFromList &cmd, ResponseContainer &rc); int getDeckPathId(int basePathId, QStringList path); int getDeckPathId(const QString &path); - bool deckListHelper(int folderId, - ServerInfo_DeckStorage_Folder *folder, - int userId, - bool inheritedPublic, - bool publicOnly); - int getDeckOwnerId(int deckId); - bool isDeckEffectivelyPublic(int deckId); + bool deckListHelper(int folderId, ServerInfo_DeckStorage_Folder *folder); Response::ResponseCode cmdDeckList(const Command_DeckList &cmd, ResponseContainer &rc); - Response::ResponseCode cmdDeckListOtherUser(const Command_DeckListOtherUser &cmd, ResponseContainer &rc); - Response::ResponseCode cmdDeckSetVisibility(const Command_DeckSetVisibility &cmd, ResponseContainer &rc); Response::ResponseCode cmdDeckNewDir(const Command_DeckNewDir &cmd, ResponseContainer &rc); void deckDelDirHelper(int basePathId); void sendServerMessage(const QString userName, const QString message); @@ -123,12 +107,6 @@ private: Response::ResponseCode cmdDeckUpload(const Command_DeckUpload &cmd, ResponseContainer &rc); DeckList *getDeckFromDatabase(int deckId); Response::ResponseCode cmdDeckDownload(const Command_DeckDownload &cmd, ResponseContainer &rc); - Response::ResponseCode cmdDeckDownloadPublic(const Command_DeckDownloadPublic &cmd, ResponseContainer &rc); - Response::ResponseCode cmdDeckShareCreate(const Command_DeckShareCreate &cmd, ResponseContainer &rc); - Response::ResponseCode cmdDeckShareList(const Command_DeckShareList &cmd, ResponseContainer &rc); - Response::ResponseCode cmdDeckShareListMine(const Command_DeckShareListMine &cmd, ResponseContainer &rc); - Response::ResponseCode cmdDeckShareRemove(const Command_DeckShareRemove &cmd, ResponseContainer &rc); - Response::ResponseCode cmdDeckShareDownload(const Command_DeckShareDownload &cmd, ResponseContainer &rc); Response::ResponseCode cmdReplayList(const Command_ReplayList &cmd, ResponseContainer &rc); Response::ResponseCode cmdReplayDownload(const Command_ReplayDownload &cmd, ResponseContainer &rc); Response::ResponseCode cmdReplayModifyMatch(const Command_ReplayModifyMatch &cmd, ResponseContainer &rc); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7bb834d7e..4f5dc88eb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,12 +12,10 @@ add_test(NAME server_card_counter_test COMMAND server_card_counter_test) add_test(NAME server_counter_test COMMAND server_counter_test) add_test(NAME server_rate_limiter_test COMMAND server_rate_limiter_test) add_test(NAME server_developer_role_test COMMAND server_developer_role_test) -add_test(NAME server_game_join_test COMMAND server_game_join_test) add_test(NAME warning_categories_test COMMAND warning_categories_test) add_test(NAME lag_monitor_test COMMAND lag_monitor_test) add_test(NAME latency_tracker_test COMMAND latency_tracker_test) add_test(NAME metrics_registry_test COMMAND metrics_registry_test) -add_test(NAME loader_local_matching_test COMMAND loader_local_matching_test) add_test(NAME deck_hash_performance_test COMMAND deck_hash_performance_test) set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 15) @@ -35,23 +33,11 @@ add_executable(server_card_counter_test server_card_counter_test.cpp) add_executable(server_counter_test server_counter_test.cpp) add_executable(server_rate_limiter_test server_rate_limiter_test.cpp) add_executable(server_developer_role_test server_developer_role_test.cpp) -add_executable(server_game_join_test server_game_join_test.cpp) add_executable(warning_categories_test warning_categories_test.cpp) add_executable(lag_monitor_test ${CMAKE_SOURCE_DIR}/cockatrice/src/client/lag_monitor.cpp lag_monitor_test.cpp) target_include_directories(lag_monitor_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src) add_executable(latency_tracker_test latency_tracker_test.cpp) add_executable(metrics_registry_test ../servatrice/src/metrics_registry.cpp metrics_registry_test.cpp) -add_executable( - loader_local_matching_test - ${CMAKE_SOURCE_DIR}/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp - ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/cache_settings.cpp - ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/card_counter_settings.cpp - ${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/shortcuts_settings.cpp - ${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/client/release_channel.cpp - ${VERSION_STRING_CPP} - loader_local_matching_test.cpp -) -target_include_directories(loader_local_matching_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src) find_package(GTest) @@ -89,12 +75,10 @@ if(NOT GTEST_FOUND) add_dependencies(server_counter_test gtest) add_dependencies(server_rate_limiter_test gtest) add_dependencies(server_developer_role_test gtest) - add_dependencies(server_game_join_test gtest) add_dependencies(warning_categories_test gtest) add_dependencies(lag_monitor_test gtest) add_dependencies(latency_tracker_test gtest) add_dependencies(metrics_registry_test gtest) - add_dependencies(loader_local_matching_test gtest) endif() include_directories(${GTEST_INCLUDE_DIRS}) @@ -130,10 +114,6 @@ target_link_libraries( server_developer_role_test libcockatrice_network libcockatrice_rng Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} ) -target_link_libraries( - server_game_join_test libcockatrice_network_server_remote libcockatrice_rng Threads::Threads ${GTEST_BOTH_LIBRARIES} - ${TEST_QT_MODULES} -) target_link_libraries( warning_categories_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} ) @@ -143,9 +123,6 @@ target_link_libraries( ) target_include_directories(metrics_registry_test PRIVATE ${CMAKE_SOURCE_DIR}/servatrice/src) target_link_libraries(metrics_registry_test ${TEST_QT_MODULES} Threads::Threads ${GTEST_BOTH_LIBRARIES}) -target_link_libraries( - loader_local_matching_test libcockatrice_settings Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} -) add_subdirectory(card_zone_algorithms) add_subdirectory(carddatabase) diff --git a/tests/carddatabase/carddatabase_test.cpp b/tests/carddatabase/carddatabase_test.cpp index 392f6395a..3fa0e3834 100644 --- a/tests/carddatabase/carddatabase_test.cpp +++ b/tests/carddatabase/carddatabase_test.cpp @@ -2,14 +2,9 @@ #include "test_card_database_path_provider.h" #include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include #include #include + namespace { @@ -38,49 +33,6 @@ TEST(CardDatabaseTest, LoadXml) ASSERT_EQ(0, db->query()->getAllMainCardTypes().size()) << "Types not empty after clear"; ASSERT_EQ(NotLoaded, db->getLoadStatus()) << "Incorrect status after clear"; } - -TEST(CardDatabaseTest, Xml4LocalizedDataRoundTrip) -{ - NoopCardSetPriorityController controller; - CardSetPtr set = - CardSet::newInstance(&controller, "TST", "Test Set", "expansion", QDate(), CardSet::PriorityPrimary); - - QHash props; - props["manacost"] = "1R"; - PrintingInfo printing(set, LazyPropertiesHash(props)); - SetToPrintingsMap setsInfo; - setsInfo["TST"].append(printing); - - CardInfo::UiAttributes attributes = {.tableRow = 1}; - CardInfoPtr card = - CardInfo::newInstance("Lightning Bolt", "Deal 3 damage.", false, {}, {}, {}, setsInfo, attributes); - card->setLocalizedName("de", "Blitzschlag"); - card->setLocalizedText("de", "Blitzschlag fügt 3 Schadenspunkte zu."); - - SetNameMap sets; - sets.insert("TST", set); - CardNameMap cards; - cards.insert("Lightning Bolt", card); - - QTemporaryDir tempDir; - const QString fileName = tempDir.filePath("cards.xml"); - NoopCardPreferenceProvider prefProvider; - CockatriceXml4Parser writer(&prefProvider, &controller); - ASSERT_TRUE(writer.saveToFile({}, sets, cards, fileName)); - - CardDatabaseData data; - CockatriceXml4Parser parser(&prefProvider, &controller); - QFile file(fileName); - ASSERT_TRUE(file.open(QIODevice::ReadOnly)); - parser.parseFileInto(file, data); - - CardInfoPtr loaded = data.cards.value("Lightning Bolt"); - ASSERT_FALSE(loaded.isNull()); - ASSERT_EQ(loaded->getName(), "Lightning Bolt"); - ASSERT_EQ(loaded->getLocalizedName("de"), "Blitzschlag"); - ASSERT_EQ(loaded->getLocalizedText("de"), "Blitzschlag fügt 3 Schadenspunkte zu."); - ASSERT_EQ(loaded->getLocalizedText("fr"), "Deal 3 damage."); -} } // namespace int main(int argc, char **argv) diff --git a/tests/loader_local_matching_test.cpp b/tests/loader_local_matching_test.cpp deleted file mode 100644 index 7ffa7157d..000000000 --- a/tests/loader_local_matching_test.cpp +++ /dev/null @@ -1,187 +0,0 @@ -#include "client/settings/cache_settings.h" -#include "interface/card_picture_loader/card_picture_loader_local.h" - -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ - -/** - * @brief Builds an ExactCard with the requested identity fields. - * - * Mirrors how the client constructs cards: the set short name feeds tryLoad()'s - * setName, and the "num" printing property feeds the collector number. - */ -ExactCard cardFor(const QString &name, const QString &setShortName, const QString &collectorNumber) -{ - CardSetPtr set; - if (!setShortName.isEmpty()) { - set = CardSet::newInstance(new NoopCardSetPriorityController(), setShortName, setShortName); - } - - LazyPropertiesHash properties; - if (!collectorNumber.isEmpty()) { - properties.insert("num", collectorNumber); - } - - return ExactCard(CardInfo::newInstance(name), PrintingInfo(set, properties)); -} - -class LocalMatcherTest : public ::testing::Test -{ -protected: - QTemporaryDir tempDir; ///< Sandboxed "pics" root for every test. - CardPictureLoaderLocal *loader = nullptr; ///< Constructed per test against the sandboxed paths. - - QString picsPath() const - { - return tempDir.path() + "/pics"; - } - - void SetUp() override - { - // The loader ctor snapshots the global picture paths once, so point them at the - // sandbox before constructing it. - SettingsCache::instance().paths().setPicsPath(picsPath()); - SettingsCache::instance().paths().setCustomPicsPath(picsPath() + "/CUSTOM/"); - - loader = new CardPictureLoaderLocal(nullptr); - } - - void TearDown() override - { - delete loader; - loader = nullptr; - } - - /** - * @brief Writes a valid 1x1 PNG under the sandboxed pics path. - */ - void writePngUnderPics(const QString &relativePath, const QColor &color = Qt::red) - { - const QString fullPath = picsPath() + "/" + relativePath; - ASSERT_TRUE(QDir().mkpath(QFileInfo(fullPath).absolutePath())); - - QImage image(1, 1, QImage::Format_RGB32); - image.fill(color); - - QImageWriter writer(fullPath, "PNG"); - ASSERT_TRUE(writer.write(image)); - } -}; - -TEST_F(LocalMatcherTest, ExactMatchBareFileWinsOverSuffixedCompanion) -{ - writePngUnderPics("downloadedPics/TestCard.png"); - writePngUnderPics("downloadedPics/TestCard (1).png"); - - const QImage image = loader->tryLoad(cardFor("TestCard", "", "")); - - EXPECT_FALSE(image.isNull()) << "The bare TestCard.png must be picked over its suffixed companion"; -} - -TEST_F(LocalMatcherTest, SuffixedFileWithoutExactMatchIsNotLoaded) -{ - // The pre-refactor prefix match would have accepted "TestCard (1).png" for "TestCard". - writePngUnderPics("downloadedPics/TestCard (1).png"); - - const QImage image = loader->tryLoad(cardFor("TestCard", "", "")); - - EXPECT_TRUE(image.isNull()) << "A suffixed file must not satisfy an exact card-name lookup"; -} - -TEST_F(LocalMatcherTest, SetFolderLookupIgnoresSuffixedFiles) -{ - writePngUnderPics("M10/TestCard (1).png"); - - const QImage image = loader->tryLoad(cardFor("TestCard", "M10", "")); - - EXPECT_TRUE(image.isNull()) << "Set-folder lookups must also require an exact name match"; -} - -TEST_F(LocalMatcherTest, SetFolderLookupStillResolvesExactFile) -{ - writePngUnderPics("M10/TestCard.png"); - writePngUnderPics("M10/TestCard (1).png"); - - const QImage image = loader->tryLoad(cardFor("TestCard", "M10", "")); - - EXPECT_FALSE(image.isNull()) << "The exact file in the set folder must still resolve"; -} - -TEST_F(LocalMatcherTest, RootDownloadedPicsFallbackResolvesSchemeFilename) -{ - // Non-set-folder export schemes (Name_Set_Collector) write straight into downloadedPics/. - writePngUnderPics("downloadedPics/TestCard_M10_1.png"); - - const QImage image = loader->tryLoad(cardFor("TestCard", "M10", "1")); - - EXPECT_FALSE(image.isNull()) << "downloadedPics/TestCard_M10_1.png must resolve via the root fallback"; -} - -TEST_F(LocalMatcherTest, RootFallbackResolvesDashSeparatedVariant) -{ - writePngUnderPics("downloadedPics/TestCard-M10-1.png"); - - const QImage image = loader->tryLoad(cardFor("TestCard", "M10", "1")); - - EXPECT_FALSE(image.isNull()) << "The dash-separated import variant must resolve via the root fallback"; -} - -TEST_F(LocalMatcherTest, DownloadedPicsSetSubfolderStillResolves) -{ - writePngUnderPics("downloadedPics/M10/TestCard_M10_1.png"); - - const QImage image = loader->tryLoad(cardFor("TestCard", "M10", "1")); - - EXPECT_FALSE(image.isNull()) << "The set-subfolder export scheme must keep resolving"; -} - -TEST_F(LocalMatcherTest, SetFolderCandidateTakesPrecedenceOverRootFallback) -{ - writePngUnderPics("M10/TestCard_M10_1.png", Qt::red); - writePngUnderPics("downloadedPics/TestCard_M10_1.png", Qt::blue); - - const QImage image = loader->tryLoad(cardFor("TestCard", "M10", "1")); - - ASSERT_FALSE(image.isNull()); - EXPECT_EQ(image.pixelColor(0, 0), QColor(Qt::red)) << "The set-folder candidate must be preferred"; -} - -} // namespace - -int main(int argc, char **argv) -{ - // Redirect SettingsCache reads/writes (app-data location) away from the real user profile. - QStandardPaths::setTestModeEnabled(true); - - // Some CI containers run as a uid without a passwd entry (e.g. GitHub's docker - // runner), so HOME resolves to "/" and the test-mode qttest data dir cannot be - // created. SettingsCache's QSettings then silently drops every write, reads come - // back empty, and the paths the loader searches are "". Give the test a writable - // HOME for the duration of the run so settings behave like on a normal machine. - QTemporaryDir home; - if (home.isValid()) { - qputenv("HOME", home.path().toLocal8Bit()); - } - - QCoreApplication app(argc, argv); - QLoggingCategory::setFilterRules("card_picture_loader.*=false\nsettings_cache.*=false"); - - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} \ No newline at end of file diff --git a/tests/oracle/oracle_importer_test.cpp b/tests/oracle/oracle_importer_test.cpp index f66616e37..66c7bdbe1 100644 --- a/tests/oracle/oracle_importer_test.cpp +++ b/tests/oracle/oracle_importer_test.cpp @@ -72,18 +72,6 @@ protected: return card; } - // Helper: build a single MTGJSON foreignData entry - QJsonObject makeForeignEntry(const QString &language, const QString &name, const QString &text) - { - QJsonObject entry; - entry["language"] = language; - entry["name"] = name; - if (!text.isEmpty()) { - entry["text"] = text; - } - return entry; - } - NoopCardSetPriorityController *controller; OracleImporter *importer; CardSetPtr set; @@ -884,243 +872,6 @@ TEST_F(OracleImporterTest, DisablingProgressReportingSuppressesScanEmissions) ASSERT_GT(emissions, 0); } -// Localized card text tests -// ============================================================================ - -TEST_F(OracleImporterTest, ImportsLocalizedTextForRequestedLanguage) -{ - QJsonObject card = makeCard("Lightning Bolt"); - card["foreignData"] = - QJsonArray{makeForeignEntry("German", "Blitzschlag", "Blitzschlag fügt 3 Schadenspunkte zu.")}; - QJsonArray cards{card}; - - importer->setCardLang("de"); - importer->importCardsFromSet(set, cards); - importer->applyLocalizedData(); - - auto result = importer->getCardList().value("Lightning Bolt"); - ASSERT_FALSE(result.isNull()); - ASSERT_EQ(result->getLocalizedName("de"), "Blitzschlag"); - ASSERT_EQ(result->getLocalizedText("de"), "Blitzschlag fügt 3 Schadenspunkte zu."); - // English identity untouched - ASSERT_EQ(result->getName(), "Lightning Bolt"); - ASSERT_EQ(result->getText(), "Rules text."); -} - -TEST_F(OracleImporterTest, ImportsLocalizedNameAndTextForMultiFaceCards) -{ - // MTGJSON reports multi-face cards (adventure/split/aftermath/prepare) as one - // card object per face; every face carries the joined name but only its own - // face's rules text in foreignData. The importer joins the per-face texts with - // the same separator as the English merge. - QJsonObject front = makeCard("Disruptive Stormbrood // Petty Revenge"); - front["layout"] = "adventure"; - front["faceName"] = "Disruptive Stormbrood"; - front["side"] = "a"; - front["foreignData"] = QJsonArray{ - makeForeignEntry("German", "Disruptive Stormbrood // Kleinliche Rache", - "Fliegend\nWenn diese Kreatur ins Spiel kommt, zerstöre bis zu ein Artefakt oder eine " - "Verzauberung deiner Wahl.")}; - QJsonObject back = makeCard("Disruptive Stormbrood // Petty Revenge"); - back["layout"] = "adventure"; - back["faceName"] = "Petty Revenge"; - back["side"] = "b"; - back["text"] = "Destroy target creature."; - back["foreignData"] = QJsonArray{makeForeignEntry("German", "Disruptive Stormbrood // Kleinliche Rache", - "Zerstöre eine Kreatur deiner Wahl mit Stärke 3 oder weniger.")}; - QJsonArray cards{front, back}; - - importer->setCardLang("de"); - importer->importCardsFromSet(set, cards); - importer->applyLocalizedData(); - - auto result = importer->getCardList().value("Disruptive Stormbrood // Petty Revenge"); - ASSERT_FALSE(result.isNull()); - ASSERT_EQ(result->getLocalizedName("de"), "Disruptive Stormbrood // Kleinliche Rache"); - ASSERT_EQ(result->getLocalizedText("de"), - "Fliegend\nWenn diese Kreatur ins Spiel kommt, zerstöre bis zu ein Artefakt oder eine Verzauberung " - "deiner Wahl.\n\n---\n\nZerstöre eine Kreatur deiner Wahl mit Stärke 3 oder weniger."); - // English identity untouched - ASSERT_EQ(result->getName(), "Disruptive Stormbrood // Petty Revenge"); - ASSERT_EQ(result->getText(), "Rules text.\n\n---\n\nDestroy target creature."); -} - -TEST_F(OracleImporterTest, MultiFaceCardsWithoutCompleteForeignTextKeepEnglishText) -{ - // Both faces must carry a foreignData text for the joined text; otherwise the - // rules text stays English while the localized name (from a later complete - // printing) is still applied. - QJsonObject front = makeCard("Wear // Tear"); - front["layout"] = "split"; - front["faceName"] = "Wear"; - front["side"] = "a"; - front["foreignData"] = QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "Verschleiß-Text.")}; - QJsonObject back = makeCard("Wear // Tear"); - back["layout"] = "split"; - back["faceName"] = "Tear"; - back["side"] = "b"; - back["text"] = "Tear rules text."; - back["foreignData"] = QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "")}; - QJsonArray cards{front, back}; - - importer->setCardLang("de"); - importer->importCardsFromSet(set, cards); - importer->applyLocalizedData(); - - auto result = importer->getCardList().value("Wear // Tear"); - ASSERT_FALSE(result.isNull()); - // The joined name is still applied. - ASSERT_EQ(result->getLocalizedName("de"), "Verschleiß // Zerrreißung"); - // The incomplete text must not become the card's localized text. - ASSERT_TRUE(result->getLocalizedTexts().isEmpty()); - ASSERT_EQ(result->getText(), "Rules text.\n\n---\n\nTear rules text."); -} - -TEST_F(OracleImporterTest, DefaultLanguageSkipsForeignData) -{ - QJsonObject card = makeCard("Lightning Bolt"); - card["foreignData"] = - QJsonArray{makeForeignEntry("German", "Blitzschlag", "Blitzschlag fügt 3 Schadenspunkte zu.")}; - QJsonArray cards{card}; - - // cardLang defaults to "en" — foreignData must never be imported - importer->importCardsFromSet(set, cards); - importer->applyLocalizedData(); - - auto result = importer->getCardList().value("Lightning Bolt"); - ASSERT_FALSE(result.isNull()); - ASSERT_TRUE(result->getLocalizedNames().isEmpty()); - ASSERT_TRUE(result->getLocalizedTexts().isEmpty()); -} - -TEST_F(OracleImporterTest, UnsupportedLanguageSkipsForeignData) -{ - QJsonObject card = makeCard("Lightning Bolt"); - card["foreignData"] = QJsonArray{makeForeignEntry("xx", "Kochanie", "Grzmot uderza.")}; - QJsonArray cards{card}; - - importer->setCardLang("xx"); - importer->importCardsFromSet(set, cards); - importer->applyLocalizedData(); - - auto result = importer->getCardList().value("Lightning Bolt"); - ASSERT_FALSE(result.isNull()); - ASSERT_TRUE(result->getLocalizedNames().isEmpty()); -} - -TEST_F(OracleImporterTest, NonMatchingLanguageNotCollected) -{ - QJsonObject card = makeCard("Lightning Bolt"); - card["foreignData"] = QJsonArray{makeForeignEntry("French", "Éclair", "L'Éclair inflige 3 blessures.")}; - QJsonArray cards{card}; - - importer->setCardLang("de"); - importer->importCardsFromSet(set, cards); - importer->applyLocalizedData(); - - auto result = importer->getCardList().value("Lightning Bolt"); - ASSERT_FALSE(result.isNull()); - ASSERT_TRUE(result->getLocalizedNames().isEmpty()); -} - -TEST_F(OracleImporterTest, HigherPrioritySetWinsForReprint) -{ - // First printing in a reprint set, then another in a (more authoritative) - // core set: the core set's German text must win even though it was seen later. - QJsonObject reprintCard = makeCard("Lightning Bolt"); - reprintCard["foreignData"] = QJsonArray{makeForeignEntry("German", "Blitzschlag", "Älterer deutscher Text.")}; - CardSetPtr reprintSet = - CardSet::newInstance(controller, "TS2", "Second Set", QString(), QDate(), CardSet::PriorityReprint); - importer->setCardLang("de"); - importer->importCardsFromSet(reprintSet, QJsonArray{reprintCard}); - - QJsonObject primaryCard = makeCard("Lightning Bolt"); - primaryCard["foreignData"] = - QJsonArray{makeForeignEntry("German", "Blitzschlag", "Blitzschlag fügt 3 Schadenspunkte zu.")}; - CardSetPtr primarySet = - CardSet::newInstance(controller, "TS3", "Third Set", QString(), QDate(), CardSet::PriorityPrimary); - importer->importCardsFromSet(primarySet, QJsonArray{primaryCard}); - importer->applyLocalizedData(); - - auto result = importer->getCardList().value("Lightning Bolt"); - ASSERT_FALSE(result.isNull()); - ASSERT_EQ(result->getLocalizedName("de"), "Blitzschlag"); - ASSERT_EQ(result->getLocalizedText("de"), "Blitzschlag fügt 3 Schadenspunkte zu."); - ASSERT_EQ(importer->getCardList().size(), 1); -} - -TEST_F(OracleImporterTest, HigherPrioritySplitSetWinsForReprint) -{ - // Split cards print each face as its own card object; the joined text is - // collected per set with the same priority policy as single-face cards, so a - // reprint set's German text must yield to the core set's even when reprints - // are imported first. - QJsonObject reprintFront = makeCard("Wear // Tear"); - reprintFront["layout"] = "split"; - reprintFront["faceName"] = "Wear"; - reprintFront["side"] = "a"; - reprintFront["foreignData"] = - QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "Wear alter Text.")}; - QJsonObject reprintBack = makeCard("Wear // Tear"); - reprintBack["layout"] = "split"; - reprintBack["faceName"] = "Tear"; - reprintBack["side"] = "b"; - reprintBack["foreignData"] = - QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "Tear alter Text.")}; - CardSetPtr reprintSet = - CardSet::newInstance(controller, "TS2", "Second Set", QString(), QDate(), CardSet::PriorityReprint); - importer->setCardLang("de"); - importer->importCardsFromSet(reprintSet, QJsonArray{reprintFront, reprintBack}); - - QJsonObject primaryFront = makeCard("Wear // Tear"); - primaryFront["layout"] = "split"; - primaryFront["faceName"] = "Wear"; - primaryFront["side"] = "a"; - primaryFront["foreignData"] = - QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "Wear neuer Text.")}; - QJsonObject primaryBack = makeCard("Wear // Tear"); - primaryBack["layout"] = "split"; - primaryBack["faceName"] = "Tear"; - primaryBack["side"] = "b"; - primaryBack["foreignData"] = - QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "Tear neuer Text.")}; - CardSetPtr primarySet = - CardSet::newInstance(controller, "TS3", "Third Set", QString(), QDate(), CardSet::PriorityPrimary); - importer->importCardsFromSet(primarySet, QJsonArray{primaryFront, primaryBack}); - importer->applyLocalizedData(); - - auto result = importer->getCardList().value("Wear // Tear"); - ASSERT_FALSE(result.isNull()); - ASSERT_EQ(result->getLocalizedName("de"), "Verschleiß // Zerrreißung"); - ASSERT_EQ(result->getLocalizedText("de"), "Wear neuer Text.\n\n---\n\nTear neuer Text."); - ASSERT_EQ(importer->getCardList().size(), 1); -} - -TEST_F(OracleImporterTest, StartImportAppliesLocalizedData) -{ - QJsonObject card = makeCard("Lightning Bolt"); - card["foreignData"] = - QJsonArray{makeForeignEntry("Portuguese (Brazil)", "Raio", "Raio causa 3 de dano a qualquer alvo.")}; - QJsonObject dataSet; - dataSet["code"] = "tst"; - dataSet["name"] = "Test Set"; - dataSet["type"] = "expansion"; - dataSet["releaseDate"] = "2024-01-01"; - dataSet["cards"] = QJsonArray{card}; - - QJsonObject root; - root["data"] = QJsonObject{{"TST", dataSet}}; - - importer->setCardLang("pt"); - ASSERT_TRUE(importer->readSetsFromByteArray(QJsonDocument(root).toJson(QJsonDocument::Compact))); - ASSERT_EQ(importer->startImport(), 1); - - auto result = importer->getCardList().value("Lightning Bolt"); - ASSERT_FALSE(result.isNull()); - ASSERT_EQ(result->getLocalizedName("pt"), "Raio"); - ASSERT_EQ(result->getLocalizedText("pt"), "Raio causa 3 de dano a qualquer alvo."); -} - int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); diff --git a/tests/server_game_join_test.cpp b/tests/server_game_join_test.cpp deleted file mode 100644 index c84e4e066..000000000 --- a/tests/server_game_join_test.cpp +++ /dev/null @@ -1,174 +0,0 @@ -/** @file server_game_join_test.cpp - * @brief Tests for the moderator/judge game-entry restriction override in Server_Game::checkJoin. - * @ingroup Tests - */ - -#include "game/server_game.h" -#include "server.h" -#include "server_database_interface.h" -#include "server_room.h" - -#include -#include -#include - -RNG_Abstract *rng = nullptr; // referenced by the server_remote library - -namespace -{ - -class MockDatabaseInterface : public Server_DatabaseInterface -{ -public: - AuthenticationResult checkUserPassword(Server_ProtocolHandler *, - const QString &, - const QString &, - const QString &, - QString &, - int &, - bool) override - { - return NotLoggedIn; - } - int getNextReplayId() override - { - return 1; - } - int getNextGameId() override - { - return 1; - } - int getActiveUserCount(QString) override - { - return 0; - } - ServerInfo_User getUserData(const QString &, bool) override - { - return ServerInfo_User(); - } -}; - -class FakeServer : public Server -{ -public: - FakeServer() - { - setDatabaseInterface(new MockDatabaseInterface()); - } -}; - -class GameJoinOverrideTest : public ::testing::Test -{ -protected: - FakeServer server; - Server_Room room{0, 0, "", "", "", "", false, "", {}, &server}; - ServerInfo_User creator; - ServerInfo_User plainUser; - ServerInfo_User unregisteredJudge; - ServerInfo_User moderator; - ServerInfo_User judge; - Server_Game *game = nullptr; - - void SetUp() override - { - creator.set_name("creator"); - creator.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered); - plainUser.set_name("plain-user"); - plainUser.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered); - unregisteredJudge.set_name("unregistered-judge"); - unregisteredJudge.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsJudge); - moderator.set_name("moderator"); - moderator.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered | - ServerInfo_User::IsModerator); - judge.set_name("judge"); - judge.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered | ServerInfo_User::IsJudge); - } - - void TearDown() override - { - delete game; - } - - Server_Game *makeGame(bool passwordProtected, bool onlyRegistered, bool onlyBuddies, bool spectatorsAllowed) - { - GameConfig config{.creatorInfo = creator, - .gameId = 1, - .description = QString(), - .password = passwordProtected ? "secret" : QString(), - .maxPlayers = 2, - .gameTypes = QList(), - .onlyBuddies = onlyBuddies, - .onlyRegistered = onlyRegistered, - .spectatorsAllowed = spectatorsAllowed, - .spectatorsNeedPassword = true, - .spectatorsCanTalk = false, - .spectatorsSeeEverything = false, - .startingLifeTotal = 20, - .shareDecklistsOnLoad = false}; - return new Server_Game(config, &room); - } -}; - -TEST_F(GameJoinOverrideTest, StaffBypassPasswordRestriction) -{ - game = makeGame(true, false, false, true); - - // A plain user cannot override the password even with the override flag set. - EXPECT_EQ(game->checkJoin(&plainUser, "wrong", false, true, false), Response::RespWrongPassword); - // Moderators and judges may enter any game regardless of the password. - EXPECT_EQ(game->checkJoin(&moderator, "wrong", false, true, false), Response::RespOk); - EXPECT_EQ(game->checkJoin(&judge, "wrong", false, true, false), Response::RespOk); - // Without the override flag judges are still subject to the password. - EXPECT_EQ(game->checkJoin(&judge, "wrong", false, false, true), Response::RespWrongPassword); - EXPECT_EQ(game->checkJoin(&judge, "secret", false, false, true), Response::RespOk); -} - -TEST_F(GameJoinOverrideTest, StaffBypassRegisteredOnlyRestriction) -{ - game = makeGame(false, true, false, true); - - // Without the override flag the only-registered restriction still applies. - EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), false, false, false), Response::RespUserLevelTooLow); - // An unregistered judge may enter when overriding restrictions. - EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), false, true, false), Response::RespOk); -} - -TEST_F(GameJoinOverrideTest, StaffBypassBuddiesOnlyRestriction) -{ - game = makeGame(false, false, true, true); - - // A plain user who is not on the creator's buddy list gets rejected. - EXPECT_EQ(game->checkJoin(&plainUser, QString(), false, true, false), Response::RespOnlyBuddies); - // Moderators and judges bypass the buddies-only restriction. - EXPECT_EQ(game->checkJoin(&moderator, QString(), false, true, false), Response::RespOk); - EXPECT_EQ(game->checkJoin(&judge, QString(), false, true, false), Response::RespOk); -} - -TEST_F(GameJoinOverrideTest, StaffBypassSpectatorsNotAllowedRestriction) -{ - game = makeGame(false, false, false, false); - - // A plain user cannot spectate when the game disallows spectators. - EXPECT_EQ(game->checkJoin(&plainUser, QString(), true, false, false), Response::RespSpectatorsNotAllowed); - // Moderators and judges may spectate any game regardless of the password - // and the spectator restriction. - EXPECT_EQ(game->checkJoin(&moderator, "wrong", true, true, false), Response::RespOk); - EXPECT_EQ(game->checkJoin(&judge, "wrong", true, true, false), Response::RespOk); -} - -TEST_F(GameJoinOverrideTest, JudgeOverrideDoesNotGrantJudgeJoinToPlainUser) -{ - game = makeGame(false, false, false, true); - - // joining with join_as_judge still requires the judge flag even when overriding. - EXPECT_EQ(game->checkJoin(&plainUser, QString(), false, true, true), Response::RespUserLevelTooLow); - EXPECT_EQ(game->checkJoin(&judge, QString(), false, true, true), Response::RespOk); -} - -} // namespace - -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} \ No newline at end of file diff --git a/tests/settings/settings_defaults_test.cpp b/tests/settings/settings_defaults_test.cpp index 0341b2b99..eda778ae9 100644 --- a/tests/settings/settings_defaults_test.cpp +++ b/tests/settings/settings_defaults_test.cpp @@ -386,19 +386,6 @@ TEST_F(SettingsDefaultsTest, Appearance_HomeTabDisplayCardName_Default) ASSERT_EQ(s.getHomeTabDisplayCardName(), true); } -TEST_F(SettingsDefaultsTest, Appearance_HomeTabBackgroundDim_Default) -{ - AppearanceSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getHomeTabBackgroundDim(), true); -} - -TEST_F(SettingsDefaultsTest, Appearance_HomeTabBackgroundDim_SetAndGet) -{ - AppearanceSettings s(settingsPath, nullptr); - s.setHomeTabBackgroundDim(false); - ASSERT_EQ(s.getHomeTabBackgroundDim(), false); -} - // --- InterfaceSettings --- TEST_F(SettingsDefaultsTest, Interface_ShowStatusBar_Default) @@ -562,19 +549,6 @@ TEST_F(SettingsDefaultsTest, CardsDisplay_ArrowDrawAnimation_Default) ASSERT_EQ(s.getArrowDrawAnimation(), true); } -TEST_F(SettingsDefaultsTest, CardsDisplay_CardLang_Default) -{ - CardsDisplaySettings s(settingsPath, nullptr); - ASSERT_EQ(s.getCardLang(), QString("en")); -} - -TEST_F(SettingsDefaultsTest, CardsDisplay_CardLang_SetAndGet) -{ - CardsDisplaySettings s(settingsPath, nullptr); - s.setCardLang("de"); - ASSERT_EQ(s.getCardLang(), QString("de")); -} - // --- VisualDeckStorageSettings --- TEST_F(SettingsDefaultsTest, VisualDeckStorage_SortingOrder_Default) @@ -603,19 +577,6 @@ TEST_F(SettingsDefaultsTest, VisualDeckStorage_DefaultTagsList_SetAndGet) ASSERT_EQ(s.getVisualDeckStorageDefaultTagsList(), custom); } -TEST_F(SettingsDefaultsTest, VisualDeckStorage_ShowUploadTime_Default) -{ - VisualDeckStorageSettings s(settingsPath, nullptr); - ASSERT_EQ(s.getVisualDeckStorageShowUploadTime(), true); -} - -TEST_F(SettingsDefaultsTest, VisualDeckStorage_ShowUploadTime_SetAndGet) -{ - VisualDeckStorageSettings s(settingsPath, nullptr); - s.setVisualDeckStorageShowUploadTime(false); - ASSERT_EQ(s.getVisualDeckStorageShowUploadTime(), false); -} - } // namespace int main(int argc, char **argv)