diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index 911c4c55e49a8..fcafbbc9176f5 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -3,8 +3,7 @@ name: clang-tidy-review on: - pull_request: - types: [opened, synchronize, reopened] + workflow_dispatch: jobs: clang-tidy: diff --git a/.github/workflows/linux-clang-compile-tests.yml b/.github/workflows/linux-clang-compile-tests.yml index 0488b410edd56..50692318b0313 100644 --- a/.github/workflows/linux-clang-compile-tests.yml +++ b/.github/workflows/linux-clang-compile-tests.yml @@ -2,8 +2,7 @@ # SPDX-License-Identifier: GPL-2.0-or-later name: Linux Clang compilation and tests on: - pull_request: - types: [opened, synchronize, reopened] + workflow_dispatch: jobs: build: name: Linux Clang compilation and tests diff --git a/.github/workflows/linux-gcc-compile-tests.yml b/.github/workflows/linux-gcc-compile-tests.yml index c72c9b96ba6f7..edf9defd2c2aa 100644 --- a/.github/workflows/linux-gcc-compile-tests.yml +++ b/.github/workflows/linux-gcc-compile-tests.yml @@ -2,8 +2,7 @@ # SPDX-License-Identifier: GPL-2.0-or-later name: Linux GCC compilation and tests on: - pull_request: - types: [opened, synchronize, reopened] + workflow_dispatch: jobs: build: name: Linux GCC compilation and tests diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index be978022ce66f..a1430cbbf8a4a 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -2,8 +2,7 @@ # SPDX-License-Identifier: GPL-2.0-or-later name: SonarCloud analysis on: - pull_request: - types: [opened, synchronize, reopened] + workflow_dispatch: jobs: build: name: SonarCloud analysis diff --git a/.github/workflows/windows-build-and-test.yml b/.github/workflows/windows-build-and-test.yml index dab9d37a6bb2b..c5a3d093cf815 100644 --- a/.github/workflows/windows-build-and-test.yml +++ b/.github/workflows/windows-build-and-test.yml @@ -2,6 +2,11 @@ # SPDX-License-Identifier: GPL-2.0-or-later name: Windows Build and Test on: + workflow_dispatch: + # push: + # branches: + # - master + # - develop pull_request: types: [opened, synchronize, reopened] jobs: diff --git a/.gitignore b/.gitignore index e64d58766d85e..9038bbaa8b2b6 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,7 @@ t1.cfg ## Ignore Visual Studio Code config & environment files .vs/ -.vscode/ +# .vscode/ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. @@ -194,4 +194,9 @@ convert.exe *-w10startmenu.png *state-*.png theme.qrc +ionos-theme.qrc *.AppImage + +.idea/ + +shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/ diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000000000..3f0c64ea209b8 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,14 @@ +{ + "configurations": [ + { + "name": "Linux", + "compileCommands": [ + "${workspaceFolder}/../build/win32-MSVC-x64/RelWithDebInfo/compile_commands.json" + ], + "intelliSenseMode": "linux-gcc-x64", + "cStandard": "c17", + "cppStandard": "c++17" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000000..7957174658135 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,97 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "(Linux-IONOS-RelWithDebInfo) Launch HiDriveNext", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/linux-GCC-x64/RelWithDebInfo/bin/IONOS_HiDrive_Next", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + "name": "LD_LIBRARY_PATH", + "value": "/home/chaetty/CraftRoot/bin:${env:LD_LIBRARY_PATH}" + }, + { "name": "QML_IMPORT_TRACE", "value": "1" }, + { "name": "QT_LOGGING_RULES", "value": "qt.qml.debug=true" } + ], + "MIMode": "gdb", + "setupCommands": [ + { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true } + ] + }, + { + "name": "(Linux-STRATO-RelWithDebInfo) Launch HiDriveNext", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/linux-GCC-x64/RelWithDebInfo/bin/STRATO_HiDrive_Next", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + "name": "LD_LIBRARY_PATH", + "value": "/home/chaetty/CraftRoot/bin:${env:LD_LIBRARY_PATH}" + }, + { "name": "QML_IMPORT_TRACE", "value": "1" }, + { "name": "QT_LOGGING_RULES", "value": "qt.qml.debug=true" } + ], + "MIMode": "gdb", + "setupCommands": [ + { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true } + ] + }, + { + "name": "(RelWithDebInfo) Launch HiDriveNext", + "type": "cppvsdbg", + "request": "launch", + "program": "${workspaceFolder}/../build/win32-MSVC-x64/RelWithDebInfo/bin/ionos-hidrive-next.exe", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + "name": "PATH", + "value": "C:/CraftRoot/bin;C:/Craft64/bin;%PATH%" + }, + { "name": "QML_IMPORT_TRACE", "value": "1" }, + { "name": "QT_LOGGING_RULES", "value": "qt.qml.debug=true" } + ], + }, + { + "name": "(RelWithDebInfo) Launch NextCloud", + "type": "cppvsdbg", + "request": "launch", + "program": "${workspaceFolder}/../build/win32-MSVC-x64/RelWithDebInfo/bin/nextcloud.exe", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + "name": "PATH", + "value": "C:/CraftRoot/bin;C:/Craft64/bin;%PATH%" + }, + ], + }, + { + "name": "(Release) Launch NextCloud", + "type": "cppvsdbg", + "request": "launch", + "program": "${workspaceFolder}/../build/win32-MSVC-x64/Release/bin/nextcloud.exe", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + "name": "PATH", + "value": "C:/CraftRoot/bin;C:/Craft64/bin;%PATH%" + }, + ], + }, + ] +} \ No newline at end of file diff --git a/.vscode/nc-desktop-snippets.code-snippets b/.vscode/nc-desktop-snippets.code-snippets new file mode 100644 index 0000000000000..920dea7cb70f6 --- /dev/null +++ b/.vscode/nc-desktop-snippets.code-snippets @@ -0,0 +1,30 @@ +{ + // Place your nc-desktop workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and + // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope + // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is + // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. + // Placeholders with the same ids are connected. + // Example: + // "Print to console": { + // "scope": "javascript,typescript", + // "prefix": "log", + // "body": [ + // "console.log('$1');", + // "$2" + // ], + // "description": "Log output to console" + // } + + "DebugLog": { + "scope": "cpp", + "prefix": "dlog", + "body": [ + "char buffer$1[256];", + "sprintf(buffer$1, \"$2\\n\", $3);", + "OutputDebugStringA(buffer$1);" + ], + "description": "Debug log output" + } + +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000..d40b76680f637 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,22 @@ +{ + "cmake.generator": "Ninja", + "cmake.configureSettings": { + "CMAKE_PREFIX_PATH": [ + "C:/CraftRoot", + "C:/CraftRoot/dev-utils/bin", + "C:/Craft64", + "C:/Craft64/dev-utils/bin" + ], + "BUILD_TESTING":"OFF", + "LOCALBUILD": "ON", + "WHITELABEL_NAME": "ionos", + "CMAKE_CXX_CLANG_TIDY": "", + "DO_NOT_USE_PROXY": "ON", + }, + "cmake.buildArgs": ["-j", "16"], + "cmake.buildDirectory": "${workspaceFolder}/../build/${buildKitTargetOs}-${buildKitVendor}-${buildKitTargetArch}/${buildType}", + "files.associations": { + "qwizardpage": "cpp", + "xutility": "cpp" + }, +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000000000..153627d3103a2 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,37 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "clean RelWithDebInfo", + "type": "shell", + "command": "powershell", + "args": [ + "-NoProfile", + "-Command", + "Remove-Item -Recurse -Force ..\\build\\win32-MSVC-x64\\RelWithDebInfo" + ], + "problemMatcher": [], + "group": { + "kind": "build", + "isDefault": true + }, + "detail": "A task to clean the build directory using PowerShell" + }, + { + "label": "clean Release", + "type": "shell", + "command": "powershell", + "args": [ + "-NoProfile", + "-Command", + "Remove-Item -Recurse -Force ..\\build\\win32-MSVC-x64\\Release" + ], + "problemMatcher": [], + "group": { + "kind": "build", + "isDefault": true + }, + "detail": "A task to clean the build directory using PowerShell" + }, + ] +} \ No newline at end of file diff --git a/.vscode/wsl-nc-desktop.code-workspace b/.vscode/wsl-nc-desktop.code-workspace new file mode 100644 index 0000000000000..b890d2f8b5101 --- /dev/null +++ b/.vscode/wsl-nc-desktop.code-workspace @@ -0,0 +1,7 @@ +{ + "folders": [ + { + "path": ".." + } + ] +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a9c91deae16b..7869e449d5bc1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,8 @@ cmake_policy(SET CMP0071 NEW) # Enable use of QtQuick compiler/generated code project(client) +add_compile_definitions(IONOS_BUILD) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) if(APPLE) set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING "Minimum OSX deployment version") endif() @@ -15,10 +17,10 @@ set(CMAKE_CXX_STANDARD_REQUIRED 20) include(FeatureSummary) -find_program(CLANG_TIDY_EXE NAMES "clang-tidy") -if (CLANG_TIDY_EXE) - set(CMAKE_CXX_CLANG_TIDY ${CLANG_TIDY_EXE} -checks=-*,modernize-use-auto,modernize-use-using,modernize-use-nodiscard,modernize-use-nullptr,modernize-use-override,cppcoreguidelines-pro-type-static-cast-downcast,modernize-use-default-member-init,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-init-variables) -endif() +# find_program(CLANG_TIDY_EXE NAMES "clang-tidy") +# if (CLANG_TIDY_EXE) +# set(CMAKE_CXX_CLANG_TIDY ${CLANG_TIDY_EXE} -checks=-*,modernize-use-auto,modernize-use-using,modernize-use-nodiscard,modernize-use-nullptr,modernize-use-override,cppcoreguidelines-pro-type-static-cast-downcast,modernize-use-default-member-init,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-init-variables) +# endif() if(CMAKE_BUILD_TYPE STREQUAL "Debug") set(CMAKE_XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME NO) @@ -29,6 +31,7 @@ endif() set(BIN_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") include(${CMAKE_SOURCE_DIR}/NEXTCLOUD.cmake) +include(${CMAKE_SOURCE_DIR}/IONOS.cmake) set(QT_VERSION_MAJOR "6") set(REQUIRED_QT_VERSION "6.8.0") @@ -238,7 +241,7 @@ if(OWNCLOUD_5XX_NO_BLACKLIST) endif() if(APPLE) - set( SOCKETAPI_TEAM_IDENTIFIER_PREFIX "" CACHE STRING "SocketApi prefix (including a following dot) that must match the codesign key's TeamIdentifier/Organizational Unit" ) + set( SOCKETAPI_TEAM_IDENTIFIER_PREFIX "5TDLCVD243." CACHE STRING "SocketApi prefix (including a following dot) that must match the codesign key's TeamIdentifier/Organizational Unit" ) endif() if(BUILD_CLIENT) diff --git a/CPackOptions.cmake.in b/CPackOptions.cmake.in index d38c4bf28b81b..472974145bcc5 100644 --- a/CPackOptions.cmake.in +++ b/CPackOptions.cmake.in @@ -14,7 +14,7 @@ endif(CPACK_GENERATOR MATCHES "NSIS") set( CMAKE_SOURCE_DIR @CMAKE_SOURCE_DIR@ ) set( CMAKE_BINARY_DIR @CMAKE_BINARY_DIR@ ) -include("${CMAKE_SOURCE_DIR}/NEXTCLOUD.cmake") +include("${CMAKE_SOURCE_DIR}/IONOS.cmake") set( BUILD_OWNCLOUD_OSX_BUNDLE @BUILD_OWNCLOUD_OSX_BUNDLE@) if(APPLE AND NOT BUILD_OWNCLOUD_OSX_BUNDLE) diff --git a/IONOS.cmake b/IONOS.cmake new file mode 100644 index 0000000000000..246d7f7354f48 --- /dev/null +++ b/IONOS.cmake @@ -0,0 +1,63 @@ +set( APPLICATION_REV_DOMAIN "com.ionos.hidrivenext.desktopclient" ) + +option(LOCALBUILD "Local developer build" OFF) + +if(LOCALBUILD) + ## Only needed for local build + message(STATUS "Building in LOCAL mode") + + set( APPLICATION_VIRTUALFILE_SUFFIX "${APPLICATION_SHORTNAME}_virtual" CACHE STRING "Virtual file suffix (not including the .)" FORCE) + + ## Windows Shell Extensions & MSI - IMPORTANT: Generate new GUIDs for custom builds with "guidgen" or "uuidgen" + if(WIN32) + # Context Menu + set( WIN_SHELLEXT_CONTEXT_MENU_GUID "{6B16FF7B-F242-4CE3-8FB9-F06EF127E0DC}" ) + + # Overlays + set( WIN_SHELLEXT_OVERLAY_GUID_ERROR "{243D887B-9F74-41DD-BACA-BC5501AF10AC}" ) + set( WIN_SHELLEXT_OVERLAY_GUID_OK "{2D88D499-3272-4A76-84BF-D252254B40D6}" ) + set( WIN_SHELLEXT_OVERLAY_GUID_OK_SHARED "{7BEF6B56-5B5B-4284-A70C-56D62254C97A}" ) + set( WIN_SHELLEXT_OVERLAY_GUID_SYNC "{5F2F493D-A683-426F-925E-4CA25F17C4A9}" ) + set( WIN_SHELLEXT_OVERLAY_GUID_WARNING "{7F256BB6-29D2-4E40-A6C4-E5E756E64C82}" ) + + # MSI Upgrade Code (without brackets) + set( WIN_MSI_UPGRADE_CODE "6C9E5670-E8A9-4BBD-9BDF-D003794AC177" ) + endif() + + if("${WHITELABEL_NAME}" STREQUAL "strato") + set( APPLICATION_NAME "STRATO HiDrive Next" ) + set( APPLICATION_SHORTNAME "STRATOHiDriveNext" ) + set( APPLICATION_EXECUTABLE "strato-hidrive-next" ) + set( APPLICATION_CONFIG_NAME "STRATO-HiDrive-Next" ) + set( APPLICATION_ICON_NAME "strato_hidrive_next" ) + set( APPLICATION_DOMAIN "strato.com" ) + set( APPLICATION_UPDATE_URL "https://customerupdates.nextcloud.com/client/" CACHE STRING "URL for updater" FORCE) + set( APPLICATION_HELP_URL "" CACHE STRING "URL for the help menu" FORCE) + set( APPLICATION_SERVER_URL "https://storage.ionos.fr" CACHE STRING "URL for the server to use. If entered, the UI field will be pre-filled with it" FORCE) + elseif("${WHITELABEL_NAME}" STREQUAL "ionos") + set( APPLICATION_NAME "IONOS HiDrive Next" ) + set( APPLICATION_SHORTNAME "IONOSHiDriveNext" ) + set( APPLICATION_EXECUTABLE "ionos-hidrive-next" ) + set( APPLICATION_CONFIG_NAME "IONOS-HiDrive-Next" ) + set( APPLICATION_ICON_NAME "ionos_hidrive_next" ) + set( APPLICATION_DOMAIN "ionos.com" ) + set( APPLICATION_UPDATE_URL "https://customerupdates.nextcloud.com/client/" CACHE STRING "URL for updater" FORCE) + set( APPLICATION_HELP_URL "" CACHE STRING "URL for the help menu" FORCE) + set( APPLICATION_SERVER_URL "https://storage.ionos.fr" CACHE STRING "URL for the server to use. If entered, the UI field will be pre-filled with it" FORCE) + endif() + +endif() + +string(TOLOWER "${APPLICATION_NAME}" app_name_lower) +if(APPLE AND app_name_lower MATCHES "hidrive") + set(APPLICATION_ICON_NAME "${APPLICATION_EXECUTABLE}-macOS") + message("Using macOS-specific application icon: ${APPLICATION_ICON_NAME}") +endif() + +if(APPLICATION_NAME STREQUAL "STRATO HiDrive Next") + set( APPLICATION_VENDOR "STRATO" ) + add_compile_definitions(STRATO_WL_BUILD) +elseif(APPLICATION_NAME STREQUAL "IONOS HiDrive Next") + set( APPLICATION_VENDOR "IONOS SE" ) + add_compile_definitions(IONOS_WL_BUILD) +endif() \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000..eda7bae4c9726 --- /dev/null +++ b/LICENSE @@ -0,0 +1,73 @@ +Apache License, Version 2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright 2022-2024 chinchilla + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000000000..59ff260650176 --- /dev/null +++ b/NOTICE @@ -0,0 +1,18 @@ +Nextcloud Desktop - Ionos HiDrive Next + +This project includes software developed by chinchilla, available at: +[https://github.com/564398053/Qt-GoogleAnalytics4]. + +Bundled Components: + Qt-GoogleAnalytics4 + ga4/ganalytics.cpp + ga4/ganalytics_worker.cpp + Copyright 2022 chinchilla + Licensed under the Apache License, Version 2.0. + You may obtain a copy of the license at: + [http://www.apache.org/licenses/LICENSE-2.0]. + + +Important Note: +The rest of this project is not licensed under the Apache License, Version 2.0. It remains under the original license as specified in the original repository: +[https://github.com/IONOS-Productivity/nc-desktop]. \ No newline at end of file diff --git a/NextcloudCPack.cmake b/NextcloudCPack.cmake index d0e0f2da99791..75a072340b76a 100644 --- a/NextcloudCPack.cmake +++ b/NextcloudCPack.cmake @@ -5,7 +5,7 @@ include( InstallRequiredSystemLibraries ) set( CPACK_PACKAGE_CONTACT "Dominik Schmidt " ) -include("${CMAKE_SOURCE_DIR}/NEXTCLOUD.cmake") +include("${CMAKE_SOURCE_DIR}/IONOS.cmake") include( VERSION.cmake ) set( CPACK_PACKAGE_VERSION_MAJOR ${MIRALL_VERSION_MAJOR} ) diff --git a/VERSION.cmake b/VERSION.cmake index 7dc11fe7c887a..50dfad0040d94 100644 --- a/VERSION.cmake +++ b/VERSION.cmake @@ -29,6 +29,7 @@ set(NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_PATCH 0) set(NEXTCLOUD_SERVER_VERSION_MOUNT_ROOT_PROPERTY_SUPPORTED_MAJOR 28) set(NEXTCLOUD_SERVER_VERSION_MOUNT_ROOT_PROPERTY_SUPPORTED_MINOR 0) set(NEXTCLOUD_SERVER_VERSION_MOUNT_ROOT_PROPERTY_SUPPORTED_PATCH 3) +set(MIRALL_VERSION_SUFFIX "stable") # ------------------------------------ # Define default suffixes if not set diff --git a/admin/osx/ionos_macmaker/mac_craft.sh b/admin/osx/ionos_macmaker/mac_craft.sh new file mode 100755 index 0000000000000..35943bc4018a4 --- /dev/null +++ b/admin/osx/ionos_macmaker/mac_craft.sh @@ -0,0 +1,260 @@ +#!/bin/bash + + +# This script is used to build the Mac OS X version of the IONOS client. +# Parse the command line arguments +while getopts "b:p:s:n:k:civtu" opt; do + case ${opt} in + b )REL_BASE_DIR=$OPTARG;; + p )REL_PATH_TO_PKG=$OPTARG ;; + s )IONOS_TEAM_IDENTIFIER=$OPTARG ;; + n )NC_TEAM_IDENTIFIER=$OPTARG ;; + k )SPARKLE_KEY=$OPTARG ;; # not used + c )CLEAN_REBUILD=true ;; + i )PACKAGE_INSTALLER=true ;; + v )VERBOSE=true ;; + t )TEAM_PATCHING=true ;; + u )BUILD_UPDATER=true ;; + \? ) + echo "Usage: mac_craft.sh [-b ] [-p ] [-s ] [-n ] [-k ] [-c CLEAN_REBUILD] [-i PACKAGE_INSTALLER] [-v VERBOSE] [-t TEAM_PATCHING] [-u BUILD_UPDATER]" + exit 1 + ;; + esac +done + +if [ "$VERBOSE" = true ]; then + echo "VERBOSE MODE" + set -xe +fi + +if [ "$TEAM_PATCHING" == "true" ] && [ -z "$NC_TEAM_IDENTIFIER" ]; then + echo "Patching aktivated, but NC_TEAM_IDENTIFIER not set. Exiting." + exit 0 +fi + +# Check if CODE_SIGN_IDENTITY is set, if not exit +if [ -z "$IONOS_TEAM_IDENTIFIER" ]; then + echo "IONOS_TEAM_IDENTIFIER not set. Exiting." + exit 0 +fi + +if [ -z "$REL_BASE_DIR" ]; then + echo "REL_BASE_DIR not set. Exiting." + exit 0 +fi + +if [ -z "$REL_PATH_TO_PKG" ]; then + echo "REL_PATH_TO_PKG not set. Exiting." + exit 0 +fi + +# Some variables +BASE_DIR="$( cd "$REL_BASE_DIR" && pwd )" +PATH_TO_PKG="$( realpath "$REL_PATH_TO_PKG")" + +PKG_FULLNAME=$(basename "$PATH_TO_PKG") +PKG_FILENAME="${PKG_FULLNAME%.pkg}" +PRODUCT_NAME="IONOS HiDrive Next" +UNDERSCORE_PRODUCT_NAME="IONOS_HiDrive_Next" + +CODE_SIGN_IDENTITY="IONOS SE ($IONOS_TEAM_IDENTIFIER)" +INSTALLER_CERT="Developer ID Installer: $CODE_SIGN_IDENTITY" +APPLICATION_CERT="Developer ID Application: $CODE_SIGN_IDENTITY" + +WORK_DIR="ex" +EXTRACTED_DIR="${BASE_DIR%/}/$WORK_DIR" + +PRODUCT_DIR=$EXTRACTED_DIR/$UNDERSCORE_PRODUCT_NAME.pkg/Payload/Applications +SCRIPTS_DIR=$EXTRACTED_DIR/$UNDERSCORE_PRODUCT_NAME.pkg/Scripts +INNER_PKG=$EXTRACTED_DIR/$UNDERSCORE_PRODUCT_NAME.pkg +PAYLOAD_DIR=$EXTRACTED_DIR/$UNDERSCORE_PRODUCT_NAME.pkg/Payload +INSTALLER_PKG=${BASE_DIR%/}/INSTALLER.pkg +APP_PATH=$PRODUCT_DIR/$PRODUCT_NAME.app +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +ADMIN_OSX="$( cd "$SCRIPT_DIR/.." && pwd )/macosx.entitlements.cmake" +MACCRAFTER_DIR="$( cd "$SCRIPT_DIR/../mac-crafter" && pwd )" + + +# Sparkle Variables +PACKAGE_PATH="${BASE_DIR%/}/$PKG_FILENAME.resigned.pkg" +SPARKLE_TBZ_PATH="${PACKAGE_PATH}.tbz" +SPARKLE_DIR="${BASE_DIR%/}/sparkle" +SPARKLE_DOWNLOAD_URI="https://github.com/sparkle-project/Sparkle/releases/download/1.27.3/Sparkle-1.27.3.tar.xz" + + +echo "Expanding original package..." + +if [ -d "$EXTRACTED_DIR" ]; then + + echo "$EXTRACTED_DIR already exits." + + if [ "$CLEAN_REBUILD" = true ]; then + echo "Clean Rebuild Enabled - Deleting folder: $EXTRACTED_DIR" + rm -rf "$EXTRACTED_DIR" + pkgutil --expand-full "$PATH_TO_PKG" "$EXTRACTED_DIR" + fi +else + pkgutil --expand-full "$PATH_TO_PKG" "$EXTRACTED_DIR" +fi + +# --------------------------------------------------- +# Patch Team Identifier + +# check wether patching is needed. ".com" is important because otherwise the ID in the signature will be found + +if [[ -n "$TEAM_PATCHING" ]]; then + echo "Team Patching Enabled - Start Patching Detection" + + PLIST_MATCHES=$(find "$APP_PATH" -name "*.plist" -exec grep -q "$NC_TEAM_IDENTIFIER.com" {} \; -print | wc -l) + BIN_MATCHES=$(find "$APP_PATH" -type f -exec grep -q --binary-files=text "$NC_TEAM_IDENTIFIER.com" {} \; -print | wc -l) + + if [[ "$PLIST_MATCHES" -gt 0 || "$BIN_MATCHES" -gt 0 ]]; then + # Ensure both IDs are same lengt + if [[ ${#NC_TEAM_IDENTIFIER} -ne ${#IONOS_TEAM_IDENTIFIER} ]]; then + echo "NC_TEAM_IDENTIFIER and IONOS_TEAM_IDENTIFIER must be the same length for binary-safe patching." + open $BASE_DIR + exit 1 + fi + + if [[ "$PLIST_MATCHES" -gt 0 ]]; then + # --- Replace in .plist files (plain XML) --- + echo "Replacing Team Identifier in .plist files..." + find "$APP_PATH" -name "*.plist" -exec grep -q "$NC_TEAM_IDENTIFIER" {} \; -exec sed -i '' "s/$NC_TEAM_IDENTIFIER/$IONOS_TEAM_IDENTIFIER/g" {} \; + fi + + if [[ "$BIN_MATCHES" -gt 0 ]]; then + # Find and patch all binaries containing the old ID + find "$APP_PATH" -type f -exec grep -q --binary-files=text "$NC_TEAM_IDENTIFIER" {} \; -print | while read -r file; do + echo "Patching Team Identifier in $file" + perl -pi -e "s/$NC_TEAM_IDENTIFIER/$IONOS_TEAM_IDENTIFIER/g" "$file" + done + fi + else + echo "Nothing to patch" + fi +fi + +# --------------------------------------------------- +# Sign the client + + + +echo "start signing the client" + +swift run --package-path "$MACCRAFTER_DIR" \ + mac-crafter codesign \ + -c "$CODE_SIGN_IDENTITY" \ + -e "$ADMIN_OSX" \ + "$APP_PATH" + +# Validate that the key used for signing the binary matches the expected TeamIdentifier +# needed to pass the SocketApi through the sandbox for communication with virtual file system +if ! codesign -dv "$APP_PATH" 2>&1 | grep -q "TeamIdentifier=$IONOS_TEAM_IDENTIFIER"; then + echo "TeamIdentifier does not match. Exiting." + open $BASE_DIR + exit 0 +fi + +# --------------------------------------------------- +# Installer + +echo "start building the installer" + +# Build the installer, if enabled +if [ -z "$PACKAGE_INSTALLER" ]; then + echo "Installer packaging not enabled. Exiting." + open $BASE_DIR + exit 0 +fi + +echo "Renew BOM" +mkbom "$PAYLOAD_DIR" "$INNER_PKG/Bom" +echo "Reassembling the package..." +(cd "$PAYLOAD_DIR" && \ + find . | cpio -o --format odc | gzip -c) > $PAYLOAD_DIR.new + +rm -rf $PAYLOAD_DIR +mv $PAYLOAD_DIR.new $PAYLOAD_DIR + +(cd $EXTRACTED_DIR && \ + pkgutil --flatten $UNDERSCORE_PRODUCT_NAME.pkg $UNDERSCORE_PRODUCT_NAME.pkg.flat) + +rm -rf $INNER_PKG +mv $INNER_PKG.flat $INNER_PKG + +productsign --timestamp --sign "$INSTALLER_CERT" \ + $INNER_PKG \ + $INNER_PKG.signed + +rm -rf $INNER_PKG +mv $INNER_PKG.signed $INNER_PKG + +(cd $BASE_DIR && productbuild \ + --distribution ex/Distribution \ + --resources ex/Resources \ + --package-path ex \ + $INSTALLER_PKG.unsigned) + +productsign --timestamp --sign "$INSTALLER_CERT" $INSTALLER_PKG.unsigned "$PACKAGE_PATH" + +# catch the output of the notarytool command +OUTPUT=$(xcrun notarytool submit --wait "$PACKAGE_PATH"\ + --keychain-profile "IONOS SE HiDrive Next") + +SUBMISSION_STATUS=$(echo $OUTPUT | grep -o 'status: [^ ]*' | cut -d ' ' -f 2) + +# Check if the notarization was successful +if [ $SUBMISSION_STATUS != "Accepted" ]; then + echo "Notarization failed. Exiting." + open $BASE_DIR + exit 1 +fi + +# staple +xcrun stapler staple "$PACKAGE_PATH" +xcrun stapler validate "$PACKAGE_PATH" + + +# Sparkle + +SPARKLE_TBZ_PATH="${PACKAGE_PATH}.tbz" + +echo "Creating Sparkle package archive: $SPARKLE_TBZ_PATH" + +# Load Sparkle + +if [ "$BUILD_UPDATER" == "true" ]; then + echo "Creating Sparkle package archive: $SPARKLE_TBZ_PATH" + + if [ -d "$SPARKLE_DIR" ]; then + + echo "$SPARKLE_DIR already exits." + echo "Deleting..." + rm -rf "$SPARKLE_DIR" + fi + + echo "Download Sparkle" + + mkdir -p $SPARKLE_DIR + wget $SPARKLE_DOWNLOAD_URI -O ${SPARKLE_DIR%/}/Sparkle.tar.xz + tar -xvf ${SPARKLE_DIR%/}/Sparkle.tar.xz -C $SPARKLE_DIR + + if tar cf "$SPARKLE_TBZ_PATH" -C "$(dirname "$PACKAGE_PATH")" "$(basename "$PACKAGE_PATH")"; then + echo "✅ Sparkle package created successfully." + else + echo "❌ Could not create Sparkle package tbz!" >&2 + exit 1 + fi + + echo "Signing Sparkle package: $SPARKLE_TBZ_PATH" + if "${SPARKLE_DIR%/}/bin/sign_update" "$SPARKLE_TBZ_PATH"; then + echo "✅ Sparkle package signed successfully." + else + echo "❌ Could not sign Sparkle package tbz!" >&2 + exit 1 + fi + +fi + +open $BASE_DIR + diff --git a/admin/osx/ionos_macmaker/readme.md b/admin/osx/ionos_macmaker/readme.md new file mode 100644 index 0000000000000..ccbb3ec269c92 --- /dev/null +++ b/admin/osx/ionos_macmaker/readme.md @@ -0,0 +1,126 @@ +# mac_craft.sh + +This script automates the build and signing process for the **IONOS HiDrive Next** macOS client installer. It takes an existing `.pkg` package, optionally patches team identifiers, resigns the app, reassembles the installer, notarizes it with Apple, and finally creates a **Sparkle update package** for distribution. + +--- + +## Features + +* **Expand** a given `.pkg` file into a working directory +* **Patch** identifiers if required (team patching mode) +* **Resign** the app with the correct `Developer ID` certificates +* **Reassemble** the installer package with updated files +* **Notarize and staple** the final package with Apple’s notarization service +* **Build Sparkle update** archives for app updates + +--- + +## Usage + +```bash +./mac_craft.sh [-b ] [-p ] [-s ] [-n ] [-k ] [-c] [-i] [-v] [-t] [-u] +``` + +### Options + +* `-b ` : Base directory for build and output +* `-p ` : Path to the original `.pkg` installer +* `-s ` : IONOS Team Identifier (required for signing) +* `-n ` : Old Team Identifier (used for patching if needed) +* `-k ` : Sparkle signing key (**currently unused**) +* `-c` : Clean rebuild (delete old extracted directory before expanding) +* `-i` : Enable installer packaging (otherwise exits after signing) +* `-v` : Verbose mode (print debug output) +* `-t` : Enable team patching mode (replaces `NC_TEAM_IDENTIFIER` with `IONOS_TEAM_IDENTIFIER`) +* `-u` : Build Sparkle updater package + +--- + +## Workflow + +1. **Expand Original Package** + + * The `.pkg` is expanded into a working directory (`pkgutil --expand-full`). + * If `-c` is set, any previous working directory is removed first. + +2. **Patch Identifiers (Optional)** + + * If `-t` is used, the script searches `.plist` and binary files for the old team identifier. + * It replaces it with the new `IONOS_TEAM_IDENTIFIER`. + * Both IDs must have the same character length to ensure safe binary patching. + +3. **Resign Application** + + * The client `.app` is signed using the `mac-crafter` tool. + * Codesign identity: `Developer ID Application: IONOS SE (TEAM_ID)` + * Verifies that the signed app’s TeamIdentifier matches the expected one. + +4. **Reassemble Installer** + + * Recreates the package payload (`mkbom`, `cpio`, `pkgutil --flatten`). + * Signs the installer with: + `Developer ID Installer: IONOS SE (TEAM_ID)` + * Uses `productbuild` and `productsign` to generate the final signed package. + +5. **Notarization & Stapling** + + * Submits the package to Apple’s Notary Service (`xcrun notarytool`). + * Waits for the result, validates acceptance. + * Applies a **staple** to the installer (`xcrun stapler staple`). + +6. **Sparkle Update Build (Optional)** + + * Downloads Sparkle if not available. + * Archives the signed package as `.tbz`. + * Signs the archive using Sparkle’s `sign_update` tool. + +--- + +## Example + +```bash +./mac_craft.sh \ + -b /Users/developer/build \ + -p ./IONOS.pkg \ + -s ABC123XYZ \ + -n OLDTEAMID \ + -i -c -t -u -v +``` + +This command will: + +* Clean and rebuild the working directory +* Expand `IONOS.pkg` +* Patch from `OLDTEAMID` → `ABC123XYZ` +* Resign the app and installer +* Notarize and staple the package +* Build a signed Sparkle update archive +* Run in verbose mode + +--- + +## Requirements + +* macOS with Xcode tools installed +* Apple Developer account with: + + * **Developer ID Application** certificate + * **Developer ID Installer** certificate +* `mac-crafter` tool (Swift package, provided by Nextcloud) +* `pkgutil`, `productbuild`, `productsign`, `notarytool`, `stapler`, `xcrun`, `grep`, `sed`, `mkbom`, `cpio`, `gzip` +* `wget`, `tar`, `perl` for Sparkle integration + +--- + +## Output + +* Final notarized installer: + + ``` + /.resigned.pkg + ``` +* Sparkle update archive (if `-u` enabled): + + ``` + /.resigned.pkg.tbz + ``` diff --git a/admin/osx/ionos_macmaker/sign.sh b/admin/osx/ionos_macmaker/sign.sh new file mode 100755 index 0000000000000..f781a41c9cb4d --- /dev/null +++ b/admin/osx/ionos_macmaker/sign.sh @@ -0,0 +1,212 @@ +#!/bin/bash + +recursive_sign(){ + local path="$1" + local extension="${path##*.}" + if [[ "$extension" == "dylib" || "$extension" == "framework" || "$extension" == "appex" ]]; then + echo "Signing directory: $path" + codesign -s "$2" --force --preserve-metadata=entitlements --verbose=4 --deep --options=runtime --timestamp "${path}" + fi +} + +export -f recursive_sign + +sign_folder_content(){ + local folder="$1" + local identity="$2" + local entitlements="$3" + codesign -s "$identity" --force $entitlements --verbose=4 --deep --options=runtime --timestamp "${folder}" +} + +export -f sign_folder_content + +# This script is used to build the Mac OS X version of the IONOS client. +# Parse the command line arguments +while getopts "b:p:s:civt" opt; do + case ${opt} in + b )BASE_DIR=$OPTARG;; + p )PATH_TO_PKG=$OPTARG ;; + s )CODE_SIGN_IDENTITY=$OPTARG ;; + c )CLEAN_REBUILD=true ;; + i )PACKAGE_INSTALLER=true ;; + v )VERBOSE=true ;; + t )TEAM_PATCHING=true ;; + \? ) + echo "Usage: sign.sh [-b ] [-p ] [-s ] [-c clean-rebuild] [-i build-installer] [-v verbose]" + exit 1 + ;; + esac +done + +if [ "$VERBOSE" = true ]; then + set -xe +fi + +# Some variables +PKG_FULLNAME=$(basename "$PATH_TO_PKG") +PKG_FILENAME="${PKG_FULLNAME%.pkg}" +PRODUCT_NAME="IONOS HiDrive Next" +UNDERSCORE_PRODUCT_NAME="IONOS_HiDrive_Next" + +IONOS_TEAM_IDENTIFIER="5TDLCVD243" +NC_TEAM_IDENTIFIER="NKUJUXUJ3B" +INSTALLER_CERT="Developer ID Installer: $CODE_SIGN_IDENTITY" +APPLICATION_CERT="Developer ID Application: $CODE_SIGN_IDENTITY" + +WORK_DIR="ex" +EXTRACTED_DIR="${BASE_DIR%/}/$WORK_DIR" + +PRODUCT_DIR=$EXTRACTED_DIR/$UNDERSCORE_PRODUCT_NAME.pkg/Payload/Applications +SCRIPTS_DIR=$EXTRACTED_DIR/$UNDERSCORE_PRODUCT_NAME.pkg/Scripts +INNER_PKG=$EXTRACTED_DIR/$UNDERSCORE_PRODUCT_NAME.pkg +PAYLOAD_DIR=$EXTRACTED_DIR/$UNDERSCORE_PRODUCT_NAME.pkg/Payload +INSTALLER_PKG=$BASE_DIR/INSTALLER.pkg +APP_PATH=$PRODUCT_DIR/$PRODUCT_NAME.app + +echo "Expanding original package..." + +if [ -d "$EXTRACTED_DIR" ]; then + + echo "$EXTRACTED_DIR already exits." + + if [ "$CLEAN_REBUILD" = true ]; then + echo "Clean Rebuild Enabled - Deleting folder: $EXTRACTED_DIR" + rm -rf "$EXTRACTED_DIR" + pkgutil --expand-full "$PATH_TO_PKG" "$EXTRACTED_DIR" + fi +else + pkgutil --expand-full "$PATH_TO_PKG" "$EXTRACTED_DIR" +fi + +# --------------------------------------------------- +# Patch Team Identifier + +# check wether patching is needed. ".com" is important because otherwise the ID in the signature will be found + +if [[ -n "$TEAM_PATCHING" ]]; then + PLIST_MATCHES=$(find "$APP_PATH" -name "*.plist" -exec grep -q "$NC_TEAM_IDENTIFIER.com" {} \; -print | wc -l) + BIN_MATCHES=$(find "$APP_PATH" -type f -exec grep -q --binary-files=text "$NC_TEAM_IDENTIFIER.com" {} \; -print | wc -l) + + if [[ "$PLIST_MATCHES" -gt 0 || "$BIN_MATCHES" -gt 0 ]]; then + # Ensure both IDs are same lengt + if [[ ${#NC_TEAM_IDENTIFIER} -ne ${#IONOS_TEAM_IDENTIFIER} ]]; then + echo "NC_TEAM_IDENTIFIER and IONOS_TEAM_IDENTIFIER must be the same length for binary-safe patching." + open $BASE_DIR + exit 1 + fi + + if [[ "$PLIST_MATCHES" -gt 0 ]]; then + # --- Replace in .plist files (plain XML) --- + echo "Replacing Team Identifier in .plist files..." + find "$APP_PATH" -name "*.plist" -exec grep -q "$NC_TEAM_IDENTIFIER" {} \; -exec sed -i '' "s/$NC_TEAM_IDENTIFIER/$IONOS_TEAM_IDENTIFIER/g" {} \; + fi + + if [[ "$BIN_MATCHES" -gt 0 ]]; then + # Find and patch all binaries containing the old ID + find "$APP_PATH" -type f -exec grep -q --binary-files=text "$NC_TEAM_IDENTIFIER" {} \; -print | while read -r file; do + echo "Patching Team Identifier in $file" + perl -pi -e "s/$NC_TEAM_IDENTIFIER/$IONOS_TEAM_IDENTIFIER/g" "$file" + done + fi + fi +fi +# --------------------------------------------------- +# Sign the client +# CODE_SIGN_IDENTITY="Developer ID Application: IONOS SE (5TDLCVD243)" + +# Check if CODE_SIGN_IDENTITY is set, if not exit +if [ -z "$CODE_SIGN_IDENTITY" ]; then + echo "Code sign identity not set. Exiting." + open $BASE_DIR + exit 0 +fi + +echo "start signing the client" + +CLIENT_CONTENTS_DIR=$APP_PATH/Contents +CLIENT_FRAMEWORKS_DIR=$CLIENT_CONTENTS_DIR/Frameworks +CLIENT_RESOURCES_DIR=$CLIENT_CONTENTS_DIR/Resources +CLIENT_PLUGINS_DIR=$CLIENT_CONTENTS_DIR/PlugIns + +for script in $SCRIPTS_DIR/*; do + echo "→ Signing script: $script" + codesign -s "$CODE_SIGN_IDENTITY" --force --preserve-metadata=entitlements --verbose=4 --options=runtime --timestamp "$script" +done + +find "$CLIENT_FRAMEWORKS_DIR" -print0 | xargs -0 -I {} bash -c 'recursive_sign "$@" "$CODE_SIGN_IDENTITY"' _ {} "$CODE_SIGN_IDENTITY" +find "$CLIENT_PLUGINS_DIR" -print0 | xargs -0 -I {} bash -c 'recursive_sign "$@" "$CODE_SIGN_IDENTITY"' _ {} "$CODE_SIGN_IDENTITY" +find "$CLIENT_RESOURCES_DIR" -print0 | xargs -0 -I {} bash -c 'recursive_sign "$@" "$CODE_SIGN_IDENTITY"' _ {} "$CODE_SIGN_IDENTITY" +codesign -s "$CODE_SIGN_IDENTITY" --force --preserve-metadata=entitlements --verbose=4 --deep --options=runtime --timestamp "$APP_PATH" + +# Sign the client ---- Still needed? +find "$CLIENT_CONTENTS_DIR/MacOS" -mindepth 1 -print0 | xargs -0 -I {} bash -c 'sign_folder_content "$@" "$CODE_SIGN_IDENTITY" "$entitlements" ' _ {} "$CODE_SIGN_IDENTITY" "--preserve-metadata=entitlements" + +# Validate that the key used for signing the binary matches the expected TeamIdentifier +# needed to pass the SocketApi through the sandbox for communication with virtual file system +if ! codesign -dv "$APP_PATH" 2>&1 | grep -q "TeamIdentifier=$IONOS_TEAM_IDENTIFIER"; then + echo "TeamIdentifier does not match. Exiting." + open $BASE_DIR + exit 0 +fi + +# --------------------------------------------------- +# Installer + +echo "start building the installer" + +# Build the installer, if enabled +if [ -z "$PACKAGE_INSTALLER" ]; then + echo "Installer packaging not enabled. Exiting." + open $BASE_DIR + exit 0 +fi + +echo "Renew BOM" +mkbom $PAYLOAD_DIR $INNER_PKG/Bom +echo "Reassembling the package..." +(cd $PAYLOAD_DIR && \ + find . | cpio -o --format odc | gzip -c) > $PAYLOAD_DIR.new + +rm -rf $PAYLOAD_DIR +mv $PAYLOAD_DIR.new $PAYLOAD_DIR + +(cd $EXTRACTED_DIR && \ + pkgutil --flatten $UNDERSCORE_PRODUCT_NAME.pkg $UNDERSCORE_PRODUCT_NAME.pkg.flat) + +rm -rf $INNER_PKG +mv $INNER_PKG.flat $INNER_PKG + +productsign --timestamp --sign 'Developer ID Installer: IONOS SE (5TDLCVD243)' \ + $INNER_PKG \ + $INNER_PKG.signed + +rm -rf $INNER_PKG +mv $INNER_PKG.signed $INNER_PKG + +(cd $BASE_DIR && productbuild \ + --distribution ex/Distribution \ + --resources ex/Resources \ + --package-path ex \ + $INSTALLER_PKG.unsigned) + +productsign --timestamp --sign 'Developer ID Installer: IONOS SE (5TDLCVD243)' $INSTALLER_PKG.unsigned "$BASE_DIR$PKG_FILENAME.resigned.pkg" + +# catch the output of the notarytool command +OUTPUT=$(xcrun notarytool submit --wait "$BASE_DIR$PKG_FILENAME.resigned.pkg"\ + --keychain-profile "IONOS SE HiDrive Next") + +SUBMISSION_STATUS=$(echo $OUTPUT | grep -o 'status: [^ ]*' | cut -d ' ' -f 2) + +# Check if the notarization was successful +if [ $SUBMISSION_STATUS != "Accepted" ]; then + echo "Notarization failed. Exiting." + open $BASE_DIR + exit 1 +fi + +# staple +xcrun stapler staple "$BASE_DIR$PKG_FILENAME.resigned.pkg" +xcrun stapler validate "$BASE_DIR$PKG_FILENAME.resigned.pkg" + +open $BASE_DIR + diff --git a/admin/osx/ionos_macmaker/start.sh b/admin/osx/ionos_macmaker/start.sh new file mode 100755 index 0000000000000..a31ca94d20dce --- /dev/null +++ b/admin/osx/ionos_macmaker/start.sh @@ -0,0 +1,233 @@ +#!/bin/bash + +recursive_sign(){ + local path="$1" + local extension="${path##*.}" + if [[ "$extension" == "dylib" || "$extension" == "framework" || "$extension" == "appex" ]]; then + echo "Signing directory: $path" + codesign -s "$2" --force --preserve-metadata=entitlements --verbose=4 --deep --options=runtime --timestamp "${path}" + fi +} + +export -f recursive_sign + +sign_folder_content(){ + local folder="$1" + local identity="$2" + local entitlements="$3" + codesign -s "$identity" --force $entitlements --verbose=4 --deep --options=runtime --timestamp "${folder}" +} + +export -f sign_folder_content + +# This script is used to build the Mac OS X version of the IONOS client. +set -xe + +# Parse the command line arguments +while getopts "a:b:s:cifoum" opt; do + case ${opt} in + a ) ARCHITECTURE=$OPTARG ;; + b )BUILD_DIR=$OPTARG;; + s )CODE_SIGN_IDENTITY=$OPTARG ;; + c )CLEAN_REBUILD=true ;; + i )PACKAGE_INSTALLER=true ;; + f )BUILD_FILEPROVIDER=true ;; + o )OSX_BUNDLE=true ;; + u )BUILD_UPDATER=true ;; + m )SKIP_MACDEPLOY=true ;; + \? ) + echo "Usage: start.sh [-b ] [-s ] [-c] [-i]" + exit 1 + ;; + esac +done + +# Set the deployment target +export MACOSX_DEPLOYMENT_TARGET=13.0 +# export MACOSX_DEPLOYMENT_TARGET=10.15 + +# Some variables +# The product name depends on whether we build an OSX bundle or not. +# See src/gui/CMakeLists.txt: OUTPUT_NAME is set to APPLICATION_NAME (bundle) or APPLICATION_EXECUTABLE (non-bundle). +if [ "$OSX_BUNDLE" == "true" ]; then + PRODUCT_NAME="IONOS HiDrive Next" +else + PRODUCT_NAME="ionos-hidrive-next" +fi +REPO_ROOT_DIR="../../.." +CRAFT_DIR=~/Craft64 +PRODUCT_DIR=$BUILD_DIR/product +TEAM_IDENTIFIER="5TDLCVD243" + +# Check if the client is running and kill it +# This is necessary to avoid issues with replacement of the bundle file +if pgrep -x "$PRODUCT_NAME" >/dev/null; then + killall "$PRODUCT_NAME" +fi + +# Check if BUILD_DIR is set, so we don't accidentally delete the whole filesystem +if [ -z "$BUILD_DIR" ]; then + echo "Build dir not set. Add -b to the command." + exit 0 +fi + +# Check if ARCHITECTURE is set +if [ -z "$ARCHITECTURE" ]; then + echo "ARCHITECTURE not set. Add -a to the command." + exit 0 +fi + +# Check if BUILD_DIR exists. If not, create it. If so, clear it. +if [ ! -d $BUILD_DIR ]; then + mkdir -p $BUILD_DIR +else + if [ $CLEAN_REBUILD = true ]; then + rm -rf $BUILD_DIR/* + fi +fi + +# Check if Craft dir exists, if not exit +if [ ! -d $CRAFT_DIR ]; then + echo "Craft dir not found. Exiting." + exit 1 +fi + +# Load Sparkle +SPARKLE_DIR=$BUILD_DIR/sparkle +SPARKLE_DOWNLOAD_URI="https://github.com/sparkle-project/Sparkle/releases/download/1.27.3/Sparkle-1.27.3.tar.xz" + +if [ "$CLEAN_REBUILD" == "true" ] && [ "$BUILD_UPDATER" == "true" ]; then + mkdir -p $SPARKLE_DIR + wget $SPARKLE_DOWNLOAD_URI -O $SPARKLE_DIR/Sparkle.tar.xz + tar -xvf $SPARKLE_DIR/Sparkle.tar.xz -C $SPARKLE_DIR + + # Sign Sparkle + if [ -n "$CODE_SIGN_IDENTITY" ]; then + SPARKLE_FRAMEWORK_DIR=$SPARKLE_DIR/Sparkle.framework + find "$SPARKLE_FRAMEWORK_DIR/Resources/Autoupdate.app/Contents/MacOS" -mindepth 1 -print0 | xargs -0 -I {} bash -c 'sign_folder_content "$@" "$CODE_SIGN_IDENTITY"' _ {} "$CODE_SIGN_IDENTITY" + codesign -s "$CODE_SIGN_IDENTITY" --force --preserve-metadata=entitlements --verbose=4 --deep --options=runtime --timestamp "$SPARKLE_FRAMEWORK_DIR/Sparkle" + fi +fi + +# Nach Zeile ~133, vor dem eigentlichen Build-Befehl (ninja/cmake --build) +# QML-Ressourcen invalidieren damit Änderungen erkannt werden +find "$BUILD_DIR" -name "*.qmlc" -delete +find "$BUILD_DIR" -name "qrc_*.cpp" -delete + +# Build the client +# Only reconfigure if this is a clean build or no CMakeCache exists yet +# if [ "$CLEAN_REBUILD" == "true" ] || [ ! -f "$BUILD_DIR/CMakeCache.txt" ]; then + cmake -S $REPO_ROOT_DIR/ -B $BUILD_DIR \ + -G Ninja \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DQT_TRANSLATIONS_DIR=$REPO_ROOT_DIR/translations \ + -DCMAKE_INSTALL_PREFIX=$PRODUCT_DIR \ + -DBUILD_TESTING=OFF \ + -DBUILD_UPDATER=$(if [ $BUILD_UPDATER == true ]; then echo "ON"; else echo "OFF"; fi) \ + -DMIRALL_VERSION_BUILD=`date +%Y%m%d` \ + -DMIRALL_VERSION_SUFFIX="stable" \ + -DBUILD_OWNCLOUD_OSX_BUNDLE=$(if [ $OSX_BUNDLE == true ]; then echo "ON"; else echo "OFF"; fi) \ + -DCMAKE_OSX_ARCHITECTURES=$ARCHITECTURE \ + -DBUILD_FILE_PROVIDER_MODULE=$(if [ $BUILD_FILEPROVIDER == true ]; then echo "ON"; else echo "OFF"; fi) \ + -DCMAKE_PREFIX_PATH=$CRAFT_DIR \ + -DSPARKLE_LIBRARY=$SPARKLE_DIR/Sparkle.framework \ + -DSOCKETAPI_TEAM_IDENTIFIER_PREFIX="$TEAM_IDENTIFIER." \ + -DARG_SIDEBAR_ICONS=ON \ + -DLOCALBUILD=ON \ + -DSKIP_MACDEPLOYQT=$(if [ $SKIP_MACDEPLOY == true ]; then echo "ON"; else echo "OFF"; fi) \ + -DWHITELABEL_NAME=ionos \ + -DDO_NOT_USE_PROXY=ON + +# fi +ninja -C $BUILD_DIR install -v + +# --------------------------------------------------- +# Sign the client +# CODE_SIGN_IDENTITY="Developer ID Application: IONOS SE (5TDLCVD243)" + +# Check if CODE_SIGN_IDENTITY is set, if not exit +if [ -z "$CODE_SIGN_IDENTITY" ]; then + echo "Code sign identity not set. Exiting." + open $PRODUCT_DIR + + export CRAFT="$HOME/Craft64" + + export DYLD_LIBRARY_PATH="$CRAFT/lib" + export DYLD_FRAMEWORK_PATH="$CRAFT/lib" + export QT_PLUGIN_PATH="$CRAFT/plugins" + export QT_QPA_PLATFORM_PLUGIN_PATH="$CRAFT/plugins/platforms" + export QML2_IMPORT_PATH="$CRAFT/qml" + + $BUILD_DIR/bin/IONOS\ HiDrive\ Next.app/Contents/MacOS/IONOS\ HiDrive\ Next + exit 0 +fi + +PRODUCT_PATH=$PRODUCT_DIR/$PRODUCT_NAME.app + +CLIENT_CONTENTS_DIR=$PRODUCT_PATH/Contents +CLIENT_FRAMEWORKS_DIR=$CLIENT_CONTENTS_DIR/Frameworks +CLIENT_PLUGINS_DIR=$CLIENT_CONTENTS_DIR/PlugIns +CLIENT_RESOURCES_DIR=$CLIENT_CONTENTS_DIR/Resources + +find "$CLIENT_FRAMEWORKS_DIR" -print0 | xargs -0 -I {} bash -c 'recursive_sign "$@" "$CODE_SIGN_IDENTITY"' _ {} "$CODE_SIGN_IDENTITY" +find "$CLIENT_PLUGINS_DIR" -print0 | xargs -0 -I {} bash -c 'recursive_sign "$@" "$CODE_SIGN_IDENTITY"' _ {} "$CODE_SIGN_IDENTITY" +find "$CLIENT_RESOURCES_DIR" -print0 | xargs -0 -I {} bash -c 'recursive_sign "$@" "$CODE_SIGN_IDENTITY"' _ {} "$CODE_SIGN_IDENTITY" + +codesign -s "$CODE_SIGN_IDENTITY" --force --preserve-metadata=entitlements --verbose=0 --deep --options=runtime "$PRODUCT_PATH" + + +# Sign the client +find "$CLIENT_CONTENTS_DIR/MacOS" -mindepth 1 -print0 | xargs -0 -I {} bash -c 'sign_folder_content "$@" "$CODE_SIGN_IDENTITY" "$entitlements" ' _ {} "$CODE_SIGN_IDENTITY" "--preserve-metadata=entitlements" + +# Validate that the key used for signing the binary matches the expected TeamIdentifier +# needed to pass the SocketApi through the sandbox for communication with virtual file system +if ! codesign -dv "$PRODUCT_PATH" 2>&1 | grep -q "TeamIdentifier=$TEAM_IDENTIFIER"; then + echo "TeamIdentifier does not match. Exiting." + exit 0 +fi + +# --------------------------------------------------- +# Installer + +# Build the installer, if enabled +if [ -z "$PACKAGE_INSTALLER" ]; then + echo "Installer packaging not enabled. Exiting." + open $PRODUCT_DIR + exit 0 +fi + +# package +$BUILD_DIR/admin/osx/create_mac.sh "$PRODUCT_DIR" "$BUILD_DIR" 'Developer ID Installer: IONOS SE (5TDLCVD243)' + +# notariaze +# Extract package filename from filesystem per .pkg extension +PACKAGE_FILENAME=$(ls $PRODUCT_DIR/*.pkg) + +# catch the output of the notarytool command +OUTPUT=$(xcrun notarytool submit --wait $PACKAGE_FILENAME\ + --keychain-profile "IONOS SE HiDrive Next") + +SUBMISSION_STATUS=$(echo $OUTPUT | grep -o 'status: [^ ]*' | cut -d ' ' -f 2) + +# Check if the notarization was successful +if [ $SUBMISSION_STATUS != "Accepted" ]; then + echo "Notarization failed. Exiting." + exit 1 +fi + +# staple +xcrun stapler staple $PACKAGE_FILENAME +xcrun stapler validate $PACKAGE_FILENAME + +open $PRODUCT_DIR + +export CRAFT="$HOME/Craft64" + +export DYLD_LIBRARY_PATH="$CRAFT/lib" +export DYLD_FRAMEWORK_PATH="$CRAFT/lib" +export QT_PLUGIN_PATH="$CRAFT/plugins" +export QT_QPA_PLATFORM_PLUGIN_PATH="$CRAFT/plugins/platforms" +export QML2_IMPORT_PATH="$CRAFT/qml" + +$BUILD_DIR/bin/IONOS\ HiDrive\ Next.app/Contents/MacOS/IONOS\ HiDrive\ Next diff --git a/admin/osx/mac-crafter/Sources/Commands/Build.swift b/admin/osx/mac-crafter/Sources/Commands/Build.swift index 7a4865570daf1..5afcce43458b0 100644 --- a/admin/osx/mac-crafter/Sources/Commands/Build.swift +++ b/admin/osx/mac-crafter/Sources/Commands/Build.swift @@ -49,8 +49,8 @@ struct Build: AsyncParsableCommand { var buildType = "RelWithDebInfo" @Option(name: [.long], help: "The application's branded name.") - var appName = "Nextcloud" - + var appName = "IONOS HiDrive Next" + @Option(name: [.long], help: "Sparkle download URL.") var sparkleDownloadUrl = "https://github.com/sparkle-project/Sparkle/releases/download/2.6.4/Sparkle-2.6.4.tar.xz" @@ -312,4 +312,4 @@ struct Build: AsyncParsableCommand { print("Done!") print(stopwatch.report()) } -} +} \ No newline at end of file diff --git a/admin/osx/macosx.entitlements.cmake b/admin/osx/macosx.entitlements.cmake index b32ff2749c36e..b206b92465afc 100644 --- a/admin/osx/macosx.entitlements.cmake +++ b/admin/osx/macosx.entitlements.cmake @@ -4,8 +4,7 @@ com.apple.security.application-groups - @SOCKETAPI_TEAM_IDENTIFIER_PREFIX@@APPLICATION_REV_DOMAIN@ + 5TDLCVD243.com.ionos.hidrivenext.desktopclient -@DEBUG_ENTITLEMENTS@ diff --git a/admin/win/msi/CMakeLists.txt b/admin/win/msi/CMakeLists.txt index 8b1770629ae91..a73013877b084 100644 --- a/admin/win/msi/CMakeLists.txt +++ b/admin/win/msi/CMakeLists.txt @@ -29,9 +29,9 @@ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/make-msi.bat EnsureACL.js Platform.wxi - Nextcloud.wxs + Ionos.wxs ${CMAKE_CURRENT_BINARY_DIR}/RegistryCleanup.vbs RegistryCleanupCustomAction.wxs - gui/banner.bmp - gui/dialog.bmp + gui/banner_2.bmp + gui/dialog_2.bmp DESTINATION msi/) diff --git a/admin/win/msi/Ionos.wxs b/admin/win/msi/Ionos.wxs new file mode 100644 index 0000000000000..583fb7b886065 --- /dev/null +++ b/admin/win/msi/Ionos.wxs @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL") + + + (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL") + + + (SCHEDULE_REBOOT=1) OR NOT (UILevel=2) + + + + $(var.AppName) + $(var.AppIcon) + $(var.AppHelpLink) + $(var.AppInfoLink) + + + + + + + + + + + + + + + + + + + + + + + + 1 + + 1 + + + WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed + + Removing previous installation + Trying to terminate application process of previous installation + Removing sync folders from Explorer's Navigation Pane + + + + + + + NOT (LAUNCH=0) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (NO_SHELL_EXTENSIONS=1) + + + + + (NO_START_MENU_SHORTCUTS=1) + + + + + (NO_DESKTOP_SHORTCUT=1) + + + + diff --git a/admin/win/msi/Nextcloud.wxs b/admin/win/msi/Nextcloud~~.wxs similarity index 100% rename from admin/win/msi/Nextcloud.wxs rename to admin/win/msi/Nextcloud~~.wxs diff --git a/admin/win/msi/OEM.wxi.in b/admin/win/msi/OEM.wxi.in index 85b8b71e1d510..c91a7c98e57af 100644 --- a/admin/win/msi/OEM.wxi.in +++ b/admin/win/msi/OEM.wxi.in @@ -39,7 +39,7 @@ - - + + diff --git a/admin/win/msi/gui/banner_2.bmp b/admin/win/msi/gui/banner_2.bmp new file mode 100644 index 0000000000000..5270a99dea084 Binary files /dev/null and b/admin/win/msi/gui/banner_2.bmp differ diff --git a/admin/win/msi/gui/dialog_2.bmp b/admin/win/msi/gui/dialog_2.bmp new file mode 100644 index 0000000000000..b870b472556df Binary files /dev/null and b/admin/win/msi/gui/dialog_2.bmp differ diff --git a/admin/win/msi/make-msi.bat.in b/admin/win/msi/make-msi.bat.in index 98731a15215ec..2347091ca9608 100644 --- a/admin/win/msi/make-msi.bat.in +++ b/admin/win/msi/make-msi.bat.in @@ -107,7 +107,7 @@ exit 0 set codepage=%~1 echo :: Compiling MSI project for codepage !codepage! - "%WIX%\bin\candle.exe" -dcodepage=!codepage! -dProductCode=%PRODUCTCODE% -dPlatform=%BuildArch% -arch %BuildArch% -dHarvestAppDir="%HarvestAppDir%" -ext WixUtilExtension NCMsiHelper.wxs WinShellExt.wxs collect.wxs Nextcloud.wxs RegistryCleanupCustomAction.wxs + "%WIX%\bin\candle.exe" -dcodepage=!codepage! -dProductCode=%PRODUCTCODE% -dPlatform=%BuildArch% -arch %BuildArch% -dHarvestAppDir="%HarvestAppDir%" -ext WixUtilExtension NCMsiHelper.wxs WinShellExt.wxs collect.wxs Ionos.wxs RegistryCleanupCustomAction.wxs if %ERRORLEVEL% neq 0 exit %ERRORLEVEL% echo/ @@ -121,7 +121,7 @@ exit 0 set culture=%%c echo :: Linking MSI project for !culture! - "%WIX%\bin\light.exe" -sw1076 -ext WixUIExtension -ext WixUtilExtension -cultures:!culture!;en-us NCMsiHelper.wixobj WinShellExt.wixobj collect.wixobj Nextcloud.wixobj RegistryCleanupCustomAction.wixobj -out "!culture!_@MSI_INSTALLER_FILENAME@" + "%WIX%\bin\light.exe" -sw1076 -ext WixUIExtension -ext WixUtilExtension -cultures:!culture!;en-us NCMsiHelper.wixobj WinShellExt.wixobj collect.wixobj Ionos.wixobj RegistryCleanupCustomAction.wixobj -out "!culture!_@MSI_INSTALLER_FILENAME@" if %ERRORLEVEL% neq 0 exit %ERRORLEVEL% if NOT "!culture!" == "en-us" ( diff --git a/cmake/modules/MacOSXBundleInfo.plist.in b/cmake/modules/MacOSXBundleInfo.plist.in index 962240069c9a8..a8b2dfc697518 100644 --- a/cmake/modules/MacOSXBundleInfo.plist.in +++ b/cmake/modules/MacOSXBundleInfo.plist.in @@ -42,10 +42,8 @@ SUShowReleaseNotes - SUPublicDSAKeyFile - dsa_pub.pem SUPublicEDKey - c3RcfDWDayvsYSZW8FhZN1UOJhvPVN30zleb4zOqbtU= + FQ8Dq6AiSDDv4XpnyJ3b6mQBFYLPKgj9ziEg/+VNGHg= UTExportedTypeDeclarations diff --git a/config.h.in b/config.h.in index 192790319534f..bdb0e25df2177 100644 --- a/config.h.in +++ b/config.h.in @@ -11,6 +11,7 @@ #cmakedefine THEME_INCLUDE @THEME_INCLUDE@ #cmakedefine APPLICATION_NAME "@APPLICATION_NAME@" +#cmakedefine WHITELABEL_BRAND "@WHITELABEL_BRAND@" #cmakedefine APPLICATION_VENDOR "@APPLICATION_VENDOR@" #cmakedefine APPLICATION_DOMAIN "@APPLICATION_DOMAIN@" #cmakedefine APPLICATION_REV_DOMAIN "@APPLICATION_REV_DOMAIN@" diff --git a/doc/ADR/20241007_TrackingWithGA4.md b/doc/ADR/20241007_TrackingWithGA4.md new file mode 100644 index 0000000000000..8422656e3ddc1 --- /dev/null +++ b/doc/ADR/20241007_TrackingWithGA4.md @@ -0,0 +1,32 @@ +# Use Tracking with Google Analytics 4 + +## Status + +proposed + +## Context + +The web front end already tracks user interactions with GA4. To track interactions in the client software as well, an implementation with GA should also be done here. + +## Decision + +Since the use of GA4 in desktop software and the use of the pure API are not natively supported, a reverse-engineered solution had to be used (see https://ga4mp.dev/#/), similar to the use of the GA4 Measurement Protocol, which, however, does not provide a complete replacement. + +This approach implements GA4 tracking based on HTTP POST requests. + +## Implementation Details + +The implementation consists of three classes: + +* GAnalyticsWorker +* GAnalytics +* DataCollectionWrapper + +The DataCollectionWrapper is the outermost layer and is used by the application to track actions and events. The DataCollectionWrapper provides enums for various tracking pages (areas of the application, such as GeneralSettings or UserSettings) and tracking elements (specific buttons, checkboxes, or similar items). +GAnalytics acts as an intermediary between the outer layer (DataCollectionWrapper) and the communication logic. Various variables are also set here. When a tracking call is made, it is forwarded to the GAnalyticsWorker, where it is queued. + +The GAnalyticsWorker contains a queue, a message loop, and a QNetworkAccessManager. At fixed intervals, the queue is checked for tracking calls, which are then sent to the GA4 interface. If multiple calls are present, the connection is kept alive until all tracking calls have been sent. + +## Consequences + +This approach allows client-side tracking using GA4, bridging the gap between web and desktop tracking. The modular design simplifies maintenance by separating tracking logic from communication handling. Usage is straightforward; the application simply calls the DataCollectionWrapper at points where tracking should occur, ensuring that actions and events are recorded seamlessly. However, relying on a reverse-engineered solution introduces risks of future incompatibility with GA4 updates. Managing a queue and network connection adds complexity, and there might be latency when batching tracking calls. Overall, the solution balances functionality with maintainability but carries some technical risks. diff --git a/doc/ADR/20250212_UseCostomizationService.md b/doc/ADR/20250212_UseCostomizationService.md new file mode 100644 index 0000000000000..2131d2188ea8f --- /dev/null +++ b/doc/ADR/20250212_UseCostomizationService.md @@ -0,0 +1,69 @@ +# Use Customization Service + +## Status + +accepted + +## Context + +Nextcloud (NC) offers a customization service. The service is a Nextcloud white-labeling offering. It allows customers and their forks to be redesigned according to their own specifications. There are two levels. + +### Brander + +Here, the icons, names, and technical aspects are being adjusted to release a simply redesigned, color-enhanced Nextcloud. + +### Actual Customization Service + +This will integrate deeper changes into the branded client. This could be anything, in principle. + +For this to work we need to create Pull-Requests against the branches of the nextcloud repo. These pull requests can not depend on the same code line. + +More information can be read [here](https://portal.nextcloud.com/article/Branding/Customization-Service) + +### Workflow on the Repo + +A PR always has a source branch and a target branch. The target branch must be `master` in our case. Source branches can follow our established workflow. However, there are some conditions: + +- Concurrent work on different features must be possible. +- Local builds must be possible for testing purposes. +- A nightly/onPush build might be desirable. +- It must be ensured that everyone has an up-to-date status with all PRs. + +There are two possible Solutions: + +1. We use one Pull-Request containing all changes. +2. We use multiple Pull requests containing changes grouped by files. + +## Decision + +We will use only one PR containing all our changes. + +At this point in time, it would be too much work to split all Changes into separate branches/PRs. + +## Implementation Details + +### Branching + +- We use the master branch only for pulling the updated nc/master branch. +- We have a separate develop branch to develop changes. +- We create a PR to nc/masetr from develop. + +## Consequences + +### Positive + +- We can use our existing git-workflow +- We have a simple repo-structure with only 2 really needed branches +- It is easy to build locally, because all changes are on one branch +- Less Branches to backport +- We do not need to sort every change to a separate PR + +### Negative + +- Merge conflicts can be complex +- The PR will contain all the changes, this could be overwhelming to check + +### Further Thoughts + +- We need to see how this works in practice +- This is not final \ No newline at end of file diff --git a/doc/ADR/_template.md b/doc/ADR/_template.md new file mode 100644 index 0000000000000..0f2aef85df5b5 --- /dev/null +++ b/doc/ADR/_template.md @@ -0,0 +1,30 @@ +# ADR template by Michael Nygard + +This is the template in [Documenting architecture decisions - Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). +You can use [adr-tools](https://github.com/npryce/adr-tools) for managing the ADR files. + +We extended this Template with ```Implementation Details``` (optional) + +In each ADR file, write these sections: + +# Title + +## Status + +What is the status, such as proposed, accepted, rejected, deprecated, superseded, etc.? + +## Context + +What is the issue that we're seeing that is motivating this decision or change? + +## Decision + +What is the change that we're proposing and/or doing? + +## Implementation Details + +(Optional) How was this Decision implemented. I.e. Uml Diagrams, Code etc. + +## Consequences + +What becomes easier or more difficult to do because of this change? diff --git a/find_qml_imports.py b/find_qml_imports.py new file mode 100644 index 0000000000000..0c7dbce5a48a7 --- /dev/null +++ b/find_qml_imports.py @@ -0,0 +1,29 @@ +import os +import sys +import re + +def find_imports_in_qml(root_dir, import_prefix): + pattern = re.compile(r'^\s*import\s+' + re.escape(import_prefix) + r'.*$', re.MULTILINE) + matches = [] + for subdir, _, files in os.walk(root_dir): + for file in files: + if file.endswith('.qml'): + file_path = os.path.join(subdir, file) + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + for match in pattern.finditer(content): + matches.append((file_path, match.group().strip())) + except Exception: + pass # Ignore files that can't be processed + print(f"Found {len(matches)} matching import lines:") + for file_path, line in matches: + print(f"{file_path}: {line}") + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: python find_qml_imports.py ") + sys.exit(1) + import_prefix = sys.argv[1] + root_dir = sys.argv[2] + find_imports_in_qml(root_dir, import_prefix) diff --git a/fonts/OpenSans-Bold.ttf b/fonts/OpenSans-Bold.ttf new file mode 100644 index 0000000000000..efdd5e84a0397 Binary files /dev/null and b/fonts/OpenSans-Bold.ttf differ diff --git a/fonts/OpenSans-BoldItalic.ttf b/fonts/OpenSans-BoldItalic.ttf new file mode 100644 index 0000000000000..9bf9b4e97b657 Binary files /dev/null and b/fonts/OpenSans-BoldItalic.ttf differ diff --git a/fonts/OpenSans-ExtraBold.ttf b/fonts/OpenSans-ExtraBold.ttf new file mode 100644 index 0000000000000..67fcf0fb2af0c Binary files /dev/null and b/fonts/OpenSans-ExtraBold.ttf differ diff --git a/fonts/OpenSans-ExtraBoldItalic.ttf b/fonts/OpenSans-ExtraBoldItalic.ttf new file mode 100644 index 0000000000000..086722809c74d Binary files /dev/null and b/fonts/OpenSans-ExtraBoldItalic.ttf differ diff --git a/fonts/OpenSans-Italic.ttf b/fonts/OpenSans-Italic.ttf new file mode 100644 index 0000000000000..117856707b9b8 Binary files /dev/null and b/fonts/OpenSans-Italic.ttf differ diff --git a/fonts/OpenSans-Light.ttf b/fonts/OpenSans-Light.ttf new file mode 100644 index 0000000000000..6580d3a169e89 Binary files /dev/null and b/fonts/OpenSans-Light.ttf differ diff --git a/fonts/OpenSans-LightItalic.ttf b/fonts/OpenSans-LightItalic.ttf new file mode 100644 index 0000000000000..1e0c3319810ad Binary files /dev/null and b/fonts/OpenSans-LightItalic.ttf differ diff --git a/fonts/OpenSans-Regular.ttf b/fonts/OpenSans-Regular.ttf new file mode 100644 index 0000000000000..29bfd35a2bfdd Binary files /dev/null and b/fonts/OpenSans-Regular.ttf differ diff --git a/fonts/OpenSans-SemiBold.ttf b/fonts/OpenSans-SemiBold.ttf new file mode 100644 index 0000000000000..54e7059cf3635 Binary files /dev/null and b/fonts/OpenSans-SemiBold.ttf differ diff --git a/fonts/OpenSans-SemiBoldItalic.ttf b/fonts/OpenSans-SemiBoldItalic.ttf new file mode 100644 index 0000000000000..aebcf1421226d Binary files /dev/null and b/fonts/OpenSans-SemiBoldItalic.ttf differ diff --git a/ionos-theme.qrc b/ionos-theme.qrc new file mode 100644 index 0000000000000..2377ef3b72aa5 --- /dev/null +++ b/ionos-theme.qrc @@ -0,0 +1,88 @@ + + + theme/colored/ionos-data-protection-logo.png + theme/colored/strato-data-protection-logo.png + theme/ses/ses-IONOS-logo.svg + theme/colored/IONOS_logo_w_suffix_frontend.png + theme/colored/IONOS_logo_w_suffix_frontend@2x.png + theme/colored/IONOS_logo_w_suffix_frontend.svg + fonts/OpenSans-SemiBold.ttf + fonts/OpenSans-Regular.ttf + theme/ses/ses-accountDelete.svg + theme/ses/ses-activityDelete.svg + theme/ses/ses-accountLogout.svg + theme/ses/ses-accountPause.svg + theme/ses/ses-accountQuit.svg + theme/ses/ses-accountResume.svg + theme/ses/ses-activity.svg + theme/ses/ses-chevron.svg + theme/ses/ses-clipboard.svg + theme/ses/ses-lightClipboard.svg + theme/ses/ses-darkPlus.svg + theme/ses/ses-lightPlus.svg + theme/ses/ses-addlivebackup.svg + theme/ses/ses-file.svg + theme/ses/ses-folderIcon.svg + theme/ses/ses-folderIconBright.svg + theme/ses/ses-syncstate-success.svg + theme/ses/ses-syncstate-syncing.svg + theme/ses/ses-syncstate-paused.svg + theme/ses/ses-syncstate-warning.svg + theme/ses/ses-syncstate-error.svg + theme/ses/ses-state-offline.svg + theme/ses/ses-snackbar-success.svg + theme/ses/ses-snackbar-warning.svg + theme/ses/ses-snackbar-error.svg + theme/ses/ses-more.svg + theme/ses/ses-more-hover.svg + theme/ses/ses-questionmark.svg + theme/ses/ses-refresh.svg + theme/ses/ses-settings.svg + theme/ses/ses-settingsAvatar.svg + theme/ses/ses-settingsAvatarRound.svg + theme/ses/ses-info.svg + theme/ses/ses-syncArrows.svg + theme/ses/ses-external.svg + theme/ses/ses-website.svg + theme/ses/strato/ses-STRATO-logo.svg + theme/colored/STRATO_logo_w_suffix_frontend.png + theme/colored/STRATO_logo_w_suffix_frontend@2x.png + theme/colored/STRATO_logo_w_suffix_frontend.svg + theme/ses/strato/ses-accountDelete.svg + theme/ses/strato/ses-activityDelete.svg + theme/ses/strato/ses-accountLogout.svg + theme/ses/strato/ses-accountPause.svg + theme/ses/strato/ses-accountQuit.svg + theme/ses/strato/ses-accountResume.svg + theme/ses/strato/ses-activity.svg + theme/ses/strato/ses-chevron.svg + theme/ses/strato/ses-clipboard.svg + theme/ses/strato/ses-lightClipboard.svg + theme/ses/strato/ses-darkPlus.svg + theme/ses/strato/ses-lightPlus.svg + theme/ses/strato/ses-addlivebackup.svg + theme/ses/strato/ses-file.svg + theme/ses/strato/ses-folderIcon.svg + theme/ses/strato/ses-folderIconBright.svg + theme/ses/strato/ses-syncstate-success.svg + theme/ses/strato/ses-syncstate-syncing.svg + theme/ses/strato/ses-syncstate-paused.svg + theme/ses/strato/ses-syncstate-warning.svg + theme/ses/strato/ses-syncstate-error.svg + theme/ses/strato/ses-state-offline.svg + theme/ses/strato/ses-snackbar-success.svg + theme/ses/strato/ses-snackbar-warning.svg + theme/ses/strato/ses-snackbar-error.svg + theme/ses/strato/ses-more.svg + theme/ses/strato/ses-more-hover.svg + theme/ses/strato/ses-questionmark.svg + theme/ses/strato/ses-refresh.svg + theme/ses/strato/ses-settings.svg + theme/ses/strato/ses-settingsAvatar.svg + theme/ses/strato/ses-settingsAvatarRound.svg + theme/ses/strato/ses-info.svg + theme/ses/strato/ses-syncArrows.svg + theme/ses/strato/ses-external.svg + theme/ses/strato/ses-website.svg + + diff --git a/ionos-theme.qrc.in b/ionos-theme.qrc.in new file mode 100644 index 0000000000000..2377ef3b72aa5 --- /dev/null +++ b/ionos-theme.qrc.in @@ -0,0 +1,88 @@ + + + theme/colored/ionos-data-protection-logo.png + theme/colored/strato-data-protection-logo.png + theme/ses/ses-IONOS-logo.svg + theme/colored/IONOS_logo_w_suffix_frontend.png + theme/colored/IONOS_logo_w_suffix_frontend@2x.png + theme/colored/IONOS_logo_w_suffix_frontend.svg + fonts/OpenSans-SemiBold.ttf + fonts/OpenSans-Regular.ttf + theme/ses/ses-accountDelete.svg + theme/ses/ses-activityDelete.svg + theme/ses/ses-accountLogout.svg + theme/ses/ses-accountPause.svg + theme/ses/ses-accountQuit.svg + theme/ses/ses-accountResume.svg + theme/ses/ses-activity.svg + theme/ses/ses-chevron.svg + theme/ses/ses-clipboard.svg + theme/ses/ses-lightClipboard.svg + theme/ses/ses-darkPlus.svg + theme/ses/ses-lightPlus.svg + theme/ses/ses-addlivebackup.svg + theme/ses/ses-file.svg + theme/ses/ses-folderIcon.svg + theme/ses/ses-folderIconBright.svg + theme/ses/ses-syncstate-success.svg + theme/ses/ses-syncstate-syncing.svg + theme/ses/ses-syncstate-paused.svg + theme/ses/ses-syncstate-warning.svg + theme/ses/ses-syncstate-error.svg + theme/ses/ses-state-offline.svg + theme/ses/ses-snackbar-success.svg + theme/ses/ses-snackbar-warning.svg + theme/ses/ses-snackbar-error.svg + theme/ses/ses-more.svg + theme/ses/ses-more-hover.svg + theme/ses/ses-questionmark.svg + theme/ses/ses-refresh.svg + theme/ses/ses-settings.svg + theme/ses/ses-settingsAvatar.svg + theme/ses/ses-settingsAvatarRound.svg + theme/ses/ses-info.svg + theme/ses/ses-syncArrows.svg + theme/ses/ses-external.svg + theme/ses/ses-website.svg + theme/ses/strato/ses-STRATO-logo.svg + theme/colored/STRATO_logo_w_suffix_frontend.png + theme/colored/STRATO_logo_w_suffix_frontend@2x.png + theme/colored/STRATO_logo_w_suffix_frontend.svg + theme/ses/strato/ses-accountDelete.svg + theme/ses/strato/ses-activityDelete.svg + theme/ses/strato/ses-accountLogout.svg + theme/ses/strato/ses-accountPause.svg + theme/ses/strato/ses-accountQuit.svg + theme/ses/strato/ses-accountResume.svg + theme/ses/strato/ses-activity.svg + theme/ses/strato/ses-chevron.svg + theme/ses/strato/ses-clipboard.svg + theme/ses/strato/ses-lightClipboard.svg + theme/ses/strato/ses-darkPlus.svg + theme/ses/strato/ses-lightPlus.svg + theme/ses/strato/ses-addlivebackup.svg + theme/ses/strato/ses-file.svg + theme/ses/strato/ses-folderIcon.svg + theme/ses/strato/ses-folderIconBright.svg + theme/ses/strato/ses-syncstate-success.svg + theme/ses/strato/ses-syncstate-syncing.svg + theme/ses/strato/ses-syncstate-paused.svg + theme/ses/strato/ses-syncstate-warning.svg + theme/ses/strato/ses-syncstate-error.svg + theme/ses/strato/ses-state-offline.svg + theme/ses/strato/ses-snackbar-success.svg + theme/ses/strato/ses-snackbar-warning.svg + theme/ses/strato/ses-snackbar-error.svg + theme/ses/strato/ses-more.svg + theme/ses/strato/ses-more-hover.svg + theme/ses/strato/ses-questionmark.svg + theme/ses/strato/ses-refresh.svg + theme/ses/strato/ses-settings.svg + theme/ses/strato/ses-settingsAvatar.svg + theme/ses/strato/ses-settingsAvatarRound.svg + theme/ses/strato/ses-info.svg + theme/ses/strato/ses-syncArrows.svg + theme/ses/strato/ses-external.svg + theme/ses/strato/ses-website.svg + + diff --git a/ionos.qrc b/ionos.qrc new file mode 100644 index 0000000000000..0cc12434b4814 --- /dev/null +++ b/ionos.qrc @@ -0,0 +1,12 @@ + + + src/gui/tray/HeaderLogo.qml + src/gui/SesComponents/SesErrorBox.qml + src/gui/SesComponents/SesTrayHeader.qml + src/gui/tray/TrayWindowAccountMenu.qml + src/gui/tray/PrimaryPillButton.qml + src/gui/tray/SecondaryPillButton.qml + src/gui/tray/IconButton.qml + src/gui/tray/AccountMenuItem.qml + + diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExt.entitlements b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExt.entitlements index eab912dc49600..8102f4fb7df19 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExt.entitlements +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExt.entitlements @@ -6,7 +6,7 @@ com.apple.security.application-groups - $(OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX)$(OC_APPLICATION_REV_DOMAIN) + 5TDLCVD243.com.ionos.hidrivenext.desktopclient com.apple.security.network.client diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+ClientInterface.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+ClientInterface.swift index 405b6e7c7adaf..835556e582f95 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+ClientInterface.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+ClientInterface.swift @@ -94,7 +94,7 @@ extension FileProviderExtension: NSFileProviderServicing, ChangeNotificationInte userId: String, serverUrl: String, password: String, - userAgent: String = "Nextcloud-macOS/FileProviderExt", + userAgent: String = "IONOS HiDrive Next/FileProviderExt", completionHandler: ((NSError?) -> Void)? = nil ) { let account = Account(user: user, id: userId, serverUrl: serverUrl, password: password) diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+CustomActions.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+CustomActions.swift index dbc8c64b49c26..2314bb8f48a22 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+CustomActions.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+CustomActions.swift @@ -14,13 +14,13 @@ extension FileProviderExtension: NSFileProviderCustomAction { completionHandler: @escaping ((any Error)?) -> Void ) -> Progress { switch actionIdentifier.rawValue { - case "com.nextcloud.desktopclient.FileProviderExt.KeepDownloadedAction": + case "com.ionos.hidrive.desktopclient.FileProviderExt.KeepDownloadedAction": return performKeepDownloadedAction( keepDownloaded: true, onItemsWithIdentifiers: itemIdentifiers, completionHandler: completionHandler ) - case "com.nextcloud.desktopclient.FileProviderExt.AutoEvictAction": + case "com.ionos.hidrive.desktopclient.FileProviderExt.AutoEvictAction": return performKeepDownloadedAction( keepDownloaded: false, onItemsWithIdentifiers: itemIdentifiers, diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift index cbc14fdc75e27..5d28e6253baf3 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift @@ -33,8 +33,7 @@ import OSLog return nil; } - let socketPath = containerUrl.appendingPathComponent( - ".fileprovidersocket", conformingTo: .archive) + let socketPath = containerUrl.appendingPathComponent("fps", conformingTo: .archive) let lineProcessor = FileProviderSocketLineProcessor(delegate: self, log: log) return LocalSocketClient(socketPath: socketPath.path, lineProcessor: lineProcessor) }() diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Info.plist b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Info.plist index efb5462421ae5..24545ff277167 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Info.plist +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Info.plist @@ -24,7 +24,7 @@ NSExtensionFileProviderActionActivationRule SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.displayKeepDownloaded == true ).@count > 0 NSExtensionFileProviderActionIdentifier - com.nextcloud.desktopclient.FileProviderExt.KeepDownloadedAction + com.ionos.hidrive.desktopclient.FileProviderExt.KeepDownloadedAction NSExtensionFileProviderActionName Always keep downloaded @@ -32,7 +32,7 @@ NSExtensionFileProviderActionActivationRule SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.displayAllowAutoEvicting == true ).@count > 0 NSExtensionFileProviderActionIdentifier - com.nextcloud.desktopclient.FileProviderExt.AutoEvictAction + com.ionos.hidrive.desktopclient.FileProviderExt.AutoEvictAction NSExtensionFileProviderActionName Allow automatic freeing up space diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationService.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationService.swift index b9fb2ed273a2c..a8e6bfbff1a33 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationService.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/ClientCommunicationService.swift @@ -11,7 +11,7 @@ import OSLog class ClientCommunicationService: NSObject, NSFileProviderServiceSource, NSXPCListenerDelegate, ClientCommunicationProtocol { let listener = NSXPCListener.anonymous() let logger: FileProviderLogger - let serviceName = NSFileProviderServiceName("com.nextcloud.desktopclient.ClientCommunicationService") + let serviceName = NSFileProviderServiceName("com.ionos.hidrivenext.desktopclient.ClientCommunicationService") let fpExtension: FileProviderExtension init(fpExtension: FileProviderExtension) { diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionService.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionService.swift index da2f95e171d74..00473de117aae 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionService.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionService.swift @@ -14,7 +14,9 @@ import NextcloudKit /// /// This does not need to be branded because it is scoped by the app-specific file provider domains in the file system already. /// -let fpUiExtensionServiceName = NSFileProviderServiceName("com.nextcloud.desktopclient.FPUIExtensionService") +let fpUiExtensionServiceName = NSFileProviderServiceName( + "com.ionos.hidrivenext.desktopclient.FPUIExtensionService" +) /// /// The requirements of the service exposed and dedicated to the file provider user interface extension. diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/DocumentActionViewController.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/DocumentActionViewController.swift index ec4671d70e9f7..afc1fc3cfbd5c 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/DocumentActionViewController.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/DocumentActionViewController.swift @@ -64,13 +64,13 @@ class DocumentActionViewController: FPUIActionExtensionViewController { logger?.info("Preparing action: \(actionIdentifier)") switch (actionIdentifier) { - case "com.nextcloud.desktopclient.FileProviderUIExt.ShareAction": + case "com.ionos.hidrivenext.desktopclient.FileProviderUIExt.ShareAction": prepare(childViewController: ShareViewController(itemIdentifiers, serviceResolver: serviceResolver, log: log)) - case "com.nextcloud.desktopclient.FileProviderUIExt.LockFileAction": + case "com.ionos.hidrivenext.desktopclient.FileProviderUIExt.LockFileAction": prepare(childViewController: LockViewController(itemIdentifiers, locking: true, serviceResolver: serviceResolver, log: log)) - case "com.nextcloud.desktopclient.FileProviderUIExt.UnlockFileAction": + case "com.ionos.hidrivenext.desktopclient.FileProviderUIExt.UnlockFileAction": prepare(childViewController: LockViewController(itemIdentifiers, locking: false, serviceResolver: serviceResolver, log: log)) - case "com.nextcloud.desktopclient.FileProviderUIExt.EvictAction": + case "com.ionos.hidrive.desktopclient.FileProviderUIExt.EvictAction": evict(itemsWithIdentifiers: itemIdentifiers, inDomain: domain); extensionContext.completeRequest(); default: diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Info.plist b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Info.plist index ee94a3ceb1f2d..8dd133e54fcf3 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Info.plist +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Info.plist @@ -22,7 +22,7 @@ NSExtensionFileProviderActionIdentifier - com.nextcloud.desktopclient.FileProviderUIExt.UnlockFileAction + com.ionos.hidrivenext.desktopclient.FileProviderUIExt.UnlockFileAction NSExtensionFileProviderActionName Unlock file NSExtensionFileProviderActionActivationRule @@ -34,13 +34,13 @@ NSExtensionFileProviderActionName Lock file NSExtensionFileProviderActionIdentifier - com.nextcloud.desktopclient.FileProviderUIExt.LockFileAction + com.ionos.hidrivenext.desktopclient.FileProviderUIExt.LockFileAction NSExtensionFileProviderActionActivationRule SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.displayShare == true ).@count > 0 NSExtensionFileProviderActionIdentifier - com.nextcloud.desktopclient.FileProviderUIExt.ShareAction + com.ionos.hidrivenext.desktopclient.FileProviderUIExt.ShareAction NSExtensionFileProviderActionName Share options @@ -48,7 +48,7 @@ NSExtensionFileProviderActionActivationRule SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.downloaded == true ).@count > 0 NSExtensionFileProviderActionIdentifier - com.nextcloud.desktopclient.FileProviderUIExt.EvictAction + com.ionos.hidrive.desktopclient.FileProviderUIExt.EvictAction NSExtensionFileProviderActionName Free up space diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Locking/LockViewController.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Locking/LockViewController.swift index f92960571c67d..3d47a218b8d7a 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Locking/LockViewController.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Locking/LockViewController.swift @@ -184,7 +184,7 @@ class LockViewController: NSViewController { user: account.username, userId: account.id, password: account.password, - userAgent: "Nextcloud-macOS/FileProviderUIExt", + userAgent: "IONOS HiDrive Next/FileProviderUIExt", groupIdentifier: "" ) diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Sharing/ShareOptionsView.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Sharing/ShareOptionsView.swift index 5d1436b7ac3bd..23019b377ca47 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Sharing/ShareOptionsView.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Sharing/ShareOptionsView.swift @@ -194,6 +194,12 @@ class ShareOptionsView: NSView { caps.defaultPermissions & NKShare.Permission.update.rawValue != 0 ? .on : .off + groupShareMenuItem.isHidden = true + emailShareMenuItem.isHidden = false + federatedCloudShareMenuItem.isHidden = true + teamShare.isHidden = true + talkConversationShare.isHidden = true + switch type { case .publicLink: passwordProtectCheckbox.isHidden = false diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Sharing/ShareTableViewDataSource.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Sharing/ShareTableViewDataSource.swift index 1cf96a6fb8ee2..fa2e5639356c6 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Sharing/ShareTableViewDataSource.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Sharing/ShareTableViewDataSource.swift @@ -46,9 +46,7 @@ class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDele } } } - - private(set) var userAgent: String = "Nextcloud-macOS/FileProviderUIExt" - + private(set) var userAgent: String = "IONOS HiDrive Next/FileProviderUIExt" private(set) var account: Account? { didSet { guard let account = account else { diff --git a/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m b/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m index 97f32c8a3a47f..ea1782037be12 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m +++ b/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m @@ -45,19 +45,8 @@ - (instancetype)init [syncController setBadgeImage:warning label:@"Ignored" forBadgeIdentifier:@"IGNORE+SWM"]; [syncController setBadgeImage:error label:@"Error" forBadgeIdentifier:@"ERROR+SWM"]; - // The Mach port name needs to: - // - Be prefixed with the code signing Team ID - // - Then infixed with the sandbox App Group - // - The App Group itself must be a prefix of (or equal to) the application bundle identifier - // We end up in the official signed client with: 9B5WD74GWJ.com.owncloud.desktopclient.socket - // With ad-hoc signing (the '-' signing identity) we must drop the Team ID. - // When the code isn't sandboxed (e.g. the OC client or the legacy overlay icon extension) - // the OS doesn't seem to put any restriction on the port name, so we just follow what - // the sandboxed App Extension needs. - // https://developer.apple.com/library/mac/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW24 - NSURL *container = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:socketApiPrefix]; - NSURL *socketPath = [container URLByAppendingPathComponent:@".socket" isDirectory:NO]; + NSURL *socketPath = [container URLByAppendingPathComponent:@"s" isDirectory:NO]; NSLog(@"Socket path: %@", socketPath.path); diff --git a/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/xcshareddata/xcschemes/NextcloudDev.xcscheme b/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/xcshareddata/xcschemes/NextcloudDev.xcscheme index 19e87d61f6c65..0e76b4c2e7cf0 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/xcshareddata/xcschemes/NextcloudDev.xcscheme +++ b/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/xcshareddata/xcschemes/NextcloudDev.xcscheme @@ -42,7 +42,7 @@ allowLocationSimulation = "YES"> diff --git a/shell_integration/windows/WinShellExt.wxs.in b/shell_integration/windows/WinShellExt.wxs.in index a23bc12cf3031..b424ab0a67d45 100644 --- a/shell_integration/windows/WinShellExt.wxs.in +++ b/shell_integration/windows/WinShellExt.wxs.in @@ -31,11 +31,11 @@ There is a limit in Windows (oh wonder^^) so that only the first 15 extensions get invoked, this is why to use that dirty little trick to get ahead ;) See: https://docs.microsoft.com/en-us/windows/win32/shell/context-menu-handlers?redirectedfrom=MSDN#employing-the-verb-selection-model --> - - - - - + + + + + diff --git a/src/common/utility.cpp b/src/common/utility.cpp index e1e65c4a7b1e4..131cee65a9ce3 100644 --- a/src/common/utility.cpp +++ b/src/common/utility.cpp @@ -166,8 +166,8 @@ QByteArray Utility::userAgentString() QByteArray Utility::friendlyUserAgentString() { - const auto pattern = QStringLiteral("%1 (Desktop Client - %2)"); - const auto userAgent = pattern.arg(QSysInfo::machineHostName(), platform()); + const auto pattern = QStringLiteral("%1 (%2 Desktop Client - %3)"); + const auto userAgent = pattern.arg(QSysInfo::machineHostName(), qApp->applicationName(), platform()); return userAgent.toUtf8(); } diff --git a/src/csync/csync_exclude.cpp b/src/csync/csync_exclude.cpp index b92cfa08e43ca..d565e38ea76ea 100644 --- a/src/csync/csync_exclude.cpp +++ b/src/csync/csync_exclude.cpp @@ -142,7 +142,7 @@ static CSYNC_EXCLUDE_TYPE _csync_excluded_common(const QString &path, bool exclu if (bname.startsWith(QLatin1String(".owncloudsync.log"), Qt::CaseInsensitive)) { // ".owncloudsync.log*" return CSYNC_FILE_SILENTLY_EXCLUDED; } - if (bname.startsWith(QLatin1String(".nextcloudsync.log"), Qt::CaseInsensitive)) { // ".nextcloudsync.log*" + if (bname.startsWith(QLatin1String(".hidrivenextsync.log"), Qt::CaseInsensitive)) { // ".hidrivenextsync.log*" return CSYNC_FILE_SILENTLY_EXCLUDED; } if (bname.startsWith(QLatin1String(".nextcloudpermissions.log"), Qt::CaseInsensitive)) { // ".nextcloudpermissions.log*" diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 77ded6e563b12..552872e8c1192 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -9,6 +9,9 @@ find_package(KF6GuiAddons) if(CMAKE_BUILD_TYPE MATCHES Debug) add_definitions(-DQT_QML_DEBUG) endif() +if(CMAKE_BUILD_TYPE MATCHES "RelWithDebInfo") + add_definitions(-DBUILDTYPE_RELWITHDEBINFO) +endif() IF(BUILD_UPDATER) add_subdirectory(updater) @@ -21,6 +24,7 @@ if(APPLICATION_NAME STREQUAL "Nextcloud") endif() configure_file(${CMAKE_SOURCE_DIR}/theme.qrc.in ${CMAKE_SOURCE_DIR}/theme.qrc) +configure_file(${CMAKE_SOURCE_DIR}/ionos-theme.qrc.in ${CMAKE_SOURCE_DIR}/ionos-theme.qrc) set(theme_dir ${CMAKE_SOURCE_DIR}/theme) set(client_UI_SRCS @@ -54,7 +58,12 @@ set(client_UI_SRCS wizard/proxysettings.ui ) -qt_add_resources(client_UI_SRCS ../../resources.qrc ${CMAKE_SOURCE_DIR}/theme.qrc) +list(APPEND client_UI_SRCS + wizard/dataprotectionpage.ui + wizard/dataprotectionsettingspage.ui +) + +qt_add_resources(client_UI_SRCS ../../resources.qrc ../../ionos.qrc ${CMAKE_SOURCE_DIR}/theme.qrc ${CMAKE_SOURCE_DIR}/ionos-theme.qrc) set(client_SRCS accountmanager.h @@ -256,7 +265,7 @@ set(client_SRCS wizard/linklabel.cpp wizard/wizardproxysettingsdialog.h wizard/wizardproxysettingsdialog.cpp - ) +) if (NOT DISABLE_ACCOUNT_MIGRATION) list(APPEND client_SRCS @@ -265,6 +274,40 @@ if (NOT DISABLE_ACCOUNT_MIGRATION) ) endif() +list(APPEND client_SRCS + clickablelabel.h + buttonstyle.h + ionostheme.h + whitelabeltheme.h + stratotheme.h + basetheme.h + wizard/dataprotectionpage.h + wizard/dataprotectionpage.cpp + wizard/dataprotectionsettingspage.h + wizard/dataprotectionsettingspage.cpp + ga4/datacollectionwrapper.cpp + ga4/datacollectionwrapper.h + ga4/ganalytics_worker.cpp + ga4/ganalytics_worker.h + ga4/ganalytics.cpp + ga4/ganalytics.h + sessnackbar.h + sessnackbar.cpp + sesstyle.h + sesstyle.cpp + linkbutton.h + linkbutton.cpp + buttonstylestrategy.h + sesFileIconProvider.h + sesFileIconProvider.cpp + pushbuttonstylehelper.h + pushbuttonstylehelper.cpp + moreoptionsbuttonstylehelper.h + moreoptionsbuttonstylehelper.cpp + SesComponents/syncdirvalidation.h + SesComponents/syncdirvalidation.cpp +) + if (WITH_WEBENGINE) list(APPEND client_SRCS wizard/webviewpage.h @@ -719,7 +762,8 @@ endif() # if building a bundle or not and the install_qt4_executable needs to be called afterwards # # OSX: Run macdeployqt for src/gui and for src/cmd using the -executable option -if(BUILD_OWNCLOUD_OSX_BUNDLE AND NOT BUILD_LIBRARIES_ONLY) +option(SKIP_MACDEPLOYQT "Skip macdeployqt post-build step for faster incremental build" OFF) +if(BUILD_OWNCLOUD_OSX_BUNDLE AND NOT BUILD_LIBRARIES_ONLY AND NOT SKIP_MACDEPLOYQT) get_target_property (QT_QMAKE_EXECUTABLE Qt::qmake IMPORTED_LOCATION) get_filename_component(QT_BIN_DIR "${QT_QMAKE_EXECUTABLE}" DIRECTORY) find_program(MACDEPLOYQT_EXECUTABLE macdeployqt HINTS "${QT_BIN_DIR}") @@ -737,7 +781,7 @@ if(BUILD_OWNCLOUD_OSX_BUNDLE AND NOT BUILD_LIBRARIES_ONLY) "$/../.." -qmldir=${CMAKE_SOURCE_DIR}/src/gui -always-overwrite - -executable="$/${cmd_NAME}" + -executable=$/${cmd_NAME} ${NO_STRIP} COMMAND "${CMAKE_COMMAND}" -E rm -rf "${BIN_OUTPUT_DIRECTORY}/${OWNCLOUD_OSX_BUNDLE}/Contents/PlugIns/bearer" diff --git a/src/gui/ConflictDelegate.qml b/src/gui/ConflictDelegate.qml index 09318d46cea0d..36a777e85e9e5 100644 --- a/src/gui/ConflictDelegate.qml +++ b/src/gui/ConflictDelegate.qml @@ -8,7 +8,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls import Style -import com.nextcloud.desktopclient +import com.ionos.hidrivenext.desktopclient import "./tray" Item { diff --git a/src/gui/EmojiPicker.qml b/src/gui/EmojiPicker.qml index f49410d6ea399..87f490c0f11e8 100644 --- a/src/gui/EmojiPicker.qml +++ b/src/gui/EmojiPicker.qml @@ -8,7 +8,7 @@ import QtQuick.Controls import QtQuick.Layouts import Style -import com.nextcloud.desktopclient 1.0 as NC +import com.ionos.hidrivenext.desktopclient as NC import "./tray" ColumnLayout { diff --git a/src/gui/ResolveConflictsDialog.qml b/src/gui/ResolveConflictsDialog.qml index dd55598d86d30..91754479a15eb 100644 --- a/src/gui/ResolveConflictsDialog.qml +++ b/src/gui/ResolveConflictsDialog.qml @@ -10,7 +10,7 @@ import QtQuick.Layouts import QtQuick.Controls import QtQml.Models import Style -import com.nextcloud.desktopclient +import com.ionos.hidrivenext.desktopclient import "./tray" ApplicationWindow { @@ -20,6 +20,10 @@ ApplicationWindow { flags: Qt.Window | Qt.Dialog visible: true + color: Style.sesBackgroundColor + palette.base: Style.sesBackgroundColor + palette.windowText: Style.sesTrayFontColor + palette.text: Style.sesTrayFontColor LayoutMirroring.enabled: Application.layoutDirection === Qt.RightToLeft LayoutMirroring.childrenInherit: true @@ -131,22 +135,37 @@ ApplicationWindow { delegate: ConflictDelegate { width: conflictListView.contentItem.width - height: 100 + height: 140 } } } } DialogButtonBox { + id: buttonBox Layout.fillWidth: true - Button { + background: Rectangle { + color: Style.sesBackgroundColor + } + + readonly property int pixelSize: Style.sesFontPixelSize + readonly property int fontWeight: Style.sesFontNormalWeight + + PrimaryPillButton { + font.pixelSize: pixelSize + font.weight: fontWeight text: qsTr("Resolve conflicts") - DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + + onClicked: buttonBox.onAccepted() } - Button { + + SecondaryPillButton { + font.pixelSize: pixelSize + font.weight: fontWeight text: qsTr("Cancel") - DialogButtonBox.buttonRole: DialogButtonBox.RejectRole + + onClicked: buttonBox.onRejected() } onAccepted: function() { @@ -159,10 +178,4 @@ ApplicationWindow { } } } - - Rectangle { - color: palette.base - anchors.fill: parent - z: 1 - } } diff --git a/src/gui/SesComponents/SesErrorBox.qml b/src/gui/SesComponents/SesErrorBox.qml new file mode 100644 index 0000000000000..f597d509672b0 --- /dev/null +++ b/src/gui/SesComponents/SesErrorBox.qml @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2021 by Felix Weilbach + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 + +import Style 1.0 +import "../tray/" + +Item { + id: errorBox + + property string text: "" + + implicitHeight: errorMessageLayout.implicitHeight + (2 * Style.standardSpacing) + + Rectangle { + anchors.fill: parent + border.color: Style.sesErrorBoxBorder + border.width: Style.thickBorderWidth + radius: Style.sesCornerRadius + } + + GridLayout { + id: errorMessageLayout + + anchors.fill: parent + anchors.margins: Style.standardSpacing + anchors.leftMargin: Style.standardSpacing + + columns: 2 + + Image { + source: Style.sesErrorBoxIcon + width: 24 + height: 24 + Layout.rightMargin: Style.standardSpacing + } + + EnforcedPlainTextLabel { + Layout.fillWidth: true + + font.pixelSize: Style.sesFontPixelSize + font.weight: Style.sesFontBoldWeight + + text: qsTr("Error") + color: Style.sesErrorBoxText + } + + EnforcedPlainTextLabel { + id: errorMessage + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.columnSpan: 2 + + wrapMode: Text.WordWrap + text: errorBox.text + + font.pixelSize: Style.sesFontErrortextPixelSize + font.weight: Style.sesFontNormalWeight + } + } +} diff --git a/src/gui/SesComponents/SesTrayHeader.qml b/src/gui/SesComponents/SesTrayHeader.qml new file mode 100644 index 0000000000000..94d3e7e23babf --- /dev/null +++ b/src/gui/SesComponents/SesTrayHeader.qml @@ -0,0 +1,70 @@ +import QtQuick +import QtQuick.Window +import QtQuick.Controls +import QtQuick.Layouts +import Qt.labs.platform 1.1 as NativeDialogs + +import "../" +import "../filedetails/" +import "../tray/" + +// Custom qml modules are in /theme (and included by resources.qrc) +import Style 1.0 + +import com.ionos.hidrivenext.desktopclient + + +Rectangle { + + height: Style.sesTrayHeaderHeight + Style.sesHeaderTopMargin * 2 + color: Style.sesBackgroundColor + radius: 0.0 + clip: true + + RowLayout { + id: trayWindowHeaderLayout + + anchors.fill: parent + anchors.leftMargin: Style.sesTrayHeaderMargin + anchors.rightMargin: Style.sesTrayHeaderMargin + anchors.topMargin: Style.sesHeaderTopMargin + anchors.bottomMargin: Style.sesHeaderTopMargin + + TrayWindowAccountMenu{ + id: currentAccountHeaderButton + Layout.preferredWidth: Style.sesAccountButtonWidth + Layout.preferredHeight: Style.sesAccountButtonHeight + } + + HeaderButton { + id: trayWindowWebsiteButton + + icon.source: Style.sesWebsiteIcon + icon.color: Style.sesIconColor + onClicked: UserModel.openCurrentAccountServer() + + text: qsTr("Website") + + Layout.rightMargin: 2 + + Accessible.role: Accessible.Button + Accessible.name: qsTr("Open Nextcloud in browser") + Accessible.onPressAction: trayWindowWebsiteButton.clicked() + } + + TrayFoldersMenuButton { + id: openLocalFolderButton + + visible: currentUser.hasLocalFolder + currentUser: UserModel.currentUser + + onClicked: openLocalFolderButton.userHasGroupFolders ? openLocalFolderButton.toggleMenuOpen() : UserModel.openCurrentAccountLocalFolder() + + onFolderEntryTriggered: isGroupFolder ? UserModel.openCurrentAccountFolderFromTrayInfo(fullFolderPath) : UserModel.openCurrentAccountLocalFolder() + + Accessible.role: Accessible.Graphic + Accessible.name: qsTr("Open local or group folders") + Accessible.onPressAction: openLocalFolderButton.userHasGroupFolders ? openLocalFolderButton.toggleMenuOpen() : UserModel.openCurrentAccountLocalFolder() + } + } +} // Rectangle trayWindowHeaderBackground diff --git a/src/gui/SesComponents/syncdirvalidation.cpp b/src/gui/SesComponents/syncdirvalidation.cpp new file mode 100644 index 0000000000000..3357866fb897f --- /dev/null +++ b/src/gui/SesComponents/syncdirvalidation.cpp @@ -0,0 +1,53 @@ +#include "syncdirvalidation.h" +#include +#include +#include "logger.h" + +#ifdef Q_OS_WIN +bool SyncDirValidator::isValidDir() { + QString appDataPath = SyncDirValidator::appDataPath().replace("/", QDir::separator()); + QStringList pathComponents = _path.replace("/", QDir::separator()).split(QDir::separator(), Qt::SkipEmptyParts); + QStringList appDataPathComponents = appDataPath.split(QDir::separator(), Qt::SkipEmptyParts); + /* + If path is shorter than appDataPath and one path component is different, then path cannot be a real subset and is sowith valid + If appDataPath is shorter than path, we need to check, if the last appDataPath component is different from the related path component, then path is valid. + Otherwise path is a subpath from appDataPath and invalid + */ + for(int i = 0; i < qMin(pathComponents.size(), appDataPathComponents.size()); i++) { + if(pathComponents[i] != appDataPathComponents[i]) { + return true; + } + } + return false; + +} + +QString SyncDirValidator::message() { + return QObject::tr("The directory %1 cannot be part of your sync directory. Please choose another folder.").arg(_path.replace("/", QDir::separator())); +} + +QString SyncDirValidator::appDataPath() { + //Path: AppData/Roaming/ + QString appDataRoamingApplicationNamePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + QDir appDataRoamingApplicationNameDir(appDataRoamingApplicationNamePath); + appDataRoamingApplicationNameDir.cdUp(); + appDataRoamingApplicationNameDir.cdUp(); + QString appDataPath = appDataRoamingApplicationNameDir.absolutePath(); + return appDataPath; +} + +#else + +bool SyncDirValidator::isValidDir() { + return true; +} + +QString SyncDirValidator::message() { + return ""; +} + +QString SyncDirValidator::appDataPath() { + return ""; +} + +#endif \ No newline at end of file diff --git a/src/gui/SesComponents/syncdirvalidation.h b/src/gui/SesComponents/syncdirvalidation.h new file mode 100644 index 0000000000000..a91812da85775 --- /dev/null +++ b/src/gui/SesComponents/syncdirvalidation.h @@ -0,0 +1,19 @@ +#ifndef SYNCDIRVALIDATION_H +#define SYNCDIRVALIDATION_H + +#include + +class SyncDirValidator { +public: + SyncDirValidator(const QString &path) : _path(path) {} + + bool isValidDir(); + QString message(); + +private: + QString appDataPath(); + QString _path; +}; + + +#endif // SYNCDIRVALIDATION_H \ No newline at end of file diff --git a/src/gui/UserStatusMessageView.qml b/src/gui/UserStatusMessageView.qml index 965f2fe0e31b3..511326b8a383c 100644 --- a/src/gui/UserStatusMessageView.qml +++ b/src/gui/UserStatusMessageView.qml @@ -9,7 +9,7 @@ import QtQuick.Layouts import QtQuick.Controls import QtQuick.Window -import com.nextcloud.desktopclient as NC +import com.ionos.hidrivenext.desktopclient as NC import Style import "./tray" diff --git a/src/gui/UserStatusSelectorPage.qml b/src/gui/UserStatusSelectorPage.qml index 0f297f9796ea0..59cf7f9637da5 100644 --- a/src/gui/UserStatusSelectorPage.qml +++ b/src/gui/UserStatusSelectorPage.qml @@ -8,7 +8,7 @@ import QtQuick.Controls import QtQuick.Layouts import Style -import com.nextcloud.desktopclient as NC +import com.ionos.hidrivenext.desktopclient as NC Page { id: page diff --git a/src/gui/UserStatusSetStatusView.qml b/src/gui/UserStatusSetStatusView.qml index abff60d1e16a5..04bc304956c56 100644 --- a/src/gui/UserStatusSetStatusView.qml +++ b/src/gui/UserStatusSetStatusView.qml @@ -7,7 +7,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls -import com.nextcloud.desktopclient as NC +import com.ionos.hidrivenext.desktopclient as NC import Style import "./tray" diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index 171c850e79c90..671516e6f84b9 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -12,6 +12,9 @@ #include "ui_accountsettings.h" #include "theme.h" +#include "whitelabeltheme.h" +#include "buttonstyle.h" +#include "account.h" #include "foldercreationdialog.h" #include "folderman.h" #include "folderwizard.h" @@ -57,8 +60,6 @@ #include "macOS/fileprovider.h" #endif -#include "account.h" - namespace { constexpr auto propertyFolder = "folder"; constexpr auto propertyPath = "path"; @@ -75,15 +76,13 @@ class AccountSettings; Q_LOGGING_CATEGORY(lcAccountSettings, "nextcloud.gui.account.settings", QtInfoMsg) -static const char progressBarStyleC[] = - "QProgressBar {" - "border: 1px solid grey;" - "border-radius: 5px;" - "text-align: center;" - "}" - "QProgressBar::chunk {" - "background-color: %1; width: 1px;" - "}"; +const QString progressBarStyle() +{ + return QStringLiteral( + "QProgressBar::horizontal { border: 1px solid grey; border-radius: 5px; text-align: center; background-color: %1; }" + "QProgressBar::chunk { background-color: %2; width: 1px; }" + ); +} void showEnableE2eeWithVirtualFilesWarningDialog(std::function onAccept) { @@ -156,7 +155,7 @@ class MouseCursorChanger : public QObject const auto index = folderList->indexAt(pos); if (model->classify(index) == FolderStatusModel::RootFolder && (FolderStatusDelegate::errorsListRect(folderList->visualRect(index)).contains(pos) || - FolderStatusDelegate::optionsButtonRect(folderList->visualRect(index),folderList->layoutDirection()).contains(pos))) { + FolderStatusDelegate::optionsButtonRect(folderList->visualRect(index), folderList->layoutDirection()).contains(pos))) { shape = Qt::PointingHandCursor; } folderList->setCursor(shape); @@ -173,6 +172,8 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) , _userInfo(accountState, false, true) { _ui->setupUi(this); + this->setAutoFillBackground(true); + setPalette(QPalette(QPalette::Window, WLTheme.dialogBackgroundColor())); _model->setAccountState(_accountState); _model->setParent(this); @@ -182,6 +183,8 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) // Connect styleChanged events to our widgets, so they can adapt (Dark-/Light-Mode switching) connect(this, &AccountSettings::styleChanged, delegate, &FolderStatusDelegate::slotStyleChanged); + _ui->_folderList->setFont(WLTheme.settingsFontDefault()); + _ui->_folderList->header()->hide(); _ui->_folderList->setItemDelegate(delegate); _ui->_folderList->setModel(_model); @@ -202,6 +205,16 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) fpSettingsLayout->setContentsMargins(0, 0, 0, 0); fpSettingsLayout->addWidget(fpSettingsWidget); fileProviderTab->setLayout(fpSettingsLayout); + + _ui->tabWidget->setStyleSheet(QStringLiteral("QTabWidget::pane { background-color: %1; }").arg(WLTheme.white())); + _ui->tabWidget->tabBar()->setStyleSheet("QTabBar::tab {\ + color: #000000;\ + }\ + QTabBar::tab:selected {\ + color: #ffffff;\ + }"); + } else { + disguiseTabWidget(); } #else const auto tabWidget = _ui->tabWidget; @@ -219,6 +232,15 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) connectionSettingsLayout->addWidget(networkSettings); connectionSettingsTab->setLayout(connectionSettingsLayout); + const auto connectionSettingsTabIndex = _ui->tabWidget->indexOf(connectionSettingsTab); + if(connectionSettingsTabIndex >= 0){ + _ui->tabWidget->removeTab(connectionSettingsTabIndex); + } + _ui->tabWidget->setCurrentIndex(0); +#ifndef BUILD_FILE_PROVIDER_MODULE + _ui->tabWidget->tabBar()->hide(); +#endif + const auto mouseCursorChanger = new MouseCursorChanger(this); mouseCursorChanger->folderList = _ui->_folderList; mouseCursorChanger->model = _model; @@ -226,6 +248,11 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) _ui->_folderList->setAttribute(Qt::WA_Hover, true); _ui->_folderList->installEventFilter(mouseCursorChanger); +#ifdef Q_OS_MAC + _ui->expandMemoryButton->setAutoDefault(false); + _ui->expandMemoryButton->setFocusPolicy(Qt::NoFocus); +#endif + connect(this, &AccountSettings::removeAccountFolders, AccountManager::instance(), &AccountManager::removeAccountFolders); connect(_ui->_folderList, &QWidget::customContextMenuRequested, @@ -268,12 +295,9 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) connect(FolderMan::instance(), &FolderMan::folderListChanged, _model, &FolderStatusModel::resetFolders); connect(this, &AccountSettings::folderChanged, _model, &FolderStatusModel::resetFolders); - - // quotaProgressBar style now set in customizeStyle() - /*QColor color = palette().highlight().color(); - _ui->quotaProgressBar->setStyleSheet(QString::fromLatin1(progressBarStyleC).arg(color.name()));*/ - // Connect E2E stuff + _ui->encryptionMessage->setVisible(false); + _ui->encryptionMessage->hide(); if (_accountState->isConnected()) { setupE2eEncryption(); } else { @@ -289,6 +313,8 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) connect(&_userInfo, &UserInfo::quotaUpdated, this, &AccountSettings::slotUpdateQuota); + connect(_ui->expandMemoryButton, &QAbstractButton::clicked, this, &AccountSettings::slotExpandMemoryClicked); + customizeStyle(); connect(_accountState->account()->e2e(), &ClientSideEncryption::startingDiscoveryEncryptionUsbToken, @@ -299,6 +325,7 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) void AccountSettings::slotE2eEncryptionMnemonicReady() { + return; // E2E encryption message disabled const auto actionDisableEncryption = addActionToEncryptionMessage(tr("Forget encryption setup"), e2EeUiActionForgetEncryptionId); connect(actionDisableEncryption, &QAction::triggered, this, [this] { forgetEncryptionOnDeviceForAccount(_accountState->account()); @@ -555,10 +582,12 @@ void AccountSettings::openIgnoredFilesDialog(const QString & absFolderPath) const QString ignoreFile{absFolderPath + ".sync-exclude.lst"}; const auto layout = new QVBoxLayout(); const auto ignoreListWidget = new IgnoreListTableWidget(this); + ignoreListWidget->setFont(WLTheme.settingsFont()); ignoreListWidget->readIgnoreFile(ignoreFile); layout->addWidget(ignoreListWidget); const auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + customizeButtonBox(buttonBox); layout->addWidget(buttonBox); const auto dialog = new QDialog(); @@ -572,9 +601,28 @@ void AccountSettings::openIgnoredFilesDialog(const QString & absFolderPath) }); connect(buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::close); + dialog->setPalette(QPalette(QPalette::Window, WLTheme.white())); + dialog->setWindowFlag(Qt::WindowContextHelpButtonHint, false); + dialog->open(); } +void AccountSettings::customizeButtonBox(QDialogButtonBox *buttonBox){ + buttonBox->layout()->setSpacing(16); + buttonBox->setContentsMargins(0,0,11,10); + + const auto okButton = buttonBox->button(QDialogButtonBox::Ok); + + okButton->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); + okButton->setMinimumSize(80, 40); + + buttonBox->button(QDialogButtonBox::Cancel)->setMinimumSize(80, 40); + +#if defined(Q_OS_MAC) + buttonBox->layout()->setSpacing(32); +#endif +} + void AccountSettings::slotSubfolderContextMenuRequested(const QModelIndex& index, const QPoint& pos) { Q_UNUSED(pos); @@ -637,11 +685,63 @@ void AccountSettings::slotSubfolderContextMenuRequested(const QModelIndex& index ac = availabilityMenu->addAction(Utility::vfsFreeSpaceActionText()); connect(ac, &QAction::triggered, this, [this, folder, path] { slotSetSubFolderAvailability(folder, path, PinState::OnlineOnly); }); + + styleCustomContextMenu(availabilityMenu); } + styleCustomContextMenu(&menu); + menu.exec(QCursor::pos()); } +void AccountSettings::styleCustomContextMenu(QMenu *menu) const +{ + menu->setWindowFlags(menu->windowFlags() | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint); + + menu->setAttribute(Qt::WA_TranslucentBackground); + + menu->setStyleSheet( + QStringLiteral( + "QMenu {" + "background-color: %1; " + "border: 1px solid %2; " + "padding: 15px; " + "border-radius: %7; " + "font-family: %8; " + "font-size: %9; " + "font-weight: %10; " + "}" + "QMenu::item {" + "background-color: transparent;" + "padding: 16px 18px; " + "color: %3; " + "border-radius: 8px; " + "}" + "QMenu::item:selected {" + "background-color: %5; " + "color: %3; " + "border-radius: 8px; " + "}" + "QMenu::item:pressed {" + "background-color: %6; " + "color: %4; " + "border-radius: 8px; " + "}" + ).arg( + WLTheme.white(), + WLTheme.menuBorderColor(), + WLTheme.menuTextColor(), + WLTheme.menuPressedTextColor(), + WLTheme.menuSelectedItemColor(), + WLTheme.menuPressedItemColor(), + WLTheme.menuBorderRadius(), + WLTheme.contextMenuFont(), + WLTheme.settingsTextSize(), + WLTheme.settingsTextWeight() + ) + ); +} + void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos) { const auto treeView = _ui->_folderList; @@ -672,6 +772,13 @@ void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos) const auto menu = new QMenu(treeView); + connect(menu, &QMenu::aboutToHide, [treeView, index]() { + auto* delegate = qobject_cast(treeView->itemDelegate(index)); + delegate->MousePos = QPoint(-1, -1); + treeView->update(); + }); + + menu->setAttribute(Qt::WA_DeleteOnClose); auto ac = menu->addAction(tr("Open folder")); @@ -718,6 +825,8 @@ void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos) ac = menu->addAction(tr("Disable virtual file support …")); connect(ac, &QAction::triggered, this, &AccountSettings::slotDisableVfsCurrentFolder); ac->setDisabled(Theme::instance()->enforceVirtualFilesSyncFolder()); + + styleCustomContextMenu(availabilityMenu); } if (const auto mode = bestAvailableVfsMode(); @@ -732,6 +841,7 @@ void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos) } } + styleCustomContextMenu(menu); menu->popup(treeView->mapToGlobal(pos)); } @@ -740,16 +850,6 @@ void AccountSettings::slotFolderListClicked(const QModelIndex &indx) { if (indx.data(FolderStatusDelegate::AddButton).toBool()) { // "Add Folder Sync Connection" - const auto treeView = _ui->_folderList; - const auto pos = treeView->mapFromGlobal(QCursor::pos()); - QStyleOptionViewItem opt; - opt.initFrom(treeView); - const auto btnRect = treeView->visualRect(indx); - const auto btnSize = treeView->itemDelegateForIndex(indx)->sizeHint(opt, indx); - const auto actual = QStyle::visualRect(opt.direction, btnRect, QRect(btnRect.topLeft(), btnSize)); - if (!actual.contains(pos)) { - return; - } if (indx.flags() & Qt::ItemIsEnabled) { slotAddFolder(); @@ -884,8 +984,22 @@ void AccountSettings::slotRemoveCurrentFolder() .arg(shortGuiLocalPath), QMessageBox::NoButton, this); + + messageBox->setStyleSheet( + QStringLiteral("QMessageBox QLabel { %1; } QDialog { background-color: %2; }").arg( + WLTheme.fontConfigurationCss( + WLTheme.settingsFont(), + WLTheme.settingsTextSize(), + WLTheme.settingsTextWeight(), + WLTheme.titleColor() + ), + WLTheme.dialogBackgroundColor() + ) + ); + messageBox->setAttribute(Qt::WA_DeleteOnClose); const auto yesButton = messageBox->addButton(tr("Remove Folder Sync Connection"), QMessageBox::YesRole); + yesButton->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); messageBox->addButton(tr("Cancel"), QMessageBox::NoRole); connect(messageBox, &QMessageBox::finished, this, [messageBox, yesButton, folder, row, this]{ if (messageBox->clickedButton() == yesButton) { @@ -1005,7 +1119,22 @@ void AccountSettings::slotDisableVfsCurrentFolder() "will become available again." "\n\n" "This action will abort any currently running synchronization.")); + + msgBox->setStyleSheet(QStringLiteral( + "QMessageBox QLabel { %1 background-color: %2; }").arg( + WLTheme.fontConfigurationCss( + WLTheme.settingsFont(), + WLTheme.settingsTextSize(), + WLTheme.settingsTextWeight(), + WLTheme.titleColor() + ), + WLTheme.white() + ) + ); + const auto acceptButton = msgBox->addButton(tr("Disable support"), QMessageBox::AcceptRole); + acceptButton->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); + msgBox->addButton(tr("Cancel"), QMessageBox::RejectRole); connect(msgBox, &QMessageBox::finished, msgBox, [this, msgBox, folder, acceptButton] { msgBox->deleteLater(); @@ -1154,6 +1283,7 @@ void AccountSettings::migrateCertificateForAccount(const AccountPtr &account) void AccountSettings::showConnectionLabel(const QString &message, QStringList errors) { + #ifndef IONOS_BUILD const auto errStyle = QLatin1String("color:#ffffff; background-color:#bb4d4d;padding:5px;" "border-width: 1px; border-style: solid; border-color: #aaaaaa;" "border-radius:5px;"); @@ -1167,12 +1297,30 @@ void AccountSettings::showConnectionLabel(const QString &message, QStringList er errors.prepend(message); auto userFriendlyMsg = errors.join(QLatin1String("
")); qCDebug(lcAccountSettings) << userFriendlyMsg; - Theme::replaceLinkColorString(userFriendlyMsg, QColor(0xc1c8e6)); + Theme::replaceLinkColorString(userFriendlyMsg, QColor("#c1c8e6")); + _ui->connectLabel->setText(userFriendlyMsg); + _ui->connectLabel->setToolTip({}); + _ui->connectLabel->setStyleSheet(errStyle); + } + _ui->accountStatus->setVisible(false); + #else + + _ui->accountStatus->setVisible(false); + + const auto errStyle = QLatin1String("color:#ffffff; background-color:#bb4d4d;padding:5px;" + "border-width: 1px; border-style: solid; border-color: #aaaaaa;" + "border-radius:5px;"); + if (!errors.isEmpty()) { + _ui->accountStatus->setVisible(true); + errors.prepend(message); + auto userFriendlyMsg = errors.join(QLatin1String("
")); + qCDebug(lcAccountSettings) << userFriendlyMsg; + Theme::replaceLinkColorString(userFriendlyMsg, QColor("#c1c8e6")); _ui->connectLabel->setText(userFriendlyMsg); _ui->connectLabel->setToolTip({}); _ui->connectLabel->setStyleSheet(errStyle); } - _ui->accountStatus->setVisible(!message.isEmpty()); + #endif } void AccountSettings::slotEnableCurrentFolder(bool terminate) @@ -1199,9 +1347,30 @@ void AccountSettings::slotEnableCurrentFolder(bool terminate) QMessageBox::Yes | QMessageBox::No, this); msgbox->setAttribute(Qt::WA_DeleteOnClose); msgbox->setDefaultButton(QMessageBox::Yes); + msgbox->defaultButton()->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); + + QHBoxLayout *buttonLayout = msgbox->findChild(); + buttonLayout->setSpacing(8); + +#ifdef Q_OS_MAC + buttonLayout->setSpacing(24); +#endif + connect(msgbox, &QMessageBox::accepted, this, [this]{ slotEnableCurrentFolder(true); }); + + msgbox->setStyleSheet( + QStringLiteral("QMessageBox QLabel { %1; }").arg( + WLTheme.fontConfigurationCss( + WLTheme.settingsFont(), + WLTheme.settingsTextSize(), + WLTheme.settingsTextWeight(), + WLTheme.titleColor() + ) + ) + ); + msgbox->open(); return; } @@ -1271,6 +1440,7 @@ void AccountSettings::slotUpdateQuota(qint64 total, qint64 used) _ui->quotaInfoLabel->setText(tr("%1 of %2 in use").arg(usedStr, totalStr)); _ui->quotaInfoLabel->setToolTip(toolTip); _ui->quotaProgressBar->setToolTip(toolTip); + _ui->quotaInfo2Label->setText(tr("Storage space %1% occupied").arg(percentStr)); } else { _ui->quotaProgressBar->setVisible(false); _ui->quotaInfoLabel->setToolTip({}); @@ -1385,6 +1555,8 @@ void AccountSettings::slotAccountStateChanged() void AccountSettings::checkClientSideEncryptionState() { + return; // E2E encryption message disabled + /* TODO: We should probably do something better here. * Verify if the user has a private key already uploaded to the server, * if it has, do not offer to create one. @@ -1446,6 +1618,11 @@ void AccountSettings::slotHideSelectiveSyncWidget() _ui->selectiveSyncLabel->hide(); } +void AccountSettings::slotExpandMemoryClicked() +{ + QDesktopServices::openUrl(QUrl(tr("https://wl.hidrive.com/easy/0057"))); +} + void AccountSettings::slotSelectiveSyncChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) @@ -1476,6 +1653,7 @@ void AccountSettings::slotSelectiveSyncChanged(const QModelIndex &topLeft, } _ui->selectiveSyncApply->setEnabled(true); + _ui->selectiveSyncApply->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); _ui->selectiveSyncButtons->setVisible(true); if (shouldBeVisible != wasVisible) { @@ -1628,9 +1806,9 @@ void AccountSettings::refreshSelectiveSyncStatus() QString infoString; if (!unsyncedFoldersString.isEmpty()) { - infoString += !cfg.confirmExternalStorage() ? tr("There are folders that were not synchronized because they are too big: ") - : !cfg.newBigFolderSizeLimit().first ? tr("There are folders that were not synchronized because they are external storages: ") - : tr("There are folders that were not synchronized because they are too big or external storages: "); + infoString += !cfg.confirmExternalStorage() ? tr("There are folders that were not synchronized because they are too big:") + " " + : !cfg.newBigFolderSizeLimit().first ? tr("There are folders that were not synchronized because they are external storages:") + " " + : tr("There are folders that were not synchronized because they are too big or external storages:") + " "; infoString += unsyncedFoldersString; } @@ -1676,14 +1854,58 @@ void AccountSettings::customizeStyle() auto msg = _ui->connectLabel->text(); Theme::replaceLinkColorStringBackgroundAware(msg); _ui->connectLabel->setText(msg); - + const auto color = palette().highlight().color(); - _ui->quotaProgressBar->setStyleSheet(QString::fromLatin1(progressBarStyleC).arg(color.name())); + const auto toolTipStyle = QStringLiteral("QToolTip { color: %1; background-color: %2; border: 1px solid %1; }") + .arg(WLTheme.titleColor(), WLTheme.dialogBackgroundColor()); + _ui->quotaProgressBar->setStyleSheet(progressBarStyle().arg(WLTheme.dialogBackgroundColor(), WLTheme.quotaProgressColor()) + toolTipStyle); + + _ui->quotaInfoLabel->setStyleSheet( + QStringLiteral("QLabel { %1 } %2").arg( + WLTheme.fontConfigurationCss( + WLTheme.settingsFont(), + WLTheme.settingsTextSize(), + WLTheme.settingsTitleWeight600(), + WLTheme.titleColor() + ), + toolTipStyle + ) + ); + + _ui->quotaInfo2Label->setStyleSheet( + WLTheme.fontConfigurationCss( + WLTheme.settingsFont(), + WLTheme.settingsSmallTextSize(), + WLTheme.settingsTextWeight(), + WLTheme.titleColor() + ) + ); + + _ui->_folderList->setStyleSheet( + QStringLiteral("background: %1; %2;").arg( + WLTheme.white(), + WLTheme.fontConfigurationCss( + WLTheme.settingsFont(), + WLTheme.settingsTextSize(), + WLTheme.settingsTextWeight(), + WLTheme.titleColor() + ) + ) + ); + +#if defined(Q_OS_MAC) + _ui->selectiveSyncLabel->setStyleSheet(QString("color: %1;").arg(WLTheme.black())); + _ui->horizontalLayout->setSpacing(16); +#endif + } void AccountSettings::setupE2eEncryption() { - connect(_accountState->account()->e2e(), &ClientSideEncryption::initializationFinished, this, &AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync); + connect(_accountState->account()->e2e(), + &ClientSideEncryption::initializationFinished, + this, + &AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync); if (_accountState->account()->e2e()->isInitialized()) { slotE2eEncryptionMnemonicReady(); @@ -1758,6 +1980,14 @@ void AccountSettings::setupE2eEncryptionMessage() connect(actionSetupE2e, &QAction::triggered, this, &AccountSettings::slotE2eEncryptionGenerateKeys); } +void AccountSettings::disguiseTabWidget() const +{ + // Ensure all elements of the tab widget are hidden. + // Document mode lets the child view take up the whole view. + _ui->tabWidget->setDocumentMode(true); + _ui->tabWidget->tabBar()->hide(); +} + } // namespace OCC #include "accountsettings.moc" diff --git a/src/gui/accountsettings.h b/src/gui/accountsettings.h index 6db64c3f7e229..6fde0dcb95b7e 100644 --- a/src/gui/accountsettings.h +++ b/src/gui/accountsettings.h @@ -12,6 +12,7 @@ #include #include #include +#include #include "folder.h" #include "userinfo.h" @@ -126,8 +127,10 @@ private slots: void forgetE2eEncryption(); void checkClientSideEncryptionState(); void removeActionFromEncryptionMessage(const QString &actionId); + void slotExpandMemoryClicked(); private: + void styleCustomContextMenu(QMenu *menu) const; bool event(QEvent *) override; QAction *addActionToEncryptionMessage(const QString &actionTitle, const QString &actionId); @@ -136,6 +139,10 @@ private slots: /// Returns the alias of the selected folder, empty string if none [[nodiscard]] QString selectedFolderAlias() const; + void disguiseTabWidget() const; + + void customizeButtonBox(QDialogButtonBox *buttonBox); + Ui::AccountSettings *_ui; FolderStatusModel *_model; diff --git a/src/gui/accountsettings.ui b/src/gui/accountsettings.ui index 2db413221b169..88e9c17168390 100644 --- a/src/gui/accountsettings.ui +++ b/src/gui/accountsettings.ui @@ -13,9 +13,21 @@ Form - + + 32 + + + 32 + + + 32 + + + 32 + + - + @@ -38,6 +50,8 @@ + + @@ -52,7 +66,7 @@ 16777215 - 7 + 8 @@ -62,6 +76,43 @@ -1 + false + + + + + + + + 0 + 0 + + + + Expand Memory + + + + + + + + + + 0 + 0 + + + + + + + Storage space: … + + + Qt::PlainText + + false @@ -263,7 +314,7 @@ QTabWidget::Rounded - 0 + 1 diff --git a/src/gui/addcertificatedialog.cpp b/src/gui/addcertificatedialog.cpp index 4721e2cccebe8..c21335bee41a9 100644 --- a/src/gui/addcertificatedialog.cpp +++ b/src/gui/addcertificatedialog.cpp @@ -16,6 +16,7 @@ AddCertificateDialog::AddCertificateDialog(QWidget *parent) { ui->setupUi(this); ui->labelErrorCertif->setText(""); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); } AddCertificateDialog::~AddCertificateDialog() diff --git a/src/gui/application.cpp b/src/gui/application.cpp index 1454d93040963..6a4322ced650a 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -29,15 +29,20 @@ #include "theme.h" #include "updatechannel.h" +#include "ga4/datacollectionwrapper.h" + #if defined(BUILD_UPDATER) #include "updater/ocupdater.h" #endif #include "owncloudsetupwizard.h" +#include "sesstyle.h" #include "version.h" #include "csync_exclude.h" #include "common/vfs.h" +#include + #include "config.h" #if defined(Q_OS_WIN) @@ -387,6 +392,10 @@ Application::Application(int &argc, char **argv) connect(this, &SharedTools::QtSingleApplication::messageReceived, this, &Application::slotParseMessage); +#ifdef IONOS_BUILD + setStyle(new sesStyle(QStyleFactory::create("WindowsVista"))); +#endif + // create accounts and folders from a legacy desktop client or from the current config file setupAccountsAndFolders(); @@ -485,6 +494,30 @@ Application::~Application() AccountManager::instance()->shutdown(); } +void Application::startTracking() +{ + DataCollectionWrapper dcw; + dcw.initDataCollection(); + AccountPtr account = AccountManager::instance()->accounts().first()->account(); + QByteArray byteArray = account->credentials()->user().toUtf8(); // Convert the input string to a byte array + QByteArray hash = QCryptographicHash::hash(byteArray, QCryptographicHash::Sha256); // Perform the hash + + ConfigFile cfg; + dcw.setSendData(cfg.sendData()); + dcw.setAccount(account); + + dcw.setClientID(hash.toHex()); + dcw.login(); +} + +void Application::stopTracking() +{ + DataCollectionWrapper dcw; + dcw.accountRemoved(); + dcw.setClientID(QString()); + dcw.setAccount(nullptr); +} + void Application::setupAccountsAndFolders() { _folderManager.reset(new FolderMan); @@ -636,6 +669,14 @@ void Application::slotAccountStateRemoved(AccountState *accountState) _folderManager.data(), &FolderMan::slotServerVersionChanged); } + if(AccountManager::instance()->accounts().isEmpty()) { + stopTracking(); + } + else + { + startTracking(); + } + // if there is no more account, show the wizard. if (_gui && AccountManager::instance()->accounts().isEmpty()) { // allow to add a new account if there is non any more. Always think @@ -657,6 +698,8 @@ void Application::slotAccountStateAdded(AccountState *accountState) connect(accountState->account().data(), &Account::serverVersionChanged, _folderManager.data(), &FolderMan::slotServerVersionChanged); + startTracking(); + _gui->slotTrayMessageIfServerUnsupported(accountState->account()); } @@ -741,9 +784,9 @@ void Application::setupLogging() logger->setLogDebug(true); #endif - logger->enterNextLogFile(QStringLiteral("nextcloud.log"), OCC::Logger::LogType::Log); + logger->enterNextLogFile(QStringLiteral("hidrivenext.log"), OCC::Logger::LogType::Log); logger->enterNextLogFile(QStringLiteral("permanent_delete.log"), OCC::Logger::LogType::DeleteLog); - + qCInfo(lcApplication) << "##################" << _theme->appName() << "locale:" << QLocale::system().name() << "ui_lang:" << property("ui_lang") diff --git a/src/gui/application.h b/src/gui/application.h index caba9df0e893f..970d34e0bffda 100644 --- a/src/gui/application.h +++ b/src/gui/application.h @@ -97,7 +97,8 @@ protected slots: private: void setHelp(); - + void startTracking(); + void stopTracking(); void handleEditLocallyFromOptions(); AccountManager::AccountsRestoreResult restoreLegacyAccount(); diff --git a/src/gui/authenticationdialog.cpp b/src/gui/authenticationdialog.cpp index de4a1e66cadb1..b371ca5de4a45 100644 --- a/src/gui/authenticationdialog.cpp +++ b/src/gui/authenticationdialog.cpp @@ -35,6 +35,7 @@ AuthenticationDialog::AuthenticationDialog(const QString &realm, const QString & connect(box, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(box, &QDialogButtonBox::rejected, this, &QDialog::reject); lay->addWidget(box); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); } QString AuthenticationDialog::user() const diff --git a/src/gui/basetheme.h b/src/gui/basetheme.h new file mode 100644 index 0000000000000..1d8ff5fe0d9fd --- /dev/null +++ b/src/gui/basetheme.h @@ -0,0 +1,530 @@ +#ifndef _BASETHEME_H +#define _BASETHEME_H + +#include +#include +#include "theme.h" + +namespace OCC { + +class BaseTheme : public QObject{ + Q_OBJECT + Q_PROPERTY(QString dialogBackgroundColor READ dialogBackgroundColor CONSTANT) + Q_PROPERTY(QString trayFontColor READ trayFontColor CONSTANT) + Q_PROPERTY(QString trayBorderColor READ trayBorderColor CONSTANT) + Q_PROPERTY(QString trayInputFieldBorderColor READ trayInputFieldBorderColor CONSTANT) + Q_PROPERTY(QString trayBackgroundColor READ trayBackgroundColor CONSTANT) + Q_PROPERTY(QString iconDarkColor READ iconDarkColor CONSTANT) + Q_PROPERTY(QString buttonIconColor READ buttonIconColor CONSTANT) + Q_PROPERTY(QString buttonHoveredColor READ buttonHoveredColor CONSTANT) + Q_PROPERTY(QString buttonPressedColor READ buttonPressedColor CONSTANT) + Q_PROPERTY(QString toolButtonHoveredColor READ toolButtonHoveredColor CONSTANT) + Q_PROPERTY(QString toolButtonPressedColor READ toolButtonPressedColor CONSTANT) + Q_PROPERTY(QString pillButtonPrimaryColor READ pillButtonPrimaryColor CONSTANT) + Q_PROPERTY(QString pillButtonSecondaryColor READ pillButtonSecondaryColor CONSTANT) + Q_PROPERTY(QString pillButtonBorderColor READ pillButtonBorderColor CONSTANT) + Q_PROPERTY(QString clipboardBackgroundColor READ clipboardBackgroundColor CONSTANT) + Q_PROPERTY(QString trayErrorBorderColor READ trayErrorBorderColor CONSTANT) + Q_PROPERTY(QString trayErrorTextColor READ trayErrorTextColor CONSTANT) + Q_PROPERTY(QString sesHeaderLogoIcon READ sesHeaderLogoIcon CONSTANT) + Q_PROPERTY(QString websiteIcon READ websiteIcon CONSTANT) + Q_PROPERTY(QString folderIcon READ folderIcon CONSTANT) + Q_PROPERTY(QString moreIcon READ moreIcon CONSTANT) + Q_PROPERTY(QString moreHoverIcon READ moreHoverIcon CONSTANT) + Q_PROPERTY(QString avatarIcon READ avatarIcon CONSTANT) + Q_PROPERTY(QString plusIcon READ plusIcon CONSTANT) + Q_PROPERTY(QString lightPlusIcon READ lightPlusIcon CONSTANT) + Q_PROPERTY(QString quitIcon READ quitIcon CONSTANT) + Q_PROPERTY(QString resumeIcon READ resumeIcon CONSTANT) + Q_PROPERTY(QString pauseIcon READ pauseIcon CONSTANT) + Q_PROPERTY(QString settingsIcon READ settingsIcon CONSTANT) + Q_PROPERTY(QString logoutIcon READ logoutIcon CONSTANT) + Q_PROPERTY(QString deleteIcon READ deleteIcon CONSTANT) + Q_PROPERTY(QString clipboardIcon READ clipboardIcon CONSTANT) + Q_PROPERTY(QString lightClipboardIcon READ lightClipboardIcon CONSTANT) + Q_PROPERTY(QString chevronIcon READ chevronIcon CONSTANT) + Q_PROPERTY(QString syncSuccessIcon READ syncSuccessIcon CONSTANT) + Q_PROPERTY(QString syncErrorIcon READ syncErrorIcon CONSTANT) + Q_PROPERTY(QString syncOfflineIcon READ syncOfflineIcon CONSTANT) + Q_PROPERTY(QString snackbarErrorIcon READ snackbarErrorIcon CONSTANT) + Q_PROPERTY(QString activityIcon READ activityIcon CONSTANT) + +public: + + virtual ~BaseTheme() = default; + + virtual QString themePrefix(QString context = "qml") const { + if (context == "qml") { + return QString("qrc:///client/theme/"); + } + return QString(Theme::themePrefix); + } + + virtual QString additionalThemePrefix() const { return QStringLiteral(""); } + + virtual QString avatarIcon(QString context = "qml") const { + return themePrefix(context) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-settingsAvatar.svg"); + } + + virtual QString roundAvatarIcon() const { + return QString(Theme::themePrefix) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-settingsAvatarRound.svg"); + } + + virtual QString folderIcon(QString context = "qml") const { + return themePrefix(context) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-folderIcon.svg"); + } + + virtual QString syncArrows() const { + return QString(Theme::themePrefix) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-syncArrows.svg"); + } + + virtual QString questionCircleIcon() const { + return QString(Theme::themePrefix) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-questionMark.svg"); + } + + virtual QString liveBackupPlusIcon() const { + return QString(Theme::themePrefix) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-addlivebackup.svg"); + } + + virtual QString websiteIcon(QString context = "qml") const { + return themePrefix(context) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-website.svg"); + } + + virtual QString moreIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-more.svg"); + } + + virtual QString moreHoverIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-more-hover.svg"); + } + + virtual QString plusIcon(QString context = "qml") const { + return themePrefix(context) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-darkPlus.svg"); + } + + virtual QString lightPlusIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-lightPlus.svg"); + } + + virtual QString quitIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-accountQuit.svg"); + } + + virtual QString resumeIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-accountResume.svg"); + } + + virtual QString pauseIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-accountPause.svg"); + } + + virtual QString settingsIcon(QString context = "qml") const { + return themePrefix(context) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-settings.svg"); + } + + virtual QString logoutIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-accountLogout.svg"); + } + + virtual QString sesHeaderLogoIcon() const = 0; + + virtual QString deleteIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-accountDelete.svg"); + } + + virtual QString activityDeleteIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-activityDelete.svg"); + } + + virtual QString refreshIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-refresh.svg"); + } + + virtual QString infoIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-info.svg"); + } + + virtual QString clipboardIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-clipboard.svg"); + } + + virtual QString lightClipboardIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-lightClipboard.svg"); + } + + virtual QString chevronIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-chevron.svg"); + } + + virtual QString syncSuccessIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-syncstate-success.svg"); + } + + virtual QString syncErrorIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-syncstate-error.svg"); + } + + virtual QString syncOfflineIcon(QString context = "qml") const { + return themePrefix(context) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-state-offline.svg"); + } + + virtual QString snackbarErrorIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-snackbar-error.svg"); + } + + virtual QString activityIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-activity.svg"); + } + + virtual int treeViewIconSize() const { + return 32; + } + + //Control Configuration: Sizes + virtual QString toolbarActionBorderRadius() const { + return "8px"; + } + + virtual QString toolbarSideMargin() const { + return "10px"; + } + + virtual int toolbarIconSize() const { + return 24; + } + + virtual QString buttonRadius() const { + return "4px"; + } + + virtual int buttonRadiusInt() const { + return 4; + } + + virtual QString buttonPadding() const { + return "10px"; + } + + virtual QString smallMargin() const { + return "8"; + } + + virtual int minimalSettingsDialogWidth() const { + return 740; + } + + virtual int wizardFixedWidth() const { + return 576; + } + + virtual int wizardFixedHeight() const { + return 704; + } + + virtual int LoginPageSpacer() const { + return 45; + } + + //Font Configuration + virtual QString settingsFont() const { + return "Segoe UI"; + } + + virtual QString contextMenuFont() const { + //TODO + return ":/client/fonts/OpenSans-Regular.ttf"; + } + + virtual QString settingsSmallTextSize() const { + return "14px"; + } + + virtual int settingsTextPixel() const { + return 16; + } + + virtual QString settingsTextSize() const { + return QString::number(settingsTextPixel()) + "px"; + } + + virtual int settingsTitlePixel() const { + return 20; + } + + virtual QString settingsTitleSize() const { + return QString::number(settingsTitlePixel()) + "px"; + } + + virtual int settingsBigTitlePixel() const { + return 24; + } + + virtual QString settingsBigTitleSize() const { + return QString::number(settingsBigTitlePixel()) + "px"; + } + + virtual QString onboardingTitle() const { + return "28px"; + } + + virtual QString settingsTextWeight() const { + return "400"; + } + + virtual QString settingsTitleWeight400() const { + return "400"; + } + + virtual QString settingsTitleWeight500() const { + return "500"; + } + + virtual QString settingsTitleWeight600() const { + return "600"; + } + + virtual QFont::Weight settingsTitleWeightDemiBold() const { + return QFont::DemiBold; + } + + virtual QFont::Weight settingsTitleWeightNormal() const { + return QFont::Normal; + } + + virtual QFont settingsFontDefault() const { + QFont defaultFont(settingsFont()); + defaultFont.setPixelSize(settingsTextPixel()); + defaultFont.setWeight(settingsTitleWeightNormal()); + return defaultFont; + } + + virtual QString titleColor() const { + return "#000000"; + } + + virtual QString folderWizardSubtitleColor() const { + return "#104996"; + } + + virtual QString folderWizardPathColor() const { + return "#97A3B4"; + } + + virtual QString loginWizardFontGrey() const { + return "#616161"; + } + + virtual QString loginWizardFontLightGrey() const { + return "#BDBDBD"; + } + + virtual QString trayFontColor() const { + return "#001B41"; + } + + virtual QString trayBorderColor() const { + return "#D7D7D7"; + } + + virtual QString trayInputFieldBorderColor() const { + return "#718095"; + } + + virtual QString fontConfigurationCss(QString font, QString size, QString weight, QString color) const { + return QString("font-family: %1; font-size: %2; font-weight: %3; color: %4; ").arg( + font, + size, + weight, + color); + } + + //Colors + virtual QString settingsLinkColor() const { + return "#02306A"; + } + + virtual QString quotaProgressColor() const { + return "#308cc6"; + } + + virtual QString syncProgressColor() const { + return "#359ada"; + } + + virtual QString buttonPrimaryColor() const { + return "#0F6CBD"; + } + + virtual QString buttonSecondaryColor() const { + return "#FFFFFF"; + } + + virtual QString buttonSecondaryBorderColor() const { + return "#D1D1D1"; + } + + virtual QString buttonDisabledColor() const { + return "#F0F0F0"; + } + + virtual QString buttonPrimaryHoverColor() const { + return "#115EA3"; + } + + virtual QString buttonSecondaryHoverColor() const { + return "#F5F5F5"; + } + + virtual QString buttonPrimaryPressedColor() const { + return "#0C3B5E"; + } + + virtual QString buttonSecondaryPressedColor() const { + return "#E0E0E0"; + } + + virtual QString buttonPrimaryFocusedBorderColor() const { + return "#000000"; + } + + virtual QString buttonSecondaryFocusedBorderColor() const { + return "#000000"; + } + + virtual QString buttonDisabledFontColor() const { + return "#BDBDBD"; + } + + virtual QString pillButtonPrimaryColor() const { + return "#0B2A63"; + } + + virtual QString pillButtonSecondaryColor() const { + return "#FFFFFF"; + } + + virtual QString pillButtonBorderColor() const { + return "#0B2A63"; + } + + virtual QString clipboardBackgroundColor() const { + return "#FFFFFF"; + } + + virtual QString white() const { + return "#FFFFFF"; + } + + virtual QString black() const { + return "#000000"; + } + + virtual QString dialogBackgroundColor() const { + return "#FAFAFA"; + } + + virtual QString trayBackgroundColor() const { + return "#FFFFFF"; + } + + virtual QString menuBorderColor() const { + return "#2E4360"; + } + + virtual QString menuTextColor() const { + return "#001B41"; + } + + virtual QString menuPressedTextColor() const { + return "#001B41"; + } + + virtual QString iconDarkColor() const { + return "#001B41"; + } + + virtual QString menuSelectedItemColor() const { + return "#F4F7FA"; + } + + virtual QString menuPressedItemColor() const { + return "#F4F7FA"; + } + + virtual QString menuBorderRadius() const { + return "16px"; + } + + virtual QString buttonIconColor() const { + return "#1474C4"; + } + + virtual QString buttonIconHoverColor() const { + return "#FFFFFF"; + } + + virtual QString buttonPressedColor() const { + return "#0B2A63"; + } + + virtual QString buttonHoveredColor() const { + return "#1474C4"; + } + + virtual QString toolButtonHoveredColor() const { + return "#DBEDF8"; + } + + virtual QString toolButtonPressedColor() const { + return "#95CAEB"; + } + + virtual QString errorColor() const { + return "#FDF3F4"; + } + + virtual QString errorBorderColor() const { + return "#EEACB2"; + } + + virtual QString trayErrorBorderColor() const { + return "#F50C00"; + } + + virtual QString trayErrorTextColor() const { + return "#C80A00"; + } + + virtual QString warningBorderColor() const { + return "#F4BFAB"; + } + + virtual QString warningColor() const { + return "#FDF6F3"; + } + + virtual QString successBorderColor() const { + return "#9FD89F"; + } + + virtual QString successColor() const { + return "#F1FAF1"; + } + + virtual QString infoBorderColor() const { + return "#11C7E6"; + } + + virtual QString infoColor() const { + return "#E6F9FC"; + } + + virtual QString treeViewHoverColor() const { + return "#e5f3ff"; + } + + private: + inline static const QString _sesFolder = QStringLiteral("ses/"); +}; +} +#endif // _BASETHEME_H \ No newline at end of file diff --git a/src/gui/buttonstyle.h b/src/gui/buttonstyle.h new file mode 100644 index 0000000000000..087db1560af04 --- /dev/null +++ b/src/gui/buttonstyle.h @@ -0,0 +1,339 @@ + +#ifndef _BUTTONSTYLE_H +#define _BUTTONSTYLE_H + +#include "whitelabeltheme.h" +#include +#include + +namespace OCC{ + +enum class ButtonStyleName { + Primary, + Secondary, + MoreOptions, +}; +OCSYNC_EXPORT Q_NAMESPACE; +Q_ENUM_NS(ButtonStyleName); +} +Q_DECLARE_METATYPE(OCC::ButtonStyleName); + +namespace OCC{ +class ButtonStyle +{ +protected: + ButtonStyle() + { + qRegisterMetaType("OCC::ButtonStyleName"); + } + ~ButtonStyle() {} + +public: + + // Default + virtual QString buttonDefaultColor() const = 0; + virtual QString buttonDefaultBorderColor() const = 0; + // Hover + virtual QString buttonHoverColor() const = 0; + virtual QString buttonHoverBorderColor() const = 0; + // Pressed + virtual QString buttonPressedColor() const = 0; + virtual QString buttonPressedBorderColor() const = 0; + // Disabled + virtual QString buttonDisabledColor() const = 0; + virtual QString buttonDisabledBorderColor() const = 0; + // Focused + virtual QString buttonFocusedColor() const = 0; + virtual QString buttonFocusedBorderColor() const = 0; + // Font + virtual QString buttonDisabledFontColor() const = 0; + virtual QString buttonFontColor() const = 0; + //Icon + virtual QString buttonIconDefaultColor() const = 0; + virtual QString buttonIconHoverColor() const = 0; +}; + +class PrimaryButtonStyle : public ButtonStyle { +private: + PrimaryButtonStyle() + { + } + ~PrimaryButtonStyle() {} +public: + + PrimaryButtonStyle(PrimaryButtonStyle &other) = delete; + void operator=(const PrimaryButtonStyle &) = delete; + + static PrimaryButtonStyle& GetInstance() { + static PrimaryButtonStyle instance; + return instance; + } + + // Default + QString buttonDefaultColor() const override + { + return OCC::WLTheme.buttonPrimaryColor(); + } + + QString buttonDefaultBorderColor() const override + { + return OCC::WLTheme.buttonPrimaryColor(); + } + + //Hover + QString buttonHoverColor() const override + { + return OCC::WLTheme.buttonPrimaryHoverColor(); + } + + QString buttonHoverBorderColor() const override + { + return OCC::WLTheme.buttonPrimaryHoverColor(); + } + + // Pressed + QString buttonPressedColor() const override + { + return OCC::WLTheme.buttonPrimaryPressedColor(); + } + + QString buttonPressedBorderColor() const override + { + return OCC::WLTheme.buttonPrimaryPressedColor(); + } + + // Disabled + QString buttonDisabledColor() const override + { + return OCC::WLTheme.buttonDisabledColor(); + } + + QString buttonDisabledBorderColor() const override + { + return OCC::WLTheme.buttonDisabledColor(); + } + + // Focused + QString buttonFocusedColor() const override + { + return OCC::WLTheme.buttonPrimaryColor(); + } + + QString buttonFocusedBorderColor() const override + { + return OCC::WLTheme.buttonPrimaryFocusedBorderColor(); + } + + // Font + QString buttonDisabledFontColor() const override + { + return OCC::WLTheme.buttonDisabledFontColor(); + } + + QString buttonFontColor() const override + { + return OCC::WLTheme.white(); + } + + // Icon (Three Dots) + QString buttonIconDefaultColor() const override + { + return OCC::WLTheme.white(); + } + + QString buttonIconHoverColor() const override + { + return OCC::WLTheme.white(); + } +}; + +class SecondaryButtonStyle : public ButtonStyle { +protected: + SecondaryButtonStyle() + { + } + ~SecondaryButtonStyle() {} +public: + + SecondaryButtonStyle(SecondaryButtonStyle &other) = delete; + void operator=(const SecondaryButtonStyle &) = delete; + + static SecondaryButtonStyle& GetInstance() { + static SecondaryButtonStyle instance; + return instance; + } + + // Default + QString buttonDefaultColor() const override + { + return OCC::WLTheme.buttonSecondaryColor(); + } + + QString buttonDefaultBorderColor() const override + { + return OCC::WLTheme.buttonSecondaryBorderColor(); + } + + // Hover + QString buttonHoverColor() const override + { + return OCC::WLTheme.buttonSecondaryHoverColor(); + } + + QString buttonHoverBorderColor() const override + { + return OCC::WLTheme.buttonSecondaryBorderColor(); + } + + // Pressed + QString buttonPressedColor() const override + { + return OCC::WLTheme.buttonSecondaryPressedColor(); + } + + QString buttonPressedBorderColor() const override + { + return OCC::WLTheme.buttonSecondaryBorderColor(); + } + + // Disabled + QString buttonDisabledColor() const override + { + return OCC::WLTheme.buttonDisabledColor(); + } + + QString buttonDisabledBorderColor() const override + { + return OCC::WLTheme.buttonDisabledColor(); + } + + // Focused + QString buttonFocusedColor() const override + { + return OCC::WLTheme.white(); + } + + QString buttonFocusedBorderColor() const override + { + return OCC::WLTheme.buttonSecondaryFocusedBorderColor(); + } + + // Font + QString buttonDisabledFontColor() const override + { + return OCC::WLTheme.buttonDisabledFontColor(); + } + + QString buttonFontColor() const override + { + return OCC::WLTheme.black(); + } + + // Icon (Three Dots) + QString buttonIconDefaultColor() const override + { + return OCC::WLTheme.white(); + } + + QString buttonIconHoverColor() const override + { + return OCC::WLTheme.white(); + } +}; + +class MoreOptionsButtonStyle : public ButtonStyle { +protected: + MoreOptionsButtonStyle() + { + } + ~MoreOptionsButtonStyle() {} +public: + + MoreOptionsButtonStyle(MoreOptionsButtonStyle &other) = delete; + void operator=(const MoreOptionsButtonStyle &) = delete; + + static MoreOptionsButtonStyle& GetInstance() { + static MoreOptionsButtonStyle instance; + return instance; + } + + // Default + QString buttonDefaultColor() const override + { + return OCC::WLTheme.white(); + } + + QString buttonDefaultBorderColor() const override + { + return OCC::WLTheme.white(); + } + + // Hover + QString buttonHoverColor() const override + { + return OCC::WLTheme.buttonHoveredColor(); + } + + QString buttonHoverBorderColor() const override + { + return OCC::WLTheme.buttonHoveredColor(); + } + + // Pressed + QString buttonPressedColor() const override + { + return OCC::WLTheme.buttonPressedColor(); + } + + QString buttonPressedBorderColor() const override + { + return OCC::WLTheme.buttonPressedColor(); + } + + // Disabled + QString buttonDisabledColor() const override + { + return OCC::WLTheme.buttonDisabledColor(); + } + + QString buttonDisabledBorderColor() const override + { + return OCC::WLTheme.buttonDisabledColor(); + } + + // Focused + QString buttonFocusedColor() const override + { + return OCC::WLTheme.white(); + } + + QString buttonFocusedBorderColor() const override + { + return OCC::WLTheme.black(); + } + + // Font + QString buttonDisabledFontColor() const override + { + return OCC::WLTheme.buttonDisabledFontColor(); + } + + QString buttonFontColor() const override + { + return OCC::WLTheme.black(); + } + + // Icon (Three Dots) + QString buttonIconDefaultColor() const + { + return OCC::WLTheme.buttonIconColor(); + } + + QString buttonIconHoverColor() const + { + return OCC::WLTheme.buttonIconHoverColor(); + } +}; +} + +#endif // _BUTTONSTYLE_H diff --git a/src/gui/buttonstylestrategy.h b/src/gui/buttonstylestrategy.h new file mode 100644 index 0000000000000..c6a316d25d3de --- /dev/null +++ b/src/gui/buttonstylestrategy.h @@ -0,0 +1,60 @@ +#ifndef BUTTONSTYLESTRATEGY_H +#define BUTTONSTYLESTRATEGY_H + +#include "buttonstyle.h" +#include +#include + + +class ButtonStyleStrategy +{ +public: + virtual ~ButtonStyleStrategy() = default; + + static OCC::ButtonStyle& getButtonStyle(const QWidget *widget, const QStyleOptionButton *option) + { + OCC::ButtonStyleName buttonStyleName; + if(widget != nullptr) + { + buttonStyleName = determineButtonStyleName(widget, option); + } + else + { + buttonStyleName = OCC::ButtonStyleName::Secondary; + } + + switch (buttonStyleName) + { + case OCC::ButtonStyleName::MoreOptions: + return OCC::MoreOptionsButtonStyle::GetInstance(); + case OCC::ButtonStyleName::Primary: + return OCC::PrimaryButtonStyle::GetInstance(); + case OCC::ButtonStyleName::Secondary: + default: + return OCC::SecondaryButtonStyle::GetInstance(); + } + } + + static OCC::ButtonStyleName determineButtonStyleName(const QWidget *widget, const QStyleOptionButton *option) + { + QVariant propertyValue = widget->property("buttonStyle"); + if(propertyValue.isValid()){ + + return propertyValue.value(); + } + + return getButtonStyleNameByObjectName(widget); + } + + static OCC::ButtonStyleName getButtonStyleNameByObjectName(const QWidget *widget) + { + static const QMap buttonStyleMap = { + {"qt_wizard_finish", OCC::ButtonStyleName::Primary} + }; + + QString buttonName = widget->objectName(); + return buttonStyleMap.value(buttonName, OCC::ButtonStyleName::Secondary); + } +}; + +#endif // BUTTONSTYLESTRATEGY_H \ No newline at end of file diff --git a/src/gui/caseclashfilenamedialog.cpp b/src/gui/caseclashfilenamedialog.cpp index 59599330ae80d..66e683eb329b2 100644 --- a/src/gui/caseclashfilenamedialog.cpp +++ b/src/gui/caseclashfilenamedialog.cpp @@ -8,6 +8,7 @@ #include "account.h" #include "folder.h" +#include "buttonstyle.h" #include "common/filesystembase.h" #include "common/utility.h" @@ -74,6 +75,8 @@ CaseClashFilenameDialog::CaseClashFilenameDialog(AccountPtr account, Q_ASSERT(_account); Q_ASSERT(_folder); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + const auto filePathFileInfo = QFileInfo(_filePath); const auto conflictFileName = filePathFileInfo.fileName(); @@ -89,6 +92,7 @@ CaseClashFilenameDialog::CaseClashFilenameDialog(AccountPtr account, _ui->setupUi(this); _ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); _ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Rename file")); + _ui->buttonBox->button(QDialogButtonBox::Ok)->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); _ui->descriptionLabel->setText(tr("The file \"%1\" could not be synced because of a case clash conflict with an existing file on this system.").arg(_originalFileName)); _ui->explanationLabel->setText(tr("%1 does not support equal file names with only letter casing differences.").arg(QSysInfo::prettyProductName())); @@ -140,10 +144,13 @@ CaseClashFilenameDialog::CaseClashFilenameDialog(AccountPtr account, _ui->buttonBox->setStandardButtons(_ui->buttonBox->standardButtons() &~ QDialogButtonBox::No); if (_conflictSolver.allowedToRename()) { _ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + _ui->buttonBox->button(QDialogButtonBox::Ok)->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); + _ui->filenameLineEdit->setEnabled(true); _ui->filenameLineEdit->selectAll(); } else { _ui->buttonBox->setStandardButtons(_ui->buttonBox->standardButtons() | QDialogButtonBox::No); + _ui->buttonBox->button(QDialogButtonBox::Ok)->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); } }); @@ -157,6 +164,8 @@ CaseClashFilenameDialog::CaseClashFilenameDialog(AccountPtr account, }); checkIfAllowedToRename(); + + customizeStyle(); } CaseClashFilenameDialog::~CaseClashFilenameDialog() = default; @@ -276,5 +285,43 @@ void CaseClashFilenameDialog::onFilenameLineEditTextChanged(const QString &text) _ui->buttonBox->button(QDialogButtonBox::Ok) ->setEnabled(isTextValid); + _ui->buttonBox->button(QDialogButtonBox::Ok)->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); + } + +void CaseClashFilenameDialog::customizeStyle() +{ + this->setStyleSheet( + QStringLiteral("QDialog {background-color: %1; color: %2;} QLabel{ %3;}").arg( + WLTheme.dialogBackgroundColor(), + WLTheme.black(), + WLTheme.fontConfigurationCss( + WLTheme.settingsFont(), + WLTheme.settingsTextSize(), + WLTheme.settingsTextWeight(), + WLTheme.titleColor() + ) + ) + ); + + _ui->filenameLineEdit->setStyleSheet( + QStringLiteral( + "color: %1; font-family: %2; font-size: %3; font-weight: %4; border-radius: %5; border: 1px " + "solid %6; padding: 0px 12px; text-align: left; vertical-align: middle; height: 40px; background: %7; ").arg( + WLTheme.folderWizardPathColor(), + WLTheme.settingsFont(), + WLTheme.settingsTextSize(), + WLTheme.settingsTextWeight(), + WLTheme.buttonRadius(), + WLTheme.menuBorderColor(), + WLTheme.white() + ) + ); + + #ifdef Q_OS_MAC + _ui->buttonBox->layout()->setSpacing(24); + _ui->buttonBox->setLayoutDirection(Qt::LeftToRight); + #endif +} + } diff --git a/src/gui/caseclashfilenamedialog.h b/src/gui/caseclashfilenamedialog.h index 295c482f0fc9d..115fcb1052ace 100644 --- a/src/gui/caseclashfilenamedialog.h +++ b/src/gui/caseclashfilenamedialog.h @@ -68,5 +68,7 @@ private slots: QString _relativeFilePath; QString _originalFileName; QString _newFilename; + + void customizeStyle(); }; } diff --git a/src/gui/caseclashfilenamedialog.ui b/src/gui/caseclashfilenamedialog.ui index a5944fc709886..7477525cbe60a 100644 --- a/src/gui/caseclashfilenamedialog.ui +++ b/src/gui/caseclashfilenamedialog.ui @@ -120,6 +120,9 @@ Open existing file + + true + @@ -226,6 +229,9 @@ Open clashing file + + true + diff --git a/src/gui/clickablelabel.h b/src/gui/clickablelabel.h new file mode 100644 index 0000000000000..f0ae1628cb596 --- /dev/null +++ b/src/gui/clickablelabel.h @@ -0,0 +1,28 @@ +#ifndef CLICKABLELABEL_H +#define CLICKABLELABEL_H + +#include +#include +#include + +class ClickableLabel : public QLabel { + Q_OBJECT + +public: + explicit ClickableLabel(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()) + : QLabel(parent, f) {} + explicit ClickableLabel(const QString &text, QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()) + : QLabel(text, parent, f) {} +signals: + void clicked(); + +protected: + void mousePressEvent(QMouseEvent* event) override { + if (event->button() == Qt::LeftButton) { + emit clicked(); + } + QLabel::mousePressEvent(event); + } +}; + +#endif // CLICKABLELABEL_H \ No newline at end of file diff --git a/src/gui/cloudproviders/cloudproviderwrapper.cpp b/src/gui/cloudproviders/cloudproviderwrapper.cpp index 5d0dad7ff2feb..9acef0e4690b7 100644 --- a/src/gui/cloudproviders/cloudproviderwrapper.cpp +++ b/src/gui/cloudproviders/cloudproviderwrapper.cpp @@ -130,7 +130,7 @@ void CloudProviderWrapper::slotUpdateProgress(const QString &folder, const Progr qint64 currentFile = progress.currentFile(); qint64 totalFileCount = qMax(progress.totalFiles(), currentFile); if (progress.trustEta()) { - msg = tr("Syncing %1 of %2 (%3 left)") + msg = tr("Syncing %1 of %2 (%3 left)") .arg(currentFile) .arg(totalFileCount) .arg(Utility::durationToDescriptiveString2(progress.totalProgress().estimatedEta)); diff --git a/src/gui/conflictdialog.cpp b/src/gui/conflictdialog.cpp index 5b56c2987e768..122bd6f550670 100644 --- a/src/gui/conflictdialog.cpp +++ b/src/gui/conflictdialog.cpp @@ -8,6 +8,7 @@ #include "conflictsolver.h" #include "common/utility.h" +#include "buttonstyle.h" #include #include @@ -42,8 +43,10 @@ ConflictDialog::ConflictDialog(QWidget *parent) { _ui->setupUi(this); forceHeaderFont(_ui->conflictMessage); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); _ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); _ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Keep selected version")); + _ui->buttonBox->button(QDialogButtonBox::Ok)->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); _ui->conflictMessage->setTextFormat(Qt::PlainText); @@ -59,6 +62,8 @@ ConflictDialog::ConflictDialog(QWidget *parent) connect(_solver, &ConflictSolver::localVersionFilenameChanged, this, &ConflictDialog::updateWidgets); connect(_solver, &ConflictSolver::remoteVersionFilenameChanged, this, &ConflictDialog::updateWidgets); + + customizeStyle(); } QString ConflictDialog::baseFilename() const @@ -170,6 +175,37 @@ void ConflictDialog::updateButtonStates() : isRemotePicked ? tr("Keep server version") : tr("Keep selected version"); _ui->buttonBox->button(QDialogButtonBox::Ok)->setText(text); + _ui->buttonBox->button(QDialogButtonBox::Ok)->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); + +} + +void ConflictDialog::customizeStyle() +{ + this->setStyleSheet( + QStringLiteral("QDialog {background-color: %1; color: %2;} QLabel{ %3;}").arg( + WLTheme.dialogBackgroundColor(), + WLTheme.black(), + WLTheme.fontConfigurationCss( + WLTheme.settingsFont(), + WLTheme.settingsTextSize(), + WLTheme.settingsTextWeight(), + WLTheme.titleColor() + ) + ) + ); + + #ifdef Q_OS_MAC + _ui->buttonBox->layout()->setSpacing(24); + _ui->buttonBox->setLayoutDirection(Qt::LeftToRight); + + _ui->localVersionRadio->setStyleSheet( + QStringLiteral("QCheckBox {color: %1;}").arg(WLTheme.black()) + ); + + _ui->remoteVersionRadio->setStyleSheet( + QStringLiteral("QCheckBox {color: %1;}").arg(WLTheme.black()) + ); + #endif } } // namespace OCC diff --git a/src/gui/conflictdialog.h b/src/gui/conflictdialog.h index f17bcaa986b57..905e5c2da68f1 100644 --- a/src/gui/conflictdialog.h +++ b/src/gui/conflictdialog.h @@ -37,6 +37,7 @@ public slots: private: void updateWidgets(); void updateButtonStates(); + void customizeStyle(); QString _baseFilename; QScopedPointer _ui; diff --git a/src/gui/conflictsolver.cpp b/src/gui/conflictsolver.cpp index 1bdd584055b9b..c7f56565612c6 100644 --- a/src/gui/conflictsolver.cpp +++ b/src/gui/conflictsolver.cpp @@ -5,11 +5,14 @@ #include "conflictsolver.h" +#include #include #include #include "common/utility.h" #include "filesystem.h" +#include "buttonstyle.h" +#include "whitelabeltheme.h" namespace OCC { @@ -196,8 +199,27 @@ bool ConflictSolver::confirmDeletion() QFileInfo info(_localVersionFilename); const auto message = FileSystem::isDir(_localVersionFilename) ? tr("Do you want to delete the directory %1 and all its contents permanently?").arg(Utility::escape(info.dir().dirName())) - : tr("Do you want to delete the file %1 permanently?").arg(Utility::escape(info.fileName())); - const auto result = QMessageBox::question(_parentWidget, tr("Confirm deletion"), message, buttons); + : tr("Do you want to delete the file %1 permanently?").arg(Utility::escape(info.fileName())); + + QMessageBox msgBox(_parentWidget); + msgBox.setWindowTitle(tr("Confirm deletion")); + msgBox.setText(message); + msgBox.setStandardButtons(buttons); + msgBox.button(QMessageBox::Yes)->setProperty("buttonStyle", QVariant::fromValue(OCC::ButtonStyleName::Primary)); + + msgBox.setStyleSheet( + QStringLiteral("QMessageBox { background-color: %1; } QLabel { %2 }").arg( + WLTheme.dialogBackgroundColor(), + WLTheme.fontConfigurationCss( + WLTheme.settingsFont(), + WLTheme.settingsTextSize(), + WLTheme.settingsTextWeight(), + WLTheme.titleColor() + ) + ) + ); + + const auto result = static_cast(msgBox.exec()); switch (result) { case QMessageBox::YesToAll: diff --git a/src/gui/creds/webflowcredentials.cpp b/src/gui/creds/webflowcredentials.cpp index b293ff6828a11..5b42971c8b5a5 100644 --- a/src/gui/creds/webflowcredentials.cpp +++ b/src/gui/creds/webflowcredentials.cpp @@ -169,8 +169,8 @@ void WebFlowCredentials::askFromUser() { _askDialog->setUrl(url); } - QString msg = tr("You have been logged out of your account %1 at %2. Please login again.") - .arg(_account->prettyName(), _account->url().toDisplayString()); + QString msg = tr("You have been logged out of your account %1 at %2. Please login again.") + .arg(_account->eliedName(200), _account->url().toDisplayString()); _askDialog->setInfo(msg); _askDialog->show(); diff --git a/src/gui/creds/webflowcredentialsdialog.cpp b/src/gui/creds/webflowcredentialsdialog.cpp index e37f755e90bbc..01c4c76acd226 100644 --- a/src/gui/creds/webflowcredentialsdialog.cpp +++ b/src/gui/creds/webflowcredentialsdialog.cpp @@ -11,7 +11,7 @@ #include "application.h" #include "owncloudgui.h" #include "wizard/owncloudwizardcommon.h" - +#include "whitelabeltheme.h" #ifdef WITH_WEBENGINE #include "wizard/webview.h" #endif // WITH_WEBENGINE @@ -29,6 +29,11 @@ WebFlowCredentialsDialog::WebFlowCredentialsDialog(Account *account, bool useFlo { setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + setStyleSheet(QStringLiteral("QDialog { background-color: %1; }").arg(WLTheme.dialogBackgroundColor())); + + setFixedWidth(646); + setSizePolicy(QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum)); + _layout = new QVBoxLayout(this); int spacing = _layout->spacing(); auto margin = _layout->contentsMargins(); @@ -40,12 +45,24 @@ WebFlowCredentialsDialog::WebFlowCredentialsDialog(Account *account, bool useFlo _containerLayout->setContentsMargins(margin); _infoLabel = new QLabel(); - _infoLabel->setTextFormat(Qt::PlainText); + _infoLabel->setTextFormat(Qt::RichText); _infoLabel->setAlignment(Qt::AlignCenter); + _infoLabel->setWordWrap(true); + _infoLabel->setContentsMargins(0, 32, 0, 0); + _infoLabel->setStyleSheet(WLTheme.fontConfigurationCss( + WLTheme.settingsFont(), + WLTheme.settingsTextSize(), + WLTheme.settingsTitleWeight600(), + WLTheme.titleColor() + )); _containerLayout->addWidget(_infoLabel); + layout()->setSizeConstraint(QLayout::SetFixedSize); + if (_useFlow2) { _flow2AuthWidget = new Flow2AuthWidget(); + _flow2AuthWidget->shrinkTopMarginForText(); + _containerLayout->addWidget(_flow2AuthWidget); connect(_flow2AuthWidget, &Flow2AuthWidget::authResult, this, &WebFlowCredentialsDialog::slotFlow2AuthResult); @@ -122,7 +139,7 @@ void WebFlowCredentialsDialog::setError(const QString &error) { slotShowSettingsDialog(); if (_useFlow2 && _flow2AuthWidget) { - _flow2AuthWidget->setError(error); + _flow2AuthWidget->setError("Error", error); return; } diff --git a/src/gui/filedetails/FileActivityView.qml b/src/gui/filedetails/FileActivityView.qml index aa866baa9c599..938c6ebc95ce2 100644 --- a/src/gui/filedetails/FileActivityView.qml +++ b/src/gui/filedetails/FileActivityView.qml @@ -7,7 +7,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls -import com.nextcloud.desktopclient +import com.ionos.hidrivenext.desktopclient import Style import "../tray" diff --git a/src/gui/filedetails/FileDetailsPage.qml b/src/gui/filedetails/FileDetailsPage.qml index c061eff726883..e353dd44e54bb 100644 --- a/src/gui/filedetails/FileDetailsPage.qml +++ b/src/gui/filedetails/FileDetailsPage.qml @@ -7,7 +7,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls -import com.nextcloud.desktopclient +import com.ionos.hidrivenext.desktopclient import Style import "../tray" @@ -36,7 +36,12 @@ Page { localPath: root.localPath } - Connections { + font.family: Style.sesOpenSansRegular + font.pixelSize: Style.sesFontPixelSize + font.weight: Style.sesFontNormalWeight + palette.windowText: Style.sesTrayFontColor + + Connections { target: Systray function onShowFileDetailsPage(fileLocalPath, page) { if (!root.fileDetails.sharingAvailable && page == Systray.FileDetailsPage.Sharing) { @@ -60,12 +65,11 @@ Page { bottomPadding: intendedPadding background: Rectangle { - color: palette.base - visible: root.backgroundsVisible + color: Style.sesBackgroundColor } header: ColumnLayout { - spacing: root.intendedPadding + spacing: Style.sesMediumMargin GridLayout { id: headerGridLayout @@ -99,7 +103,7 @@ Page { Layout.rowSpan: headerGridLayout.rows Layout.preferredWidth: Style.trayListItemIconSize - Layout.leftMargin: root.intendedPadding + Layout.leftMargin: Style.sesMediumMargin Layout.fillHeight: true verticalAlignment: Image.AlignVCenter @@ -114,26 +118,34 @@ Page { id: fileNameLabel Layout.fillWidth: true - Layout.rightMargin: headerGridLayout.textRightMargin + Layout.rightMargin: Style.sesFileDetailsHeaderModifier text: root.fileDetails.name - font.bold: true + + font.pixelSize: Style.sesFontPixelSizeTitle + font.weight: Style.sesFontBoldWeight + wrapMode: Text.Wrap } - Button { + IconButton { id: closeButton + customHoverEnabled: false + Layout.rowSpan: headerGridLayout.rows - Layout.preferredWidth: Style.activityListButtonWidth - Layout.preferredHeight: Style.activityListButtonHeight - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + Layout.preferredWidth: Style.iconButtonWidth + Layout.preferredHeight: width Layout.rightMargin: headerGridLayout.textRightMargin - icon.source: "image://svgimage-custom-color/clear.svg" + "/" + palette.buttonText - icon.width: Style.activityListButtonIconSize - icon.height: Style.activityListButtonIconSize + iconSource: Style.sesAccountQuit + toolTipText: qsTr("Dismiss") + + font.pixelSize: Style.sesFontPixelSize + font.weight: Style.sesFontNormalWeight + visible: root.showCloseButton + onClicked: root.closeButtonClicked() } @@ -141,9 +153,13 @@ Page { id: fileDetailsLabel Layout.fillWidth: true - Layout.rightMargin: headerGridLayout.textRightMargin + Layout.rightMargin: Style.sesFileDetailsHeaderModifier + + text: `${root.fileDetails.sizeString}, ${root.fileDetails.lastChangedString}` + + font.pixelSize: Style.sesFontHintPixelSize + font.weight: Style.sesFontNormalWeight - text: `${root.fileDetails.sizeString} · ${root.fileDetails.lastChangedString}` wrapMode: Text.Wrap } @@ -154,8 +170,12 @@ Page { Layout.rightMargin: headerGridLayout.textRightMargin text: root.fileDetails.lockExpireString + color: palette.midlight wrapMode: Text.Wrap visible: headerGridLayout.showFileLockedString + + font.pixelSize: Style.sesFontHintPixelSize + font.weight: Style.sesFontNormalWeight } Row { @@ -198,38 +218,23 @@ Page { ToolTip { popupType: Qt.platform.os === "windows" ? Popup.Item : Popup.Native visible: hoverHandler.hovered - text: tagRepeater.fileTagModel.overflowTagsString + + font.pixelSize: Style.sesFontPixelSize + font.weight: Style.sesFontNormalWeight + background: Rectangle { + color: Style.sesBackgroundColor + border.color: Style.sesBorderColor + border.width: 1 + radius: 4 + } + contentItem: Text { + text: tagRepeater.fileTagModel.overflowTagsString + color: Style.sesTrayFontColor + } } } } } - - TabBar { - id: viewBar - - Layout.leftMargin: root.intendedPadding - Layout.rightMargin: root.intendedPadding - - padding: 0 - background: null - - NCTabButton { - svgCustomColorSource: "image://svgimage-custom-color/activity.svg" - text: qsTr("Activity") - checked: swipeView.currentIndex === fileActivityView.swipeIndex - onClicked: swipeView.currentIndex = fileActivityView.swipeIndex - } - - NCTabButton { - width: visible ? implicitWidth : 0 - height: visible ? implicitHeight : 0 - svgCustomColorSource: "image://svgimage-custom-color/share.svg" - text: qsTr("Sharing") - checked: swipeView.currentIndex === shareViewLoader.swipeIndex - onClicked: swipeView.currentIndex = shareViewLoader.swipeIndex - visible: root.fileDetails.sharingAvailable - } - } } SwipeView { @@ -238,18 +243,6 @@ Page { anchors.fill: parent clip: true - FileActivityView { - id: fileActivityView - - readonly property int swipeIndex: SwipeView.index - - delegateHorizontalPadding: root.intendedPadding - - accountState: root.accountState - localPath: root.localPath - iconSize: root.iconSize - } - Loader { id: shareViewLoader diff --git a/src/gui/filedetails/FileDetailsView.qml b/src/gui/filedetails/FileDetailsView.qml index b3b8fe83bb36b..1c0e82c982bd9 100644 --- a/src/gui/filedetails/FileDetailsView.qml +++ b/src/gui/filedetails/FileDetailsView.qml @@ -7,7 +7,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls -import com.nextcloud.desktopclient +import com.ionos.hidrivenext.desktopclient import Style StackView { @@ -22,7 +22,7 @@ StackView { property bool backgroundsVisible: true background: Rectangle { - color: palette.base + color: palette.window visible: root.backgroundsVisible } diff --git a/src/gui/filedetails/FileDetailsWindow.qml b/src/gui/filedetails/FileDetailsWindow.qml index 915b9871be603..f6de351f7fea2 100644 --- a/src/gui/filedetails/FileDetailsWindow.qml +++ b/src/gui/filedetails/FileDetailsWindow.qml @@ -8,7 +8,7 @@ import QtQuick.Window import QtQuick.Layouts import QtQuick.Controls -import com.nextcloud.desktopclient +import com.ionos.hidrivenext.desktopclient import Style ApplicationWindow { @@ -17,9 +17,6 @@ ApplicationWindow { property var accountState property string localPath: "" - LayoutMirroring.enabled: Application.layoutDirection === Qt.RightToLeft - LayoutMirroring.childrenInherit: true - width: 400 height: 500 minimumWidth: 300 diff --git a/src/gui/filedetails/NCInputDateField.qml b/src/gui/filedetails/NCInputDateField.qml index e4be486a95a7e..c06702b866e60 100644 --- a/src/gui/filedetails/NCInputDateField.qml +++ b/src/gui/filedetails/NCInputDateField.qml @@ -5,7 +5,7 @@ import QtQuick import QtQuick.Controls -import com.nextcloud.desktopclient +import com.ionos.hidrivenext.desktopclient NCInputTextField { id: root diff --git a/src/gui/filedetails/NCInputTextArea.qml b/src/gui/filedetails/NCInputTextArea.qml index da1eb1ca8a413..acd885cabc227 100644 --- a/src/gui/filedetails/NCInputTextArea.qml +++ b/src/gui/filedetails/NCInputTextArea.qml @@ -7,7 +7,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts -import com.nextcloud.desktopclient +import com.ionos.hidrivenext.desktopclient import Style TextArea { @@ -35,6 +35,8 @@ TextArea { width: height height: parent.height + background: null + flat: true icon.source: "image://svgimage-custom-color/confirm.svg" + "/" + root.secondaryColor icon.color: hovered && enabled ? UserModel.currentUser.accentColor : root.secondaryColor diff --git a/src/gui/filedetails/NCInputTextField.qml b/src/gui/filedetails/NCInputTextField.qml index 45a06b6eb064f..55a728b53b7ac 100644 --- a/src/gui/filedetails/NCInputTextField.qml +++ b/src/gui/filedetails/NCInputTextField.qml @@ -7,7 +7,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts -import com.nextcloud.desktopclient +import com.ionos.hidrivenext.desktopclient import Style TextField { @@ -23,6 +23,14 @@ TextField { rightPadding: submitButton.width selectByMouse: true + + background: Rectangle { + id: textFieldBorder + radius: Style.slightlyRoundedButtonRadius + border.width: Style.normalBorderWidth + border.color: root.activeFocus ? root.validInput ? root.accentColor : Style.errorBoxBackgroundColor : root.secondaryColor + color: palette.base + } Button { id: submitButton diff --git a/src/gui/filedetails/NCTabButton.qml b/src/gui/filedetails/NCTabButton.qml index c68cb91363ad8..542eca6953740 100644 --- a/src/gui/filedetails/NCTabButton.qml +++ b/src/gui/filedetails/NCTabButton.qml @@ -8,7 +8,7 @@ import QtQuick.Window import QtQuick.Layouts import QtQuick.Controls -import com.nextcloud.desktopclient +import com.ionos.hidrivenext.desktopclient import Style import "../tray" diff --git a/src/gui/filedetails/ShareDelegate.qml b/src/gui/filedetails/ShareDelegate.qml index 16407e5f323a9..b0bd2c07ec0f0 100644 --- a/src/gui/filedetails/ShareDelegate.qml +++ b/src/gui/filedetails/ShareDelegate.qml @@ -9,7 +9,7 @@ import QtQuick.Layouts import QtQuick.Controls import Qt5Compat.GraphicalEffects -import com.nextcloud.desktopclient +import com.ionos.hidrivenext.desktopclient import Style import "../tray" import "../" @@ -40,7 +40,7 @@ GridLayout { property FileDetails fileDetails: FileDetails {} property StackView rootStackView: StackView {} property bool backgroundsVisible: true - property color accentColor: Style.ncBlue + property color accentColor: Style.sesIconColor property bool canCreateLinkShares: true property bool serverAllowsResharing: true @@ -130,6 +130,7 @@ GridLayout { Layout.column: 1 text: root.detailText + elide: Text.ElideRight visible: text !== "" } @@ -142,17 +143,19 @@ GridLayout { spacing: 0 - Button { + IconButton { id: createLinkButton Layout.alignment: Qt.AlignCenter - Layout.preferredWidth: Style.activityListButtonWidth - Layout.preferredHeight: Style.activityListButtonHeight + Layout.preferredWidth: Style.iconButtonWidth + Layout.preferredHeight: width + + toolTipText: qsTr("Create a new share link") - icon.source: "image://svgimage-custom-color/add.svg/" + palette.buttonText - icon.width: Style.activityListButtonIconSize - icon.height: Style.activityListButtonIconSize - display: AbstractButton.IconOnly + iconSource: Style.sesDarkPlus + palette.buttonText + icon.width: Style.smallIconSize + icon.height: Style.smallIconSize + customHoverEnabled: false visible: (root.isPlaceholderLinkShare || root.isSecureFileDropPlaceholderLinkShare) && root.canCreateLinkShares enabled: visible @@ -160,7 +163,7 @@ GridLayout { onClicked: root.createNewLinkShare() } - Button { + SecondaryPillButton { id: copyLinkButton function copyShareLink() { @@ -175,18 +178,28 @@ GridLayout { property bool shareLinkCopied: false - Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter - Layout.preferredWidth: shareLinkCopied ? implicitWidth : Style.activityListButtonWidth - Layout.preferredHeight: Style.activityListButtonHeight + removeBorder: true + + Layout.alignment: Qt.AlignCenter + Layout.preferredWidth: shareLinkCopied ? implicitWidth : Style.iconButtonWidth + Layout.preferredHeight: Style.iconButtonWidth + Layout.rightMargin: Style.sesSmallMargin + + toolTipText: qsTr("Copy share link location") text: shareLinkCopied ? qsTr("Copied!") : "" + textColor: Style.sesDarkGreen + backgroundColor: Style.clipboardBackgroundColor + + iconSource: shareLinkCopied ? Style.sesSyncSuccessIcon + Style.positiveColor : + Style.sesClipboard + palette.brightText + + icon.width: Style.smallIconSize + icon.height: Style.smallIconSize - icon.source: "image://svgimage-custom-color/copy.svg/" + palette.buttonText - icon.width: Style.activityListButtonIconSize - icon.height: Style.activityListButtonIconSize - display: shareLinkCopied ? AbstractButton.TextOnly : AbstractButton.IconOnly visible: root.isLinkShare || root.isInternalLinkShare enabled: visible + onClicked: copyShareLink() Behavior on Layout.preferredWidth { @@ -205,19 +218,26 @@ GridLayout { } } - Button { + IconButton { id: moreButton - Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter - Layout.preferredWidth: Style.activityListButtonWidth - Layout.preferredHeight: Style.activityListButtonHeight + property bool isHovered: moreButton.hovered || moreButton.visualFocus + property bool isActive: moreButton.pressed + + Layout.alignment: Qt.AlignCenter + Layout.preferredWidth: Style.iconButtonWidth + Layout.preferredHeight: width + + toolTipText: qsTr("Share options") + + iconSource: Style.sesMore + iconSourceHovered: Style.sesMoreHover + width: Style.smallIconSize + height: Style.smallIconSize - icon.source: "image://svgimage-custom-color/more.svg/" + palette.buttonText - icon.width: Style.activityListButtonIconSize - icon.height: Style.activityListButtonIconSize - display: AbstractButton.IconOnly visible: !root.isPlaceholderLinkShare && !root.isSecureFileDropPlaceholderLinkShare && !root.isInternalLinkShare enabled: visible + onClicked: root.rootStackView.push(shareDetailsPageComponent, {}, StackView.PushTransition) Component { diff --git a/src/gui/filedetails/ShareDetailsPage.qml b/src/gui/filedetails/ShareDetailsPage.qml index 39fbae87a22cd..7d960452fd3ec 100644 --- a/src/gui/filedetails/ShareDetailsPage.qml +++ b/src/gui/filedetails/ShareDetailsPage.qml @@ -9,14 +9,27 @@ import QtQuick.Layouts import QtQuick.Controls import Qt5Compat.GraphicalEffects -import com.nextcloud.desktopclient +import com.ionos.hidrivenext.desktopclient import Style import "../tray" +import "../SesComponents/" import "../" Page { id: root + component SesCheckBox: CheckBox { + hoverEnabled: false + palette.base: Style.sesBackgroundColor + contentItem: Text { + text: parent.text + color: Style.sesTrayFontColor + font: parent.font + leftPadding: parent.indicator.width + parent.spacing + verticalAlignment: Text.AlignVCenter + } + } + signal closeShareDetails signal deleteShare signal createNewLinkShare @@ -34,6 +47,12 @@ Page { signal setPassword(string password) signal setNote(string note) + + font.family: Style.sesOpenSansRegular + font.pixelSize: Style.sesFontPixelSize + font.weight: Style.sesFontNormalWeight + palette.text: Style.sesTrayFontColor + property bool backgroundsVisible: true property color accentColor: Style.ncBlue @@ -71,6 +90,7 @@ Page { readonly property bool isHideDownloadInProgress: shareModelData.isHideDownloadInProgress readonly property int currentPermissionMode: shareModelData.currentPermissionMode + readonly property bool isInternalShare: shareModelData.shareType === ShareModel.ShareTypeUser || shareModelData.shareType === ShareModel.ShareTypeGroup || shareModelData.shareType === ShareModel.ShareTypeCircle readonly property bool isLinkShare: shareModelData.shareType === ShareModel.ShareTypeLink readonly property bool isEmailShare: shareModelData.shareType === ShareModel.ShareTypeEmail readonly property bool shareSupportsPassword: isLinkShare || isEmailShare @@ -85,6 +105,16 @@ Page { property bool waitingForPasswordChange: false property bool waitingForNoteChange: false + readonly property int titlePixelSize: Style.sesFontPixelSizeTitle + readonly property int titleFontWeight: Style.sesFontNormalWeight + + readonly property int hintPixelSize: Style.sesFontHintPixelSize + readonly property int hintFontWeight: Style.sesFontNormalWeight + + + readonly property int pixelSize: Style.sesFontPixelSize + readonly property int fontWeight: Style.sesFontNormalWeight + function showPasswordSetError(message) { passwordErrorBoxLoader.message = message !== "" ? message : qsTr("An error occurred setting the share password."); @@ -96,8 +126,8 @@ Page { } function resetLinkShareLabelField() { - linkShareLabelTextField.text = linkShareLabel; - waitingForLinkShareLabelChange = false; + // linkShareLabelTextField.text = linkShareLabel; + // waitingForLinkShareLabelChange = false; } function resetPasswordField() { @@ -165,8 +195,7 @@ Page { padding: Style.standardSpacing * 2 background: Rectangle { - color: palette.base - visible: root.backgroundsVisible + color: Style.sesBackgroundColor } header: ColumnLayout { @@ -201,38 +230,46 @@ Page { } EnforcedPlainTextLabel { - id: headLabel + id: fileNameLabel Layout.fillWidth: true + Layout.rightMargin: headerGridLayout.textRightMargin - text: qsTr("Edit share") - font.bold: true - elide: Text.ElideRight + text: root.fileDetails.name + color: Style.sesTrayFontColor + font.pixelSize: titlePixelSize + font.weight: titleFontWeight + + wrapMode: Text.Wrap } - Button { - id: closeButton + IconButton { + id: placeholder + + customHoverEnabled: false Layout.rowSpan: headerGridLayout.rows - Layout.preferredWidth: Style.activityListButtonWidth - Layout.preferredHeight: Style.activityListButtonHeight - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + Layout.preferredWidth: Style.iconButtonWidth + Layout.preferredHeight: width Layout.rightMargin: root.padding - icon.source: "image://svgimage-custom-color/clear.svg" + "/" + palette.buttonText - icon.width: Style.activityListButtonIconSize - icon.height: Style.activityListButtonIconSize + iconSource: Style.sesAccountQuit + onClicked: root.closeShareDetails() } EnforcedPlainTextLabel { - id: secondaryLabel + id: fileDetailsLabel Layout.fillWidth: true - Layout.rightMargin: root.padding + Layout.rightMargin: headerGridLayout.textRightMargin - text: root.fileDetails.name + text: `${root.fileDetails.sizeString}, ${root.fileDetails.lastChangedString}` wrapMode: Text.Wrap + color: Style.sesTrayFontColor + + font.pixelSize: hintPixelSize + font.weight: hintFontWeight } } } @@ -250,207 +287,14 @@ Page { readonly property int itemPadding: Style.smallSpacing width: parent.width - spacing: Style.smallSpacing - - RowLayout { - Layout.fillWidth: true - height: visible ? implicitHeight : 0 - spacing: scrollContentsColumn.indicatorSpacing - - visible: root.isLinkShare - - Image { - Layout.preferredWidth: scrollContentsColumn.indicatorItemWidth - Layout.fillHeight: true - - verticalAlignment: Image.AlignVCenter - horizontalAlignment: Image.AlignHCenter - fillMode: Image.Pad - - source: "image://svgimage-custom-color/label.svg/" + palette.windowText - sourceSize.width: scrollContentsColumn.rowIconWidth - sourceSize.height: scrollContentsColumn.rowIconWidth - } - - NCInputTextField { - id: linkShareLabelTextField - - Layout.fillWidth: true - height: visible ? implicitHeight : 0 - - text: root.linkShareLabel - placeholderText: qsTr("Share label") - - enabled: root.isLinkShare && - !root.waitingForLinkShareLabelChange - - onAccepted: if(text !== root.linkShareLabel) { - root.setLinkShareLabel(text); - root.waitingForLinkShareLabelChange = true; - } - - NCBusyIndicator { - anchors.fill: parent - visible: root.waitingForLinkShareLabelChange - running: visible - z: 1 - } - } - } - - Loader { - Layout.fillWidth: true - active: !root.isFolderItem && !root.isEncryptedItem - visible: active - sourceComponent: CheckBox { - spacing: scrollContentsColumn.indicatorSpacing - leftPadding: scrollContentsColumn.itemPadding - rightPadding: scrollContentsColumn.itemPadding - indicator.width: scrollContentsColumn.indicatorItemWidth - indicator.height: scrollContentsColumn.indicatorItemWidth - - checkable: true - checked: root.editingAllowed - text: qsTr("Allow upload and editing") - enabled: !root.isSharePermissionChangeInProgress - - onClicked: root.toggleAllowEditing(checked) - - NCBusyIndicator { - anchors.fill: parent - visible: root.isSharePermissionChangeInProgress - running: visible - z: 1 - } - } - } - - Loader { - Layout.fillWidth: true - active: root.isFolderItem && !root.isEncryptedItem - visible: active - sourceComponent: ColumnLayout { - id: permissionRadioButtonsLayout - spacing: Layout.smallSpacing - width: parent.width - - ButtonGroup { - id: permissionModeRadioButtonsGroup - } - - RadioButton { - readonly property int permissionMode: ShareModel.ModeViewOnly - Layout.fillWidth: true - ButtonGroup.group: permissionModeRadioButtonsGroup - enabled: !root.isSharePermissionChangeInProgress - checked: root.currentPermissionMode === permissionMode - text: qsTr("View only") - spacing: scrollContentsColumn.indicatorSpacing - leftPadding: scrollContentsColumn.itemPadding - rightPadding: scrollContentsColumn.itemPadding - onClicked: root.permissionModeChanged(permissionMode) - } - - RadioButton { - readonly property int permissionMode: ShareModel.ModeUploadAndEditing - Layout.fillWidth: true - ButtonGroup.group: permissionModeRadioButtonsGroup - enabled: !root.isSharePermissionChangeInProgress - checked: root.currentPermissionMode === permissionMode - text: qsTr("Allow upload and editing") - spacing: scrollContentsColumn.indicatorSpacing - leftPadding: scrollContentsColumn.itemPadding - rightPadding: scrollContentsColumn.itemPadding - onClicked: root.permissionModeChanged(permissionMode) - } - - RadioButton { - readonly property int permissionMode: ShareModel.ModeFileDropOnly - Layout.fillWidth: true - ButtonGroup.group: permissionModeRadioButtonsGroup - enabled: !root.isSharePermissionChangeInProgress - checked: root.currentPermissionMode === permissionMode - text: qsTr("File drop (upload only)") - spacing: scrollContentsColumn.indicatorSpacing - leftPadding: scrollContentsColumn.itemPadding - rightPadding: scrollContentsColumn.itemPadding - onClicked: root.permissionModeChanged(permissionMode) - } - - CheckBox { - id: allowResharingCheckBox - - Layout.fillWidth: true - - spacing: scrollContentsColumn.indicatorSpacing - leftPadding: scrollContentsColumn.itemPadding - rightPadding: scrollContentsColumn.itemPadding - indicator.width: scrollContentsColumn.indicatorItemWidth - indicator.height: scrollContentsColumn.indicatorItemWidth - - checkable: true - checked: root.resharingAllowed - text: qsTr("Allow resharing") - enabled: !root.isSharePermissionChangeInProgress && root.serverAllowsResharing - visible: root.serverAllowsResharing - onClicked: root.toggleAllowResharing(checked); - - Connections { - target: root - onResharingAllowedChanged: allowResharingCheckBox.checked = root.resharingAllowed - } - } - } - NCBusyIndicator { - anchors.fill: parent - visible: root.isSharePermissionChangeInProgress - running: visible - z: 1 - } - } - - Loader { - Layout.fillWidth: true - - active: root.isLinkShare - visible: active - sourceComponent: ColumnLayout { - CheckBox { - id: hideDownloadEnabledMenuItem - - anchors.left: parent.left - anchors.right: parent.right - - spacing: scrollContentsColumn.indicatorSpacing - leftPadding: scrollContentsColumn.itemPadding - rightPadding: scrollContentsColumn.itemPadding - indicator.width: scrollContentsColumn.indicatorItemWidth - indicator.height: scrollContentsColumn.indicatorItemWidth - - checked: root.hideDownload - text: qsTr("Hide download") - enabled: !root.isHideDownloadInProgress - onClicked: root.toggleHideDownload(checked); - - NCBusyIndicator { - anchors.fill: parent - visible: root.isHideDownloadInProgress - running: visible - z: 1 - } - } - } - } - - CheckBox { + SesCheckBox { id: passwordProtectEnabledMenuItem Layout.fillWidth: true spacing: scrollContentsColumn.indicatorSpacing - leftPadding: scrollContentsColumn.itemPadding - rightPadding: scrollContentsColumn.itemPadding + padding: scrollContentsColumn.itemPadding indicator.width: scrollContentsColumn.indicatorItemWidth indicator.height: scrollContentsColumn.indicatorItemWidth @@ -466,62 +310,6 @@ Page { root.togglePasswordProtect(checked); root.waitingForPasswordProtectEnabledChange = true; } - - NCBusyIndicator { - anchors.fill: parent - visible: root.waitingForPasswordProtectEnabledChange - running: visible - z: 1 - } - } - - RowLayout { - Layout.fillWidth: true - - height: visible ? implicitHeight : 0 - spacing: scrollContentsColumn.indicatorSpacing - - visible: root.shareSupportsPassword && root.passwordProtectEnabled - - Image { - Layout.preferredWidth: scrollContentsColumn.indicatorItemWidth - Layout.fillHeight: true - - verticalAlignment: Image.AlignVCenter - horizontalAlignment: Image.AlignHCenter - fillMode: Image.Pad - - source: "image://svgimage-custom-color/lock-https.svg/" + palette.windowText - sourceSize.width: scrollContentsColumn.rowIconWidth - sourceSize.height: scrollContentsColumn.rowIconWidth - } - - NCInputTextField { - id: passwordTextField - - Layout.fillWidth: true - height: visible ? implicitHeight : 0 - - text: root.password !== "" ? root.password : root.passwordPlaceholder - enabled: visible && - root.passwordProtectEnabled && - !root.waitingForPasswordChange && - !root.waitingForPasswordProtectEnabledChange - - onAccepted: if(text !== root.password && text !== root.passwordPlaceholder) { - passwordErrorBoxLoader.message = ""; - root.setPassword(text); - root.waitingForPasswordChange = true; - } - - NCBusyIndicator { - anchors.fill: parent - visible: root.waitingForPasswordChange || - root.waitingForPasswordProtectEnabledChange - running: visible - z: 1 - } - } } Loader { @@ -542,7 +330,7 @@ Page { // Artificially add vertical padding implicitHeight: passwordErrorBox.implicitHeight + (Style.smallSpacing * 2) - ErrorBox { + SesErrorBox { id: passwordErrorBox anchors.left: parent.left anchors.right: parent.right @@ -553,14 +341,53 @@ Page { } } - CheckBox { + TextEdit { + id: passwordTextEdit + visible: root.passwordProtectEnabled + Layout.fillWidth: true + Layout.leftMargin: 3 + Layout.rightMargin: 3 + height: visible ? 64 : 0 + wrapMode: TextEdit.Wrap + selectByMouse: true + text: root.password !== "" ? root.password : root.passwordPlaceholder + + font.family: root.font.family + font.pixelSize: pixelSize + font.weight: fontWeight + + padding: scrollContentsColumn.itemPadding + enabled: visible && + root.passwordProtectEnabled && + !root.waitingForPasswordChange && + !root.waitingForPasswordProtectEnabledChange + + onEditingFinished: if(text !== root.password && text !== root.passwordPlaceholder) { + passwordErrorBoxLoader.message = ""; + root.setPassword(text); + root.waitingForPasswordChange = true; + } + + Rectangle { + id: passwordTextBorder + anchors.fill: parent + radius: Style.slightlyRoundedButtonRadius + border.width: Style.thickBorderWidth + border.color: Style.sesTrayInputField + color: Style.sesBackgroundColor + z: -1 + } + } + + SesCheckBox { id: expireDateEnabledMenuItem Layout.fillWidth: true + font.pixelSize: pixelSize + font.weight: fontWeight spacing: scrollContentsColumn.indicatorSpacing - leftPadding: scrollContentsColumn.itemPadding - rightPadding: scrollContentsColumn.itemPadding + padding: scrollContentsColumn.itemPadding indicator.width: scrollContentsColumn.indicatorItemWidth indicator.height: scrollContentsColumn.indicatorItemWidth @@ -573,72 +400,56 @@ Page { root.toggleExpirationDate(checked); root.waitingForExpireDateEnabledChange = true; } - - NCBusyIndicator { - anchors.fill: parent - visible: root.waitingForExpireDateEnabledChange - running: visible - z: 1 - } } - RowLayout { + NCInputDateField { + id: expireDateField + + font.pixelSize: pixelSize + font.weight: fontWeight + Layout.fillWidth: true + Layout.leftMargin: 3 + Layout.rightMargin: 3 height: visible ? implicitHeight : 0 - spacing: scrollContentsColumn.indicatorSpacing + leftPadding: 15 visible: root.expireDateEnabled - Image { - Layout.preferredWidth: scrollContentsColumn.indicatorItemWidth - Layout.fillHeight: true - - verticalAlignment: Image.AlignVCenter - horizontalAlignment: Image.AlignHCenter - fillMode: Image.Pad - - source: "image://svgimage-custom-color/calendar.svg/" + palette.windowText - sourceSize.width: scrollContentsColumn.rowIconWidth - sourceSize.height: scrollContentsColumn.rowIconWidth + selectByMouse: true + + dateInMs: root.expireDate + maximumDateMs: root.maximumExpireDate + minimumDateMs: { + const currentDate = new Date(); + const currentYear = currentDate.getFullYear(); + const currentMonth = currentDate.getMonth(); + const currentMonthDay = currentDate.getDate(); + // Start of day at 00:00:0000 UTC + return Date.UTC(currentYear, currentMonth, currentMonthDay + 1); } - NCInputDateField { - id: expireDateField - - Layout.fillWidth: true - height: visible ? implicitHeight : 0 - - dateInMs: root.expireDate - maximumDateMs: root.maximumExpireDate - minimumDateMs: { - const currentDate = new Date(); - const currentYear = currentDate.getFullYear(); - const currentMonth = currentDate.getMonth(); - const currentMonthDay = currentDate.getDate(); - // Start of day at 00:00:0000 UTC - return Date.UTC(currentYear, currentMonth, currentMonthDay + 1); - } - - enabled: root.expireDateEnabled && - !root.waitingForExpireDateChange && - !root.waitingForExpireDateEnabledChange + enabled: root.expireDateEnabled && + !root.waitingForExpireDateChange && + !root.waitingForExpireDateEnabledChange - onUserAcceptedDate: { - root.setExpireDate(dateInMs); - root.waitingForExpireDateChange = true; - } + onUserAcceptedDate: { + root.setExpireDate(dateInMs); + root.waitingForExpireDateChange = true; + } - NCBusyIndicator { - anchors.fill: parent - visible: root.waitingForExpireDateEnabledChange || - root.waitingForExpireDateChange - running: visible - z: 1 - } + Rectangle { + id: dateTextBorder + anchors.fill: parent + radius: Style.slightlyRoundedButtonRadius + border.width: Style.thickBorderWidth + border.color: Style.sesTrayInputField + color: Style.sesBackgroundColor + z: -1 } } - - CheckBox { + + SesCheckBox { id: noteEnabledMenuItem Layout.fillWidth: true @@ -660,13 +471,6 @@ Page { root.waitingForNoteChange = true; } } - - NCBusyIndicator { - anchors.fill: parent - visible: root.waitingForNoteChange && !noteEnabledMenuItem.checked - running: visible - z: 1 - } } RowLayout { @@ -698,6 +502,7 @@ Page { text: root.note placeholderText: qsTr("Enter a note for the recipient") + color: Style.sesTrayFontColor enabled: noteEnabledMenuItem.checked && !root.waitingForNoteChange onEditingFinished: if (text !== "" && text !== root.note) { @@ -705,89 +510,284 @@ Page { root.waitingForNoteChange = true; } - NCBusyIndicator { + background: Rectangle { + id: noteTextBorder anchors.fill: parent - visible: root.waitingForNoteChange && noteEnabledMenuItem.checked - running: visible - z: 1 + radius: Style.slightlyRoundedButtonRadius + border.width: Style.thickBorderWidth + border.color: Style.sesTrayInputField + color: Style.sesBackgroundColor + z: -1 } } + } + + Loader { + Layout.fillWidth: true + active: !root.isFolderItem && !root.isEncryptedItem + visible: active + sourceComponent: SesCheckBox { + + font.pixelSize: pixelSize + font.weight: fontWeight + + spacing: scrollContentsColumn.indicatorSpacing + padding: scrollContentsColumn.itemPadding + indicator.width: scrollContentsColumn.indicatorItemWidth + indicator.height: scrollContentsColumn.indicatorItemWidth + + checkable: true + checked: root.editingAllowed + text: qsTr("Allow upload and editing") + enabled: !root.isSharePermissionChangeInProgress + + onClicked: root.toggleAllowEditing(checked) + } } - Button { - height: Style.standardPrimaryButtonHeight - icon.source: "image://svgimage-custom-color/close.svg/" + palette.buttonText - icon.height: Style.extraSmallIconSize - text: qsTr("Unshare") - onClicked: root.deleteShare() + Loader { + Layout.fillWidth: true + active: root.isFolderItem && !root.isEncryptedItem + visible: active + sourceComponent: ColumnLayout { + id: permissionRadioButtonsLayout + spacing: 0 + width: parent.width + + ButtonGroup { + id: permissionModeRadioButtonsGroup + } + + SesCheckBox { + id: customPermissionsCheckBox + Layout.fillWidth: true + enabled: !root.isSharePermissionChangeInProgress + checkable: false + checked: true + text: qsTr("Custom Permissions") + spacing: scrollContentsColumn.indicatorSpacing + padding: scrollContentsColumn.itemPadding + indicator.width: scrollContentsColumn.indicatorItemWidth + indicator.height: scrollContentsColumn.indicatorItemWidth + onClicked: root.permissionModeChanged(permissionMode) + font.pixelSize: pixelSize + font.weight: fontWeight + } + + SesCheckBox { + readonly property int permissionMode: ShareModel.ModeViewOnly + Layout.fillWidth: true + Layout.leftMargin: 30 + ButtonGroup.group: permissionModeRadioButtonsGroup + enabled: !root.isSharePermissionChangeInProgress + checked: root.currentPermissionMode === permissionMode + text: qsTr("View only") + indicator.width: scrollContentsColumn.indicatorItemWidth + indicator.height: scrollContentsColumn.indicatorItemWidth + spacing: scrollContentsColumn.indicatorSpacing + padding: scrollContentsColumn.itemPadding + onClicked: root.permissionModeChanged(permissionMode) + visible: customPermissionsCheckBox.checked + font.pixelSize: pixelSize + font.weight: fontWeight + } + + SesCheckBox { + readonly property int permissionMode: ShareModel.ModeUploadAndEditing + Layout.fillWidth: true + Layout.leftMargin: 30 + ButtonGroup.group: permissionModeRadioButtonsGroup + enabled: !root.isSharePermissionChangeInProgress + checked: root.currentPermissionMode === permissionMode + text: qsTr("Allow upload and editing") + indicator.width: scrollContentsColumn.indicatorItemWidth + indicator.height: scrollContentsColumn.indicatorItemWidth + spacing: scrollContentsColumn.indicatorSpacing + padding: scrollContentsColumn.itemPadding + onClicked: root.permissionModeChanged(permissionMode) + visible: customPermissionsCheckBox.checked + font.pixelSize: pixelSize + font.weight: fontWeight + } + + SesCheckBox { + readonly property int permissionMode: ShareModel.ModeFileDropOnly + Layout.fillWidth: true + Layout.leftMargin: 30 + ButtonGroup.group: permissionModeRadioButtonsGroup + enabled: !root.isSharePermissionChangeInProgress + checked: root.currentPermissionMode === permissionMode + text: qsTr("File drop (upload only)") + indicator.width: scrollContentsColumn.indicatorItemWidth + indicator.height: scrollContentsColumn.indicatorItemWidth + spacing: scrollContentsColumn.indicatorSpacing + padding: scrollContentsColumn.itemPadding + onClicked: root.permissionModeChanged(permissionMode) + visible: customPermissionsCheckBox.checked && !root.isInternalShare // Removed SES-307 + font.pixelSize: pixelSize + font.weight: fontWeight + } + } } - Button { - height: Style.standardPrimaryButtonHeight - icon.source: "image://svgimage-custom-color/add.svg/" + palette.buttonText - icon.height: Style.extraSmallIconSize - text: qsTr("Add another link") - visible: root.isLinkShare && root.canCreateLinkShares - enabled: visible - onClicked: root.createNewLinkShare() + SesCheckBox { + id: allowResharingCheckBox + + Layout.fillWidth: true + + font.pixelSize: pixelSize + font.weight: fontWeight + + spacing: scrollContentsColumn.indicatorSpacing + padding: scrollContentsColumn.itemPadding + indicator.width: scrollContentsColumn.indicatorItemWidth + indicator.height: scrollContentsColumn.indicatorItemWidth + + checkable: true + checked: root.resharingAllowed + text: qsTr("Allow resharing") + enabled: !root.isSharePermissionChangeInProgress && root.serverAllowsResharing + visible: root.serverAllowsResharing + onClicked: root.toggleAllowResharing(checked); + + Connections { + target: root + onResharingAllowedChanged: allowResharingCheckBox.checked = root.resharingAllowed + } } - } - } - footer: DialogButtonBox { - topPadding: 0 - bottomPadding: root.padding - rightPadding: root.padding - leftPadding: root.padding - alignment: Qt.AlignRight | Qt.AlignVCenter - contentWidth: (contentItem as ListView).contentWidth - visible: copyShareLinkButton.visible + Loader { + Layout.fillWidth: true - background: Rectangle { color: "transparent" } + active: root.isLinkShare + visible: active + sourceComponent: ColumnLayout { + SesCheckBox { + id: hideDownloadEnabledMenuItem - Button { - id: copyShareLinkButton + anchors.left: parent.left + anchors.right: parent.right - function copyShareLink() { - clipboardHelper.text = root.link; - clipboardHelper.selectAll(); - clipboardHelper.copy(); - clipboardHelper.clear(); + font.pixelSize: pixelSize + font.weight: fontWeight - shareLinkCopied = true; - shareLinkCopyTimer.start(); + spacing: scrollContentsColumn.indicatorSpacing + padding: scrollContentsColumn.itemPadding + indicator.width: scrollContentsColumn.indicatorItemWidth + indicator.height: scrollContentsColumn.indicatorItemWidth + + checked: root.hideDownload + text: qsTr("Hide download") + enabled: !root.isHideDownloadInProgress + onClicked: root.toggleHideDownload(checked); + } + } } + } + } + + footer: ColumnLayout { + id: buttonGrid - property bool shareLinkCopied: false + spacing: 0 - height: Style.standardPrimaryButtonHeight + PrimaryPillButton { + iconSource: Style.sesLightPlus - Layout.preferredWidth: Style.activityListButtonWidth - Layout.preferredHeight: Style.activityListButtonHeight - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + font.pixelSize: pixelSize + font.weight: fontWeight + text: qsTr("Add another link") - icon.source: "image://svgimage-custom-color/copy.svg/" + palette.brightText - icon.width: Style.smallIconSize - icon.height: Style.smallIconSize - text: shareLinkCopied ? qsTr("Share link copied!") : qsTr("Copy share link") - visible: root.isLinkShare + visible: root.isLinkShare && root.canCreateLinkShares enabled: visible - onClicked: copyShareLink() + Layout.leftMargin: 16 + Layout.bottomMargin: 16 + Layout.maximumWidth: actionButtonsGrid.width - Behavior on Layout.preferredWidth { - SmoothedAnimation { duration: Style.shortAnimationDuration } + onClicked: root.createNewLinkShare() + } + + GridLayout { + id: actionButtonsGrid + + readonly property bool buttonsOverflow: unshareButton.implicitWidth + copyShareLinkButton.implicitWidth + columnSpacing + 16 + 20 > root.width + + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 20 + Layout.bottomMargin: 16 + columns: buttonsOverflow ? 1 : 3 + columnSpacing: 8 + rowSpacing: 8 + + SecondaryPillButton { + id: unshareButton + + Layout.maximumWidth: actionButtonsGrid.width + + font.pixelSize: pixelSize + font.weight: fontWeight + text: qsTr("Unshare") + + onClicked: root.deleteShare() } - TextEdit { - id: clipboardHelper - visible: false + Item { + Layout.fillWidth: true + visible: !actionButtonsGrid.buttonsOverflow } - Timer { - id: shareLinkCopyTimer - interval: Style.veryLongAnimationDuration - onTriggered: copyShareLinkButton.shareLinkCopied = false + PrimaryPillButton { + id: copyShareLinkButton + + function copyShareLink() { + clipboardHelper.text = root.link; + clipboardHelper.selectAll(); + clipboardHelper.copy(); + clipboardHelper.clear(); + + shareLinkCopied = true; + shareLinkCopyTimer.start(); + } + + property bool shareLinkCopied: false + property bool hasBeenClicked: false + + clip: true + iconSource: Style.sesLightClipboard + textWrapMode: widthAnimation.running ? Text.NoWrap : Text.WordWrap + + text: shareLinkCopied ? qsTr("Share link copied!") : qsTr("Copy share link") + + Layout.maximumWidth: actionButtonsGrid.width + + visible: root.isLinkShare + enabled: visible + + onClicked: { + hasBeenClicked = true; + copyShareLink(); + } + + Behavior on implicitWidth { + enabled: copyShareLinkButton.hasBeenClicked + SmoothedAnimation { + id: widthAnimation + duration: Style.shortAnimationDuration + } + } + + TextEdit { + id: clipboardHelper + visible: false + } + + Timer { + id: shareLinkCopyTimer + interval: Style.veryLongAnimationDuration + onTriggered: copyShareLinkButton.shareLinkCopied = false + } } } } diff --git a/src/gui/filedetails/ShareView.qml b/src/gui/filedetails/ShareView.qml index 9d593b9d295b3..322a77410e551 100644 --- a/src/gui/filedetails/ShareView.qml +++ b/src/gui/filedetails/ShareView.qml @@ -8,9 +8,10 @@ import QtQuick.Window import QtQuick.Layouts import QtQuick.Controls -import com.nextcloud.desktopclient +import com.ionos.hidrivenext.desktopclient import Style import "../tray" +import "../SesComponents" import "../" ColumnLayout { @@ -78,6 +79,10 @@ ColumnLayout { close(); } + background: Rectangle { + color: Style.sesBackgroundColor + } + anchors.centerIn: parent width: parent.width * 0.8 @@ -114,20 +119,14 @@ ColumnLayout { } } - ErrorBox { + SesErrorBox { id: errorBox Layout.fillWidth: true Layout.leftMargin: root.horizontalPadding Layout.rightMargin: root.horizontalPadding - showCloseButton: true visible: false - - onCloseButtonClicked: { - text = ""; - visible = false; - } } RowLayout { @@ -164,6 +163,7 @@ ColumnLayout { Layout.topMargin: Style.smallSpacing Layout.leftMargin: root.horizontalPadding Layout.rightMargin: root.horizontalPadding + Layout.preferredHeight: Style.sesSearchFieldHeight visible: root.userGroupSharingPossible enabled: visible && !root.loading && !root.shareModel.isShareDisabledEncryptedFolder && !shareeSearchField.isShareeFetchOngoing @@ -185,6 +185,7 @@ ColumnLayout { Layout.fillHeight: true Layout.leftMargin: root.horizontalPadding Layout.rightMargin: root.horizontalPadding + Layout.topMargin: Style.sesMediumMargin active: root.sharingPossible @@ -206,7 +207,9 @@ ColumnLayout { sourceModel: root.shareModel } - delegate: ShareDelegate { + delegate: ColumnLayout{ + width: parent.width + ShareDelegate { id: shareDelegate Connections { @@ -236,7 +239,7 @@ ColumnLayout { fileDetails: root.fileDetails rootStackView: root.rootStackView backgroundsVisible: root.backgroundsVisible - accentColor: root.accentColor + accentColor: Style.sesIconColor canCreateLinkShares: root.publicLinkSharingPossible serverAllowsResharing: root.serverAllowsResharing @@ -260,6 +263,14 @@ ColumnLayout { onSetExpireDate: shareModel.setShareExpireDateFromQml(model.share, milliseconds) onSetPassword: shareModel.setSharePasswordFromQml(model.share, password) onSetNote: shareModel.setShareNoteFromQml(model.share, note) + width: parent.width + } + + Rectangle{ + height: Style.sesMediumMargin + color: "transparent" + width: parent.width + } } Loader { @@ -302,6 +313,7 @@ ColumnLayout { id: sharingDisabledLabel width: parent.width text: qsTr("Sharing is disabled") + color: palette.midlight wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter @@ -309,6 +321,7 @@ ColumnLayout { EnforcedPlainTextLabel { width: parent.width text: qsTr("This item cannot be shared.") + color: palette.midlight wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter @@ -317,6 +330,7 @@ ColumnLayout { EnforcedPlainTextLabel { width: parent.width text: qsTr("Sharing is disabled.") + color: palette.midlight wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter diff --git a/src/gui/filedetails/ShareeDelegate.qml b/src/gui/filedetails/ShareeDelegate.qml index 52cf08ee6d423..6361703665a63 100644 --- a/src/gui/filedetails/ShareeDelegate.qml +++ b/src/gui/filedetails/ShareeDelegate.qml @@ -8,7 +8,7 @@ import QtQuick.Window import QtQuick.Layouts import QtQuick.Controls -import com.nextcloud.desktopclient +import com.ionos.hidrivenext.desktopclient import Style import "../tray" @@ -18,6 +18,10 @@ ItemDelegate { text: model.display + background: Rectangle { + color: root.hovered ? root.pressed ? Style.sesButtonPressed : Style.sesHover : Style.sesWhite + } + contentItem: RowLayout { height: visible ? implicitHeight : 0 @@ -53,6 +57,10 @@ ItemDelegate { Layout.preferredHeight: unifiedSearchResultSkeletonItemDetails.iconWidth Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + color: Style.sesTrayFontColor + font.pixelSize: Style.sesFontPixelSize + font.weight: Style.sesFontNormalWeight + Layout.fillWidth: true horizontalAlignment: Text.AlignLeft diff --git a/src/gui/filedetails/ShareeSearchField.qml b/src/gui/filedetails/ShareeSearchField.qml index 8c82ed5abde50..d89f50d5eb74e 100644 --- a/src/gui/filedetails/ShareeSearchField.qml +++ b/src/gui/filedetails/ShareeSearchField.qml @@ -8,8 +8,9 @@ import QtQuick.Window import QtQuick.Layouts import QtQuick.Controls -import com.nextcloud.desktopclient -import com.nextcloud.desktopclient as NC +import com.ionos.hidrivenext.desktopclient +// TODO Check SES-459 +import com.ionos.hidrivenext.desktopclient as NC import Style import "../tray" @@ -30,6 +31,7 @@ TextField { } readonly property int horizontalPaddingOffset: Style.trayHorizontalMargin + readonly property color placeholderColor: Style.sesSearchFieldContent readonly property double iconsScaleFactor: 0.6 function triggerSuggestionsVisibility() { @@ -37,9 +39,13 @@ TextField { } placeholderText: enabled ? qsTr("Search for users or groups…") : qsTr("Sharing is not available for this folder") + placeholderTextColor: placeholderColor + color: Style.sesTrayFontColor + Component.onCompleted: contentItem.cursorColor = Style.sesTrayFontColor verticalAlignment: Qt.AlignVCenter implicitHeight: Math.max(Style.talkReplyTextFieldPreferredHeight, contentHeight) + onActiveFocusChanged: triggerSuggestionsVisibility() onTextChanged: triggerSuggestionsVisibility() Keys.onPressed: { @@ -79,8 +85,15 @@ TextField { } } - leftPadding: searchIcon.width + searchIcon.anchors.leftMargin + horizontalPaddingOffset - rightPadding: clearTextButton.width + clearTextButton.anchors.rightMargin + horizontalPaddingOffset + leftPadding: searchIcon.width + searchIcon.anchors.leftMargin + horizontalPaddingOffset - 5 + rightPadding: root.text ? clearTextButton.width + clearTextButton.anchors.rightMargin + horizontalPaddingOffset : 5 + + background: Rectangle { + radius: 5 + border.color: Style.sesMenuBorder + border.width: 1 + + } Image { id: searchIcon @@ -99,7 +112,7 @@ TextField { fillMode: Image.PreserveAspectFit horizontalAlignment: Image.AlignLeft - source: "image://svgimage-custom-color/search.svg" + "/" + palette.placeholderText + source: "image://svgimage-custom-color/search.svg" + "/" + Style.sesSearchFieldContent sourceSize: Qt.size(parent.height * root.iconsScaleFactor, parent.height * root.iconsScaleFactor) visible: !root.shareeModel.fetchOngoing @@ -155,6 +168,11 @@ TextField { width: root.width y: root.height + background: Rectangle { + color: Style.sesBackgroundColor + border.color: Style.sesMenuBorder + } + contentItem: ScrollView { id: suggestionsScrollView @@ -175,7 +193,7 @@ TextField { highlight: Rectangle { anchors.fill: shareeListView.currentItem - color: palette.highlight + color: Style.sesHover } highlightFollowsCurrentItem: true highlightMoveDuration: 0 diff --git a/src/gui/filedetails/shareemodel.cpp b/src/gui/filedetails/shareemodel.cpp index 4693da29724d6..368b9f20c567b 100644 --- a/src/gui/filedetails/shareemodel.cpp +++ b/src/gui/filedetails/shareemodel.cpp @@ -14,7 +14,7 @@ namespace OCC { -Q_LOGGING_CATEGORY(lcShareeModel, "com.nextcloud.shareemodel") +Q_LOGGING_CATEGORY(lcShareeModel, "com.hidrivenext.shareemodel") ShareeModel::ShareeModel(QObject *parent) : QAbstractListModel(parent) diff --git a/src/gui/filedetails/sharemodel.cpp b/src/gui/filedetails/sharemodel.cpp index 06d1cb0246843..afea2c9c9c401 100644 --- a/src/gui/filedetails/sharemodel.cpp +++ b/src/gui/filedetails/sharemodel.cpp @@ -28,7 +28,7 @@ static const auto secureFileDropPlaceholderLinkShareId = QStringLiteral("__secur namespace OCC { -Q_LOGGING_CATEGORY(lcShareModel, "com.nextcloud.sharemodel") +Q_LOGGING_CATEGORY(lcShareModel, "com.hidrivenext.sharemodel") ShareModel::ShareModel(QObject *parent) : QAbstractListModel(parent) @@ -773,7 +773,7 @@ QString ShareModel::iconUrlForShare(const SharePtr &share) const switch(share->getShareType()) { case Share::TypeInternalLink: - return QString(iconsPath + QStringLiteral("external.svg")); + return QString(iconsPath + QStringLiteral("public.svg")); case Share::TypePlaceholderLink: case Share::TypeSecureFileDropPlaceholderLink: case Share::TypeLink: diff --git a/src/gui/filedetails/sortedsharemodel.cpp b/src/gui/filedetails/sortedsharemodel.cpp index 14a06a837b0ab..5fffc1ca891c6 100644 --- a/src/gui/filedetails/sortedsharemodel.cpp +++ b/src/gui/filedetails/sortedsharemodel.cpp @@ -7,7 +7,7 @@ namespace OCC { -Q_LOGGING_CATEGORY(lcSortedShareModel, "com.nextcloud.sortedsharemodel") +Q_LOGGING_CATEGORY(lcSortedShareModel, "com.hidrivenext.sortedsharemodel") SortedShareModel::SortedShareModel(QObject *parent) : QSortFilterProxyModel(parent) diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index 132390ea9fe80..589ec2b9883d9 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -1408,10 +1408,10 @@ void Folder::slotNewBigFolderDiscovered(const QString &newF, bool isExternal) journal->setSelectiveSyncList(SyncJournalDb::SelectiveSyncUndecidedList, undecidedList); emit newBigFolderDiscovered(newFolder); } - QString message = !isExternal ? (tr("A new folder larger than %1 MB has been added: %2.\n") + QString message = !isExternal ? QString(tr("A new folder larger than %1 MB has been added: %2.") + "\n") .arg(ConfigFile().newBigFolderSizeLimit().second) - .arg(newF)) - : (tr("A folder from an external storage has been added.\n")); + .arg(newF) + : (tr("A folder from an external storage has been added.") + "\n"); message += tr("Please go in the settings to select it if you wish to download it."); auto logger = Logger::instance(); @@ -1693,8 +1693,8 @@ void Folder::registerFolderWatcher() } connect(_folderWatcher.data(), &FolderWatcher::filesLockImposed, this, &Folder::slotFilesLockImposed, Qt::UniqueConnection); _folderWatcher->init(path()); - _folderWatcher->startNotificatonTest(path() + QLatin1String(".nextcloudsync.log")); - _folderWatcher->performSetPermissionsTest(path() + QLatin1String(".nextcloudpermissions.log")); + _folderWatcher->startNotificatonTest(path() + QLatin1String(".hidrivenextsync.log")); + _folderWatcher->performSetPermissionsTest(path() + QLatin1String(".hidrivenextpermissions.log")); connect(_engine.data(), &SyncEngine::lockFileDetected, _folderWatcher.data(), &FolderWatcher::slotLockFileDetectedExternally); } diff --git a/src/gui/foldercreationdialog.cpp b/src/gui/foldercreationdialog.cpp index 93e243842cf96..a956a8111e10a 100644 --- a/src/gui/foldercreationdialog.cpp +++ b/src/gui/foldercreationdialog.cpp @@ -4,6 +4,9 @@ */ #include "foldercreationdialog.h" + +#include "buttonstyle.h" +#include "whitelabeltheme.h" #include "ui_foldercreationdialog.h" #include @@ -11,6 +14,9 @@ #include #include #include +#include +#include +#include namespace OCC { @@ -22,10 +28,13 @@ FolderCreationDialog::FolderCreationDialog(const QString &destination, QWidget * , _destination(destination) { ui->setupUi(this); + setWindowTitle(tr("%1 Create new folder").arg(Theme::instance()->appNameGUI())); + customizeStyle(); - ui->labelErrorMessage->setVisible(false); + ui->errorSnackbar->setVisible(false); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + setWindowFlags(windowFlags() | Qt::Dialog | Qt::WindowMinMaxButtonsHint); connect(ui->newFolderNameEdit, &QLineEdit::textChanged, this, &FolderCreationDialog::slotNewFolderNameEditTextEdited); @@ -60,11 +69,6 @@ void FolderCreationDialog::accept() const auto fullPath = QString(_destination + "/" + ui->newFolderNameEdit->text()); - if (QDir(fullPath).exists()) { - ui->labelErrorMessage->setVisible(true); - return; - } - if (QDir(_destination).mkdir(ui->newFolderNameEdit->text())) { Q_EMIT folderCreated(fullPath); } else { @@ -77,10 +81,54 @@ void FolderCreationDialog::accept() void FolderCreationDialog::slotNewFolderNameEditTextEdited() { if (!ui->newFolderNameEdit->text().isEmpty() && QDir(_destination + "/" + ui->newFolderNameEdit->text()).exists()) { - ui->labelErrorMessage->setVisible(true); + ui->errorSnackbar->setVisible(true); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + sizeDialog(); + } else { - ui->labelErrorMessage->setVisible(false); + ui->errorSnackbar->setVisible(false); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + sizeDialog(); } } +void FolderCreationDialog::sizeDialog(){ + adjustSize(); + setFixedWidth(626); + setFixedHeight(sizeHint().height()); +} + +void FolderCreationDialog::customizeStyle() +{ + ui->buttonBox->setLayoutDirection(Qt::RightToLeft); + + this->setAutoFillBackground(true); + setPalette(QPalette(QPalette::Window, WLTheme.dialogBackgroundColor())); + + QPushButton *okButton = ui->buttonBox->button(QDialogButtonBox::Ok); + okButton->setProperty("buttonStyle", QVariant::fromValue(OCC::ButtonStyleName::Primary)); + + QHBoxLayout* buttonlayout = qobject_cast(ui->buttonBox->layout()); + buttonlayout->setSpacing(16); + + ui->newFolderNameEdit->setStyleSheet( + QStringLiteral( + "color: %1; font-family: %2; font-size: %3; font-weight: %4; border-radius: %5; border: 1px " + "solid %6; padding: 0px 12px; text-align: left; vertical-align: middle; height: 40px; background: %7; ") + .arg(WLTheme.folderWizardPathColor(), + WLTheme.settingsFont(), + WLTheme.settingsTextSize(), + WLTheme.settingsTextWeight(), + WLTheme.buttonRadius(), + WLTheme.menuBorderColor(), + WLTheme.white() + ) + ); + +#if defined(Q_OS_MAC) + buttonlayout->setSpacing(32); +#endif + + sizeDialog(); } +} \ No newline at end of file diff --git a/src/gui/foldercreationdialog.h b/src/gui/foldercreationdialog.h index d1c7ecb90a2d0..f618ecf14d6a2 100644 --- a/src/gui/foldercreationdialog.h +++ b/src/gui/foldercreationdialog.h @@ -34,6 +34,9 @@ private slots: Ui::FolderCreationDialog *ui; QString _destination; + + void customizeStyle(); + void sizeDialog(); }; } diff --git a/src/gui/foldercreationdialog.ui b/src/gui/foldercreationdialog.ui index 84d7c77e18654..de849b09215cd 100644 --- a/src/gui/foldercreationdialog.ui +++ b/src/gui/foldercreationdialog.ui @@ -1,100 +1,93 @@ - OCC::FolderCreationDialog - - - - 0 - 0 - 355 - 138 - - - - Create new folder - - - - - 0 - 90 - 341 - 32 - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - 20 - 30 - 321 - 22 - - - - Enter folder name - - - - - true - - - - 20 - 60 - 321 - 16 - - - - color: rgb(255, 0, 0) - - - Folder already exists - - - - - - - buttonBox - accepted() - OCC::FolderCreationDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - OCC::FolderCreationDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - + OCC::FolderCreationDialog + + + Create new folder + + + + + + Enter folder name + + + Qt::NoContextMenu + + + + + + + Folder already exists + + + false + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + + + + buttonBox + accepted() + OCC::FolderCreationDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + OCC::FolderCreationDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + \ No newline at end of file diff --git a/src/gui/folderman.cpp b/src/gui/folderman.cpp index 0ba7b7e8fc189..e687b06c7bfc3 100644 --- a/src/gui/folderman.cpp +++ b/src/gui/folderman.cpp @@ -1456,12 +1456,12 @@ QString FolderMan::getBackupName(QString fullPathName) const if (fullPathName.isEmpty()) return QString(); - QString newName = fullPathName + tr(" (backup)"); + QString newName = fullPathName + " " + tr("(backup)"); QFileInfo fi(newName); int cnt = 2; do { if (fi.exists()) { - newName = fullPathName + tr(" (backup %1)").arg(cnt++); + newName = fullPathName + " " + tr("(backup %1)").arg(cnt++); fi.setFile(newName); } } while (fi.exists()); diff --git a/src/gui/folderstatusdelegate.cpp b/src/gui/folderstatusdelegate.cpp index b8453dc5cbb40..0b620b252a7e7 100644 --- a/src/gui/folderstatusdelegate.cpp +++ b/src/gui/folderstatusdelegate.cpp @@ -9,7 +9,11 @@ #include "folderstatusview.h" #include "folderman.h" #include "accountstate.h" +#include "sesstyle.h" +#include "buttonstyle.h" + #include +#include #include #include @@ -17,14 +21,7 @@ #include #include #include - -inline static QFont makeAliasFont(const QFont &normalFont) -{ - QFont aliasFont = normalFont; - aliasFont.setBold(true); - aliasFont.setPointSize(normalFont.pointSize() + 2); - return aliasFont; -} +#include namespace { #ifdef Q_OS_MACOS @@ -34,6 +31,14 @@ namespace { namespace OCC { +inline static QFont makeAliasFont(const QFont &normalFont) +{ + QFont aliasFont = normalFont; + aliasFont.setWeight(WLTheme.settingsTitleWeightDemiBold()); + aliasFont.setPixelSize(WLTheme.settingsBigTitlePixel()); + return aliasFont; +} + FolderStatusDelegate::FolderStatusDelegate() : QStyledItemDelegate() { @@ -42,15 +47,21 @@ FolderStatusDelegate::FolderStatusDelegate() QString FolderStatusDelegate::addFolderText() { - return tr("Add Folder Sync Connection"); + return tr("Add Folder Sync"); +} + +QString FolderStatusDelegate::addInfoText() +{ + return tr("Synchronize any other local folder with your %1").arg(Theme::instance()->appNameGUI()); } // allocate each item size in listview. QSize FolderStatusDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { - QFont aliasFont = makeAliasFont(option.font); - QFont font = option.font; + QFont font = QFont(option.font); + font.setPixelSize(WLTheme.settingsTextPixel()); + QFont aliasFont = makeAliasFont(font); QFontMetrics fm(font); QFontMetrics aliasFm(aliasFont); @@ -61,7 +72,7 @@ QSize FolderStatusDelegate::sizeHint(const QStyleOptionViewItem &option, QFontMetrics fm(qApp->font("QPushButton")); QStyleOptionButton opt; static_cast(opt) = option; - opt.text = addFolderText(); + opt.text = addInfoText(); return QApplication::style()->sizeFromContents( QStyle::CT_PushButton, &opt, fm.size(Qt::TextSingleLine, opt.text)) + QSize(0, margins); } @@ -101,22 +112,78 @@ int FolderStatusDelegate::rootFolderHeightWithoutErrors(const QFontMetrics &fm, return h; } +void FolderStatusDelegate::drawAddButton(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QFont titleFont = option.font; + titleFont.setWeight(WLTheme.settingsTitleWeightDemiBold()); + titleFont.setPixelSize(WLTheme.settingsTitlePixel()); + QFontMetrics titleTextFm(titleFont); + const auto baseDistanceForCalculus = titleTextFm.height() / 2; + + QFont subtitleFont = option.font; + + QFontMetrics subtitleTextFm(subtitleFont); + const auto distanceToSubline = subtitleTextFm.height() / 4; + + auto iconBox = option.rect; + iconBox.setTop(iconBox.top() + baseDistanceForCalculus); + iconBox.setBottom(iconBox.top() + WLTheme.treeViewIconSize()); + iconBox.setLeft(iconBox.left() + baseDistanceForCalculus); + iconBox.setWidth(iconBox.height()); + + auto titleBox = option.rect; + titleBox.setTop(iconBox.top()); + titleBox.setBottom(iconBox.bottom() - distanceToSubline); + titleBox.setRight(titleBox.right() - baseDistanceForCalculus); + titleBox.setLeft(iconBox.right() + baseDistanceForCalculus); + + auto subtitleBox = option.rect; + subtitleBox.setTop(titleBox.bottom() + distanceToSubline); + subtitleBox.setBottom(subtitleBox.top() + 4 * distanceToSubline); + subtitleBox.setLeft(iconBox.right() + baseDistanceForCalculus); + subtitleBox.setRight(subtitleBox.right() - baseDistanceForCalculus); + + auto titleText = addFolderText(); + auto subtitleText = addInfoText(); + auto addIcon = QIcon(WLTheme.liveBackupPlusIcon()); + const auto addPixmap = addIcon.pixmap(iconBox.size(), QIcon::Normal); + + painter->save(); + painter->drawPixmap(QStyle::visualRect(option.direction, option.rect, iconBox).left(), iconBox.top(), addPixmap); + + drawElidedText(painter, option, titleTextFm, titleFont, titleText, titleBox); + + drawElidedText(painter, option, subtitleTextFm, subtitleFont, subtitleText, subtitleBox); + + painter->restore(); +} + +void FolderStatusDelegate::drawElidedText(QPainter *painter, QStyleOptionViewItem option, QFontMetrics fontMetric, QFont font, QString text, QRect rect) const{ + const auto elidedText = fontMetric.elidedText(text, Qt::ElideRight, rect.width()); + painter->setFont(font); + painter->drawText(QStyle::visualRect(option.direction, option.rect, rect), Qt::AlignLeft, elidedText); +} + void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { + QStyleOptionViewItem opt = option; + QFont font = opt.font; + font.setPixelSize(WLTheme.settingsTextPixel()); + opt.font = font; + + QStyledItemDelegate::paint(painter, opt, index); + if (index.data(AddButton).toBool()) { - const_cast(option).showDecorationSelected = false; + drawAddButton(painter, opt, index); + return; } - QStyledItemDelegate::paint(painter, option, index); - auto textAlign = Qt::AlignLeft; - const auto aliasFont = makeAliasFont(option.font); - const auto subFont = option.font; - const auto errorFont = subFont; - auto progressFont = subFont; + const auto aliasFont = makeAliasFont(opt.font); + const auto subFont = opt.font; - progressFont.setPointSize(subFont.pointSize() - 2); + const auto errorFont = subFont; QFontMetrics subFm(subFont); QFontMetrics aliasFm(aliasFont); @@ -124,23 +191,6 @@ void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem & const auto aliasMargin = aliasFm.height() / 2; const auto margin = subFm.height() / 4; - if (index.data(AddButton).toBool()) { - QStyleOptionButton opt; - static_cast(opt) = option; - if (opt.state & QStyle::State_Enabled && opt.state & QStyle::State_MouseOver && index == _pressedIndex) { - opt.state |= QStyle::State_Sunken; - } else { - opt.state |= QStyle::State_Raised; - } - opt.text = addFolderText(); - opt.rect = addButtonRect(option.rect, option.direction); - painter->save(); - painter->setFont(qApp->font("QPushButton")); - QApplication::style()->drawControl(QStyle::CE_PushButton, &opt, painter, option.widget); - painter->restore(); - return; - } - if (dynamic_cast(index.model())->classify(index) != FolderStatusModel::RootFolder) { return; } @@ -153,7 +203,6 @@ void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem & auto errorTexts = qvariant_cast(index.data(FolderErrorMsg)); auto infoTexts = qvariant_cast(index.data(FolderInfoMsg)); - auto overallPercent = qvariant_cast(index.data(SyncProgressOverallPercent)); auto overallString = qvariant_cast(index.data(SyncProgressOverallString)); auto itemString = qvariant_cast(index.data(SyncProgressItemString)); auto syncEnabled = qvariant_cast(index.data(FolderAccountConnected)); @@ -180,7 +229,7 @@ void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem & localPathRect.setTop(remotePathRect.bottom() + margin); localPathRect.setBottom(localPathRect.top() + subFm.height()); - iconRect.setBottom(localPathRect.bottom()); + iconRect.setBottom(iconRect.top() + WLTheme.treeViewIconSize()); iconRect.setWidth(iconRect.height()); const auto nextToIcon = iconRect.right() + aliasMargin; @@ -188,7 +237,7 @@ void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem & localPathRect.setLeft(nextToIcon); remotePathRect.setLeft(nextToIcon); - auto optionsButtonVisualRect = optionsButtonRect(option.rect, option.direction); + const auto iconSize = iconRect.width(); statusIcon.paint( painter, @@ -196,6 +245,10 @@ void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem & Qt::AlignCenter, syncEnabled ? QIcon::Normal : QIcon::Disabled ); + + // TODO SES-459 Check if working + // const auto statusPixmap = statusIcon.pixmap(iconSize, iconSize, syncEnabled ? QIcon::Normal : QIcon::Disabled); + // painter->drawPixmap(QStyle::visualRect(option.direction, option.rect, iconRect).left(), iconRect.top(), statusPixmap); auto palette = option.palette; @@ -210,37 +263,32 @@ void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem & painter->setPen(palette.color(colourGroup, QPalette::Text)); } - const auto elidedAlias = aliasFm.elidedText(aliasText, Qt::ElideRight, aliasRect.width()); - painter->setFont(aliasFont); - painter->drawText(QStyle::visualRect(option.direction, option.rect, aliasRect), textAlign, elidedAlias); + drawElidedText(painter, option, aliasFm, aliasFont, aliasText, aliasRect); const auto showProgess = !overallString.isEmpty() || !itemString.isEmpty(); if (!showProgess) { - painter->setFont(subFont); - const auto elidedRemotePathText = subFm.elidedText(syncText, Qt::ElideRight, remotePathRect.width()); - painter->drawText(QStyle::visualRect(option.direction, option.rect, remotePathRect), textAlign, elidedRemotePathText); + drawElidedText(painter, option, subFm, subFont, syncText, remotePathRect); - const auto elidedPathText = subFm.elidedText(pathText, Qt::ElideMiddle, localPathRect.width()); - painter->drawText(QStyle::visualRect(option.direction, option.rect, localPathRect), textAlign, elidedPathText); + drawElidedText(painter, option, subFm, subFont, pathText, localPathRect); } - auto textBoxTop = iconRect.bottom() + margin; + auto textBoxTop = qMax(localPathRect.bottom(), remotePathRect.bottom()) + margin; // paint an error overlay if there is an error string or conflict string - auto drawTextBox = [&](const QStringList &texts, QColor color) { + auto drawTextBox = [&](const QStringList &texts, QColor color, QColor borderColor) { auto rect = localPathRect; rect.setLeft(iconRect.left()); rect.setTop(textBoxTop); rect.setHeight(texts.count() * subFm.height() + 2 * margin); - rect.setRight(option.rect.right() - margin); + rect.setRight(option.rect.right() - aliasMargin); // save previous state to not mess up colours with the background (fixes issue: https://github.com/nextcloud/desktop/issues/1237) painter->save(); painter->setBrush(color); - painter->setPen(QColor(0xaa, 0xaa, 0xaa)); + painter->setPen(borderColor); painter->drawRoundedRect(QStyle::visualRect(option.direction, option.rect, rect), 4, 4); - painter->setPen(Qt::white); + painter->setPen(Qt::black); painter->setFont(errorFont); QRect textRect(rect.left() + margin, rect.top() + margin, @@ -258,73 +306,124 @@ void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem & }; if (!conflictTexts.isEmpty()) { - drawTextBox(conflictTexts, QColor(0xba, 0xba, 0x4d)); + drawTextBox(conflictTexts, QColor(WLTheme.warningColor()), QColor(WLTheme.warningBorderColor())); } if (!errorTexts.isEmpty()) { - drawTextBox(errorTexts, QColor(0xbb, 0x4d, 0x4d)); + drawTextBox(errorTexts, QColor(WLTheme.errorColor()), QColor(WLTheme.errorBorderColor())); } if (!infoTexts.isEmpty()) { - drawTextBox(infoTexts, QColor(0x4d, 0x4d, 0xba)); + drawTextBox(infoTexts, QColor(WLTheme.infoColor()), QColor(WLTheme.infoBorderColor())); } // Sync File Progress Bar: Show it if syncFile is not empty. if (showProgess) { - const auto fileNameTextHeight = subFm.boundingRect(tr("File")).height(); - constexpr auto barHeight = 7; // same height as quota bar - const auto overallWidth = option.rect.right() - aliasMargin - optionsButtonVisualRect.width() - nextToIcon; + drawSyncProgressBar(painter, opt, index, subFm, aliasMargin, remotePathRect, margin, nextToIcon); + } - painter->save(); + drawMoreOptionsButton(painter, option, index); +} + +void FolderStatusDelegate::drawSyncProgressBar(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index, const QFontMetrics &subFm, const int aliasMargin, const QRect &remotePathRect, const int margin, const int nextToIcon) const +{ + auto overallPercent = qvariant_cast(index.data(SyncProgressOverallPercent)); + auto overallString = qvariant_cast(index.data(SyncProgressOverallString)); + auto optionsButtonVisualRect = optionsButtonRect(option.rect, option.direction); + + const auto fileNameTextHeight = subFm.boundingRect(tr("File")).height(); + constexpr auto barHeight = 7; // same height as quota bar + const auto overallWidth = option.rect.right() - aliasMargin - optionsButtonVisualRect.width() - nextToIcon; + + QFont progressFont(option.font); + progressFont.setPixelSize(WLTheme.settingsTextPixel()); + progressFont.setWeight(WLTheme.settingsTitleWeightNormal()); + painter->save(); // Overall Progress Bar. - const auto progressBarRect = QRect(nextToIcon, - remotePathRect.top(), - overallWidth - 2 * margin, - barHeight); - - QStyleOptionProgressBar progressBarOpt; - - progressBarOpt.state = option.state | QStyle::State_Horizontal; - progressBarOpt.minimum = 0; - progressBarOpt.maximum = 100; - progressBarOpt.progress = overallPercent; - progressBarOpt.state = QStyle::StateFlag::State_Horizontal; - progressBarOpt.rect = QStyle::visualRect(option.direction, option.rect, progressBarRect); + const auto progressBarRect = QRect(nextToIcon, + remotePathRect.top(), + overallWidth - 2 * margin, + barHeight); + + QStyleOptionProgressBar progressBarOpt; + + progressBarOpt.state = option.state | QStyle::State_Horizontal; + progressBarOpt.minimum = 0; + progressBarOpt.maximum = 100; + progressBarOpt.progress = overallPercent; + progressBarOpt.state = QStyle::StateFlag::State_Horizontal; + progressBarOpt.rect = QStyle::visualRect(option.direction, option.rect, progressBarRect); + QPalette paletteTmp = progressBarOpt.palette; + paletteTmp.setColor(QPalette::Base, WLTheme.white()); + paletteTmp.setColor(QPalette::Highlight, WLTheme.syncProgressColor()); + progressBarOpt.palette = paletteTmp; + #ifdef Q_OS_MACOS - backupStyle->drawControl(QStyle::CE_ProgressBar, &progressBarOpt, painter, option.widget); + backupStyle->drawControl(QStyle::CE_ProgressBar, &progressBarOpt, painter, option.widget); #else - QApplication::style()->drawControl(QStyle::CE_ProgressBar, &progressBarOpt, painter, option.widget); + QApplication::style()->drawControl(QStyle::CE_ProgressBar, &progressBarOpt, painter, option.widget); #endif - // itemString is e.g. Syncing fileName1, filename2 - // syncText is Synchronizing files in local folders or Synchronizing virtual files in local folder - const auto generalSyncStatus = !itemString.isEmpty() ? itemString : syncText; - QRect generalSyncStatusRect; - generalSyncStatusRect.setTop(progressBarRect.bottom() + margin); - generalSyncStatusRect.setHeight(fileNameTextHeight); - generalSyncStatusRect.setLeft(progressBarRect.left()); - generalSyncStatusRect.setWidth(progressBarRect.width()); - painter->setFont(progressFont); +// Overall Progress Text + QRect overallProgressRect; + overallProgressRect.setTop(progressBarRect.bottom() + margin); + overallProgressRect.setHeight(fileNameTextHeight); + overallProgressRect.setLeft(progressBarRect.left()); + overallProgressRect.setWidth(progressBarRect.width()); - painter->drawText(QStyle::visualRect(option.direction, option.rect, generalSyncStatusRect), Qt::AlignLeft | Qt::AlignVCenter, generalSyncStatus); + painter->setFont(progressFont); - painter->restore(); - } + painter->drawText(QStyle::visualRect(option.direction, option.rect, overallProgressRect), Qt::AlignLeft | Qt::AlignVCenter, overallString); + + // // itemString is e.g. Syncing fileName1, filename2 + // // syncText is Synchronizing files in local folders or Synchronizing virtual files in local folder + // const auto generalSyncStatus = !itemString.isEmpty() ? itemString : syncText; + // QRect generalSyncStatusRect; + // generalSyncStatusRect.setTop(progressBarRect.bottom() + margin); + // generalSyncStatusRect.setHeight(fileNameTextHeight); + // generalSyncStatusRect.setLeft(progressBarRect.left()); + // generalSyncStatusRect.setWidth(progressBarRect.width()); + // // painter->setFont(progressFont); + + // painter->drawText(QStyle::visualRect(option.direction, option.rect, generalSyncStatusRect), Qt::AlignLeft | Qt::AlignVCenter, generalSyncStatus); painter->restore(); +} + +void FolderStatusDelegate::drawMoreOptionsButton(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + auto optionsButtonVisualRect = optionsButtonRect(option.rect, option.direction); + + QStyleOptionButton btnOpt; + btnOpt.state = option.state; + btnOpt.state &= ~(QStyle::State_Selected | QStyle::State_HasFocus | QStyle::State_MouseOver); + btnOpt.state |= QStyle::State_Raised; + if(optionsButtonVisualRect.contains(MousePos) ) { - QStyleOptionToolButton btnOpt; - btnOpt.state = option.state; - btnOpt.state &= ~(QStyle::State_Selected | QStyle::State_HasFocus); + btnOpt.state |= QStyle::State_MouseOver; + } + + if (btnOpt.state & QStyle::State_Enabled && btnOpt.state & QStyle::State_MouseOver && index == _pressedIndex) { + btnOpt.state |= QStyle::State_Sunken; + } else { btnOpt.state |= QStyle::State_Raised; - btnOpt.arrowType = Qt::NoArrow; - btnOpt.subControls = QStyle::SC_ToolButton; - btnOpt.rect = optionsButtonVisualRect; - btnOpt.icon = _iconMore; - const auto buttonSize = QApplication::style()->pixelMetric(QStyle::PM_ButtonIconSize); - btnOpt.iconSize = QSize(buttonSize, buttonSize); - QApplication::style()->drawComplexControl(QStyle::CC_ToolButton, &btnOpt, painter); } + + btnOpt.rect = optionsButtonVisualRect; + btnOpt.icon = _iconMore; + const auto iconSize = optionsButtonIconSize(); + btnOpt.iconSize = QSize(iconSize, iconSize); + QWidget buttonWidget; + buttonWidget.setProperty("buttonStyle", QVariant::fromValue(OCC::ButtonStyleName::MoreOptions)); + + QApplication::style()-> + drawControl( + static_cast(sesStyle::CE_TreeViewMoreOptions), &btnOpt, painter, &buttonWidget); +} + +int FolderStatusDelegate::optionsButtonIconSize() { + // Using this calculation to use the DPI-Scaled values. The QStyleHelper::dpiScaled is not accessible from here. + return QApplication::style()->pixelMetric(QStyle::PM_LargeIconSize) - QApplication::style()->pixelMetric(QStyle::PM_MenuScrollerHeight); } bool FolderStatusDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, @@ -335,12 +434,24 @@ bool FolderStatusDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, case QEvent::MouseMove: if (const auto *view = qobject_cast(option.widget)) { auto *me = dynamic_cast(event); - QModelIndex index; + QModelIndex pressedIndex; if (me->buttons()) { - index = view->indexAt(me->pos()); + pressedIndex = view->indexAt(me->pos()); } - if (_pressedIndex != index) { - _pressedIndex = index; + if (_pressedIndex != pressedIndex) { + _pressedIndex = pressedIndex; + view->viewport()->update(); + } + auto optionsButtonVisualRect = optionsButtonRect(option.rect, option.direction); + + MousePos = me->pos(); + if(optionsButtonVisualRect.contains(MousePos)) + { + _hoveredIndex = index; + view->viewport()->update(); + } else if(_hoveredIndex.isValid()) + { + _hoveredIndex = QModelIndex(); view->viewport()->update(); } } @@ -356,37 +467,29 @@ bool FolderStatusDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, QRect FolderStatusDelegate::optionsButtonRect(QRect within, Qt::LayoutDirection direction) { - QFont font = QFont(); + QFont font = QFont(WLTheme.settingsFont()); QFont aliasFont = makeAliasFont(font); QFontMetrics fm(font); QFontMetrics aliasFm(aliasFont); within.setHeight(FolderStatusDelegate::rootFolderHeightWithoutErrors(fm, aliasFm)); - QStyleOptionToolButton opt; - int e = QApplication::style()->pixelMetric(QStyle::PM_ButtonIconSize); - opt.rect.setSize(QSize(e,e)); - QSize size = QApplication::style()->sizeFromContents(QStyle::CT_ToolButton, &opt, opt.rect.size()); - - int margin = QApplication::style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing); - QRect r(QPoint(within.right() - size.width() - margin, + QStyleOptionButton opt; + int iconSize = optionsButtonIconSize(); + opt.rect.setSize(QSize(iconSize,iconSize)); + QSize size = QApplication::style()->sizeFromContents( + static_cast(sesStyle::CT_TreeViewMoreOptions), &opt, opt.rect.size()); + + // Using PM_LargeIconSize as margin because it get DPI Scaled, which I canot access from here + int margin = QApplication::style()->pixelMetric(QStyle::PM_LargeIconSize); + QRect r(QPoint(within.right() - size.width() - aliasFm.height() / 2, within.top() + within.height() / 2 - size.height() / 2), size); return QStyle::visualRect(direction, within, r); } -QRect FolderStatusDelegate::addButtonRect(QRect within, Qt::LayoutDirection direction) -{ - QFontMetrics fm(qApp->font("QPushButton")); - QStyleOptionButton opt; - opt.text = addFolderText(); - QSize size = QApplication::style()->sizeFromContents(QStyle::CT_PushButton, &opt, fm.size(Qt::TextSingleLine, opt.text)); - QRect r(QPoint(within.left(), within.top() + within.height() / 2 - size.height() / 2), size); - return QStyle::visualRect(direction, within, r); -} - QRect FolderStatusDelegate::errorsListRect(QRect within) { - QFont font = QFont(); + QFont font = QFont(WLTheme.settingsFont()); QFont aliasFont = makeAliasFont(font); QFontMetrics fm(font); QFontMetrics aliasFm(aliasFont); @@ -401,7 +504,7 @@ void FolderStatusDelegate::slotStyleChanged() void FolderStatusDelegate::customizeStyle() { - _iconMore = Theme::createColorAwareIcon(QLatin1String(":/client/theme/more.svg")); + _iconMore = Theme::createColorAwareIcon(QLatin1String(":/client/theme/ses/ses-more.svg"), QSize(128, 128)); } } // namespace OCC diff --git a/src/gui/folderstatusdelegate.h b/src/gui/folderstatusdelegate.h index 4bb2bfe0c0a0d..2eabbdcbb4c11 100644 --- a/src/gui/folderstatusdelegate.h +++ b/src/gui/folderstatusdelegate.h @@ -18,6 +18,7 @@ class FolderStatusDelegate : public QStyledItemDelegate Q_OBJECT public: FolderStatusDelegate(); + QPoint MousePos; enum datarole { FolderAliasRole = Qt::UserRole + 100, HeaderRole, @@ -48,8 +49,9 @@ class FolderStatusDelegate : public QStyledItemDelegate /** * return the position of the option button within the item */ + + static QRect optionsButtonRect(QRect within, Qt::LayoutDirection direction); - static QRect addButtonRect(QRect within, Qt::LayoutDirection direction); static QRect errorsListRect(QRect within); static int rootFolderHeightWithoutErrors(const QFontMetrics &fm, const QFontMetrics &aliasFm); @@ -58,9 +60,17 @@ public slots: private: void customizeStyle(); + void drawAddButton(QPainter *,const QStyleOptionViewItem &, const QModelIndex &) const; + void drawElidedText(QPainter *painter, QStyleOptionViewItem option, QFontMetrics fontMetric, QFont font, QString text, QRect rect) const; + void drawSyncProgressBar(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index, const QFontMetrics &subFm, const int aliasMargin, const QRect &remotePathRect, const int margin, const int nextToIcon) const; + void drawMoreOptionsButton(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; + + static int optionsButtonIconSize(); static QString addFolderText(); + static QString addInfoText(); QPersistentModelIndex _pressedIndex; + QPersistentModelIndex _hoveredIndex; QIcon _iconMore; }; diff --git a/src/gui/folderstatusview.cpp b/src/gui/folderstatusview.cpp index 86d16e6fc1238..05fe7cac5f59b 100644 --- a/src/gui/folderstatusview.cpp +++ b/src/gui/folderstatusview.cpp @@ -5,11 +5,15 @@ #include "folderstatusview.h" #include "folderstatusdelegate.h" +#include "whitelabeltheme.h" namespace OCC { FolderStatusView::FolderStatusView(QWidget *parent) : QTreeView(parent) { + #ifdef Q_OS_MAC + setPalette(QPalette(QPalette::ButtonText, WLTheme.white())); + #endif } QModelIndex FolderStatusView::indexAt(const QPoint &point) const @@ -23,11 +27,7 @@ QModelIndex FolderStatusView::indexAt(const QPoint &point) const QRect FolderStatusView::visualRect(const QModelIndex &index) const { - QRect rect = QTreeView::visualRect(index); - if (index.data(FolderStatusDelegate::AddButton).toBool()) { - return FolderStatusDelegate::addButtonRect(rect, layoutDirection()); - } - return rect; + return QTreeView::visualRect(index); } } // namespace OCC diff --git a/src/gui/folderwizard.cpp b/src/gui/folderwizard.cpp index fae6d197a0649..3867035600283 100644 --- a/src/gui/folderwizard.cpp +++ b/src/gui/folderwizard.cpp @@ -12,7 +12,9 @@ #include "account.h" #include "selectivesyncdialog.h" #include "accountstate.h" +#include "buttonstyle.h" #include "creds/abstractcredentials.h" +#include "SesComponents/syncdirvalidation.h" #include "wizard/owncloudwizard.h" #include "common/asserts.h" @@ -22,11 +24,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -57,7 +61,7 @@ QString FormatWarningsWizardPage::formatWarnings(const QStringList &warnings) co if (warnings.count() == 1) { formattedWarning = Utility::escape(warnings.first()); } else if (warnings.count() > 1) { - formattedWarning = "