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 f3f172693..6c29ac739 100644 --- a/cmake/NSIS.template.in +++ b/cmake/NSIS.template.in @@ -16,6 +16,7 @@ Var ReinstallMode !include LogicLib.nsh !include FileFunc.nsh !include MUI2.nsh +!include nsExec.nsh !include x64.nsh !define MUI_ABORTWARNING @@ -216,16 +217,10 @@ FunctionEnd ; old and new Qt DLLs next to the new executable, failing with ; "The procedure entry point X could not be located in the dynamic link ; library ...Qt6Network.dll" on the next start. -; -; Processes are matched by image name AND by their executable path living -; under $INSTDIR, so unrelated processes that merely share an image name -; (e.g. the Oracle DB instance "oracle.exe") are never touched. -; usage: set $R2 to the base image name (without extension, e.g. "cockatrice", -; as accepted by Get-Process -Name), call this, result in $R0 -; (1 = running from $INSTDIR, 0 = not running) Function IsAppRunning - nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { $$_.Path -like $\'$INSTDIR\*$\' } | Select-Object -First 1"' + ; usage: set $R1 to the image name, call this, result in $R0 (1 = running, 0 = not running) + nsExec::Exec 'cmd /c tasklist /FI "IMAGENAME eq $R1" /FO CSV /NH | findstr /I "$R1" >nul' Pop $R0 ${If} $R0 = 0 StrCpy $R0 1 @@ -234,20 +229,8 @@ Function IsAppRunning ${EndIf} FunctionEnd -Function CloseMatchingApps - ; gracefully ask every matching instance to close (sends WM_CLOSE) - nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { $$_.Path -like $\'$INSTDIR\*$\' } | ForEach-Object { $$null = $$_.CloseMainWindow() }"' - Pop $R3 -FunctionEnd - -Function ForceCloseMatchingApps - nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { $$_.Path -like $\'$INSTDIR\*$\' } | Stop-Process -Force -ErrorAction SilentlyContinue"' - Pop $R3 -FunctionEnd - Function WaitForAppToClose - ; usage: set $R1 to the display name (e.g. "cockatrice.exe") and $R2 to the - ; base image name (e.g. "cockatrice") + ; usage: set $R1 to the image name Call IsAppRunning ${If} $R0 = 0 Return @@ -256,7 +239,8 @@ Function WaitForAppToClose ${If} ${Silent} ; ask the application to close gracefully (WM_CLOSE), then wait for it to exit DetailPrint "Closing $R1 ..." - Call CloseMatchingApps + nsExec::Exec 'cmd /c taskkill /IM $R1' + Pop $R2 StrCpy $R8 0 ck_wait_loop: Sleep 500 @@ -271,7 +255,8 @@ Function WaitForAppToClose ${EndIf} ; give up waiting, force close DetailPrint "Force closing $R1 ..." - Call ForceCloseMatchingApps + nsExec::Exec 'cmd /c taskkill /F /IM $R1' + Pop $R2 Sleep 500 ${Else} ck_wait_prompt: @@ -290,20 +275,17 @@ FunctionEnd Function EnsureAppsNotRunning StrCpy $R1 "cockatrice.exe" - StrCpy $R2 "cockatrice" Call WaitForAppToClose StrCpy $R1 "oracle.exe" - StrCpy $R2 "oracle" Call WaitForAppToClose StrCpy $R1 "servatrice.exe" - StrCpy $R2 "servatrice" Call WaitForAppToClose FunctionEnd ; Uninstaller copies of the same routines (the uninstaller gets its own ; function set compiled in, it cannot call the installer functions). Function un.IsAppRunning - nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { $$_.Path -like $\'$INSTDIR\*$\' } | Select-Object -First 1"' + nsExec::Exec 'cmd /c tasklist /FI "IMAGENAME eq $R1" /FO CSV /NH | findstr /I "$R1" >nul' Pop $R0 ${If} $R0 = 0 StrCpy $R0 1 @@ -312,16 +294,6 @@ Function un.IsAppRunning ${EndIf} FunctionEnd -Function un.CloseMatchingApps - nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { $$_.Path -like $\'$INSTDIR\*$\' } | ForEach-Object { $$null = $$_.CloseMainWindow() }"' - Pop $R3 -FunctionEnd - -Function un.ForceCloseMatchingApps - nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { $$_.Path -like $\'$INSTDIR\*$\' } | Stop-Process -Force -ErrorAction SilentlyContinue"' - Pop $R3 -FunctionEnd - Function un.WaitForAppToClose Call un.IsAppRunning ${If} $R0 = 0 @@ -330,7 +302,8 @@ Function un.WaitForAppToClose ${If} ${Silent} DetailPrint "Closing $R1 ..." - Call un.CloseMatchingApps + nsExec::Exec 'cmd /c taskkill /IM $R1' + Pop $R2 StrCpy $R8 0 un_ck_wait_loop: Sleep 500 @@ -344,7 +317,8 @@ Function un.WaitForAppToClose Goto un_ck_wait_loop ${EndIf} DetailPrint "Force closing $R1 ..." - Call un.ForceCloseMatchingApps + nsExec::Exec 'cmd /c taskkill /F /IM $R1' + Pop $R2 Sleep 500 ${Else} un_ck_wait_prompt: @@ -363,13 +337,10 @@ FunctionEnd Function un.EnsureAppsNotRunning StrCpy $R1 "cockatrice.exe" - StrCpy $R2 "cockatrice" Call un.WaitForAppToClose StrCpy $R1 "oracle.exe" - StrCpy $R2 "oracle" Call un.WaitForAppToClose StrCpy $R1 "servatrice.exe" - StrCpy $R2 "servatrice" Call un.WaitForAppToClose FunctionEnd @@ -500,7 +471,6 @@ ${EndIf} Call EnsureAppsNotRunning ${If} $PortableMode = 0 -${AndIf} ${FileExists} "$INSTDIR\cockatrice.exe" RMDir /r "$INSTDIR\Plugins" Delete "$INSTDIR\Qt*.dll" Delete "$INSTDIR\libcrypto*.dll" @@ -581,14 +551,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..4263fc6e2 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 @@ -454,8 +444,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 @@ -668,35 +656,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 +720,10 @@ Data = Resources\") " COMPONENT Runtime ) + + if(OPENSSL_FOUND) + install(FILES ${OPENSSL_INCLUDE_DIRS} DESTINATION ./) + endif() endif() if(Qt6LinguistTools_FOUND) 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/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_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/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..b0b31074c 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 @@ -520,11 +520,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_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_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/dialogs/dlg_update.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp index 46151481c..b7d6c5296 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp @@ -243,22 +243,13 @@ void DlgUpdate::downloadSuccessful(const QUrl &filepath) // Try to open the installer. If it opens, quit Cockatrice if (process.startDetached()) { qCInfo(DlgUpdateLog) << "Opened downloaded update file successfully - closing Cockatrice"; - // Close the main window synchronously so file locks are released before the NSIS installer - // (already launched) starts replacing files. This also flushes settings and shuts down the - // tabs, but only when the close is actually accepted: MainWindow may veto it for a running - // card DB update, an open game, or an unsaved deck. In that case keep running so the user - // can resolve the blocker, and tell them the installer is already waiting. + // Close the main window synchronously so settings are saved and file locks are released + // before the NSIS installer (already launched) starts replacing files, then quit the + // application for real in case the close was suppressed (e.g. by a pending prompt). if (auto *window = qobject_cast(parent())) { - if (window->close()) { - QTimer::singleShot(0, qApp, &QCoreApplication::quit); - } else { - QMessageBox::warning(this, tr("Update"), - tr("The update installer is already running and will finish the update once " - "Cockatrice closes. Cockatrice is still busy, so it stays open for now.")); - } - } else { - QTimer::singleShot(0, qApp, &QCoreApplication::quit); + window->close(); } + QTimer::singleShot(0, qApp, &QCoreApplication::quit); close(); } else { setLabel(tr("Error")); 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/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/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/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..722df90ef 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -512,7 +512,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 +707,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 +728,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 +879,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 +916,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/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp index 90f63c09d..0013cb475 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp @@ -38,48 +38,6 @@ static double parseClampedParam(const QString &valueString, double fallback, dou 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(); @@ -96,9 +54,25 @@ bool DeckList::Metadata::readElement(QXmlStreamReader *xml, const QString &child } else if (childName == "comments") { comments = xml->readElementText(); } else if (childName == "bannerCard") { - bannerCard = readBannerCard(xml); + QString providerId = xml->attributes().value("providerId").toString(); + QString cardName = xml->readElementText(); + bannerCard = {cardName, providerId}; } else if (childName == "playmatCard") { - playmat = readPlaymatCard(xml); + QString providerId = xml->attributes().value("providerId").toString(); + // Attributes are read before readElementText consumes the element. + 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(); + 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 = parseClampedParam(marginLStr, 0.07, 0.0, 0.95); + playmat.params.marginPctR = parseClampedParam(marginRStr, 0.07, 0.0, 0.95); + playmat.params.verticalOffset = parseClampedParam(vOffStr, 0.33, 0.0, 1.0); + playmat.params.zoom = parseClampedParam(zoomStr, 1.0, 0.1, 4.0); } else if (childName == "tags") { tags.clear(); // Clear existing tags while (xml->readNextStartElement()) { @@ -210,27 +184,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 +192,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()) { diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h index 199d3a9a8..229e2077c 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h @@ -106,22 +106,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_node_tree.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.cpp index 66f228d19..35c7943b4 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); } } } 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..94fd87680 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 @@ -147,14 +147,6 @@ private: InnerDecklistNode *getZoneObjFromName(const QString &zoneName); 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/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/inner_deck_list_node.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp index 86f7f5363..510a710cd 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,24 @@ 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); + 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; } } else if (xml->isEndElement() && (childName == "zone")) { return totalCards; @@ -192,35 +187,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..958229883 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,20 +223,6 @@ 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. @@ -250,18 +236,6 @@ public: * @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_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/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/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/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..af6118646 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,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -1081,183 +1079,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..f0e369449 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 @@ -102,37 +80,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..dfbdabb5e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,7 +12,6 @@ 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) @@ -35,7 +34,6 @@ 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) @@ -89,7 +87,6 @@ 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) @@ -130,10 +127,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} ) 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..c8c5ad4c8 100644 --- a/tests/settings/settings_defaults_test.cpp +++ b/tests/settings/settings_defaults_test.cpp @@ -603,19 +603,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)