1
0
mirror of https://github.com/chylex/Nextcloud-Desktop.git synced 2026-04-04 03:11:32 +02:00

Compare commits

...

74 Commits

Author SHA1 Message Date
Michael Schuster
5880c4954e Bump version to 2.6.3
Signed-off-by: Michael Schuster <michael@schuster.ms>
2020-02-17 01:59:48 +01:00
Michael Schuster
b02bd066a9 Update translations
Signed-off-by: Michael Schuster <michael@schuster.ms>
2020-02-17 01:55:38 +01:00
Michael Schuster
6abec7cea9 Merge pull request #1789 from nextcloud/backport/1782/stable-2.6
[stable-2.6] Add UserInfo class and fetch quota via API instead of PropfindJob
2020-02-17 01:48:11 +01:00
Michael Schuster
cc4e6b236a Fix Tests linkage (missed UserInfo.cpp in CMakeLists.txt)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2020-02-17 00:31:37 +00:00
Michael Schuster
821946ad94 Code cleanup
Signed-off-by: Michael Schuster <michael@schuster.ms>
2020-02-17 00:31:37 +00:00
Michael Schuster
94c3e19ede Add UserInfo class and fetch quota via API instead of PropfindJob
The PropfindJob quota includes the size of shares and thus leads to confusion
in regard of the real space available, as shown in the UI.
This commit aims to streamline the behaviour with the Android and iOS apps,
which also utilize the API.

Details:
- Refactor the QuotaInfo class into UserInfo
- Use JsonApiJob (ocs/v1.php/cloud/user) instead of PropfindJob
- Let ConnectionValidator use the new UserInfo class to fetch
  the user and the avatar image (to avoid code duplication)
- Allow updating the avatar image upon AccountSettings visibility,
  using UserInfo's quota fetching

Signed-off-by: Michael Schuster <michael@schuster.ms>
2020-02-17 00:31:37 +00:00
rakekniven
feba6910ce Changed product name to Nextcloud
Reported at Transifex.

Signed-off-by: rakekniven <mark.ziegler@rakekniven.de>
(cherry picked from commit dfdb872e7b)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2020-02-16 19:01:39 +01:00
Michael Schuster
4f37249750 Merge pull request #1787 from nextcloud/backport/1770/stable-2.6
[stable-2.6] l10n: Changed grammar and triple dots to ellipsis
2020-02-16 18:56:55 +01:00
rakekniven
fda8c406f6 l10n: Changed grammar
Signed-off-by: rakekniven <mark.ziegler@rakekniven.de>
2020-02-16 17:56:03 +00:00
rakekniven
a804c4650a l10n: Triple dot to ellipsis
Signed-off-by: rakekniven <mark.ziegler@rakekniven.de>
2020-02-16 17:56:02 +00:00
rakekniven
e1f4963973 l10n: Triple dot to ellipsis
Signed-off-by: rakekniven <mark.ziegler@rakekniven.de>
2020-02-16 17:56:02 +00:00
rakekniven
6ae761c43c Triple dot to ellipsis
Signed-off-by: rakekniven <mark.ziegler@rakekniven.de>
2020-02-16 17:56:02 +00:00
rakekniven
504bb34d26 l10n: Triple dot to ellipsis
Signed-off-by: rakekniven <mark.ziegler@rakekniven.de>
2020-02-16 17:56:02 +00:00
rakekniven
1136cee383 l10n: Changed spelling of "user name" to "username"
Using "username" like on > 200 strings over the whole Nextcloud project.

Signed-off-by: rakekniven mark.ziegler@rakekniven.de
(cherry picked from commit 32c2c062c0)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2020-02-16 18:51:34 +01:00
Michael Schuster
591d4c812b Merge pull request #1786 from nextcloud/backport/1765/stable-2.6
[stable-2.6] Start the client in background if activated by D-Bus
2020-02-16 18:46:49 +01:00
Corentin Noël
a105e3f758 Start the client in background if activated by D-Bus
The nextcloud client can be started by any other application consuming libcloudproviers.
Make sure that the client won't pop-up if we open the file manager.

Signed-off-by: Corentin Noël <corentin.noel@collabora.com>
2020-02-16 17:46:20 +00:00
Michael Schuster
a88687bfe3 Merge pull request #1785 from nextcloud/backport/1764/stable-2.6
[stable-2.6] Do not install files related to cloud providers under Xenial
2020-02-16 18:43:46 +01:00
István Váradi
879ed544e1 Do not install files related to cloud providers under Xenial
Signed-off-by: István Váradi <Istvan.Varadi@ericsson.com>
2020-02-16 17:42:22 +00:00
Michael Schuster
13aaffc46b Merge pull request #1784 from nextcloud/backport/1760/stable-2.6
[stable-2.6] Update autoupdate.rst
2020-02-16 18:38:00 +01:00
Andre-Schuiki
62fc12fe40 Update autoupdate.rst
Hi, you have the wrong registry path in the documentation? (tested client version: 2.6.0 x64 build: 20190927)
The Nextcloud Client checks the path "HKEY_LOCAL_MACHINE\Software\Policies\Nextcloud GmbH\Nextcloud" not "HKEY_LOCAL_MACHINE\Software\Policies\Nextcloud\Nextcloud" under HKLM.
2020-02-16 17:36:59 +00:00
Michael Schuster
f4543e0c79 Merge pull request #1783 from nextcloud/backport/1729/stable-2.6
[stable-2.6] Install libcloudproviders files by default on debian
2020-02-16 18:19:15 +01:00
Corentin Noël
d35f466773 Install libcloudproviders files by default on debian
Signed-off-by: Corentin Noël <corentin@elementary.io>
2020-02-16 17:18:39 +00:00
XNG
e3cb3b28ff apply http2 qt resend patch from owncloud
Signed-off-by: XNG <Milokita@users.noreply.github.com>
(cherry picked from commit 768cf7e1ae)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2020-02-14 17:45:33 +01:00
XNG
36049afbc4 apply http2 qt resend patch from owncloud
Signed-off-by: XNG <Milokita@users.noreply.github.com>
(cherry picked from commit d87a88e39f)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2020-02-14 17:45:33 +01:00
XNG
59c165aa1d apply http2 qt resend patch from owncloud
Signed-off-by: XNG <Milokita@users.noreply.github.com>
(cherry picked from commit 314c00a8b7)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2020-02-14 17:45:30 +01:00
Dominique Fuchs
071b4abeeb Merge pull request #1774 from nextcloud/backport/1763/stable-2.6
[stable-2.6] Make sure that the libcloudprovider integration is using a valid D-Bus path
2020-02-06 07:24:51 +01:00
Corentin Noël
93e04fc72b Make sure that the libcloudprovider integration is using a valid D-Bus path
Set a simple unique identifier per folder to ensure that it is always unique.

Fixes https://github.com/nextcloud/desktop/issues/1704

Signed-off-by: Corentin Noël <corentin@elementary.io>
2020-02-06 06:22:35 +00:00
Michael Schuster
a9915c4b46 Merge pull request #1752 from nextcloud/backport/1745/stable-2.6
[stable-2.6] Use system proxy by default if no config file is present
2020-01-23 18:13:10 +01:00
Julius Härtl
a265ff52e7 Use system proxy by default if no config file is present
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2020-01-23 17:12:33 +00:00
Michael Schuster
85b4965d7f Linux AppImage build script: Use QtKeyChain master
Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit a35aa58943)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2020-01-18 16:33:26 +01:00
Roeland Jago Douma
963beec760 Windows 7 is out of support
Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
(cherry picked from commit a3aab00ca9)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2020-01-18 16:10:20 +01:00
Brandon
2f812063ac Correct wrong variable
Signed-off-by: Brandon <brandon.yeow@websparks.sg>
(cherry picked from commit d10bc1bb14)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2020-01-18 16:10:20 +01:00
Brandon
a485120a34 Correct wrong variable
Signed-off-by: Brandon <me@branbit.com>
Signed-off-by: Brandon <brandon.yeow@websparks.sg>
(cherry picked from commit 18a88fcecf)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2020-01-18 16:10:19 +01:00
ritsute
6337116de7 Handle broken shared file error gracefully
Signed-off-by: Brandon <me@branbit.com>
Signed-off-by: Brandon <brandon.yeow@websparks.sg>
(cherry picked from commit c92f520423)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2020-01-18 16:10:19 +01:00
JanDragon
a56eb2e95e Welcome to 2020
(cherry picked from commit 7565c547ae)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2020-01-18 16:09:25 +01:00
Michael Schuster
ac3246f9f2 Fix Explorer integration on Windows and the crash on other systems
- Ensure that the folder integration stays persistent in Explorer,
  the uninstaller removes the folder upon updating the client.
  Recreate all entries upon start. This has the benefit of removing
  old remains of non-working, outdated entries.

- Don't crash on the other systems when the user clicks the option
  button "Show sync folders in Explorer's Navigation Pane".
  Even though the option currently doesn't work on the other platforms,
  crashing is never good...

Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit 8f9101773c)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2020-01-18 16:09:11 +01:00
Roeland Jago Douma
aa9849c112 Ask for password on password protected link shares
Fixes #1485

This was missed when creating the new share dialog.
Now it pops up with a nice share password dialog to enter for your link
share.

Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
(cherry picked from commit 05083e32c9)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2020-01-18 16:08:56 +01:00
JanDragon
a89e49ef84 Updated year in legalnotice.cpp
(cherry picked from commit 4a64e8da83)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2020-01-18 16:08:37 +01:00
Michael Schuster
1d745535f7 KeychainChunk: Fix error handling in ReadJob::slotReadJobDone
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 12:13:26 +01:00
Michael Schuster
3184aeed43 Add temporary Flow2 translation (german)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 11:06:17 +01:00
Michael Schuster
3faf010b55 Update translations
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:18:57 +01:00
Michael Schuster
163c80e203 Fix date in ActivityWidget and remove unnecessary string conversion
The local date and time value was converted into a string, just to be converted
into another string, to be converted to a value once again, returning zero as
the result. This caused the widget to always display "now".

Looks like this was a simply copy and paste mistake from this line in
ActivityListModel::slotActivitiesReceived:

a._dateTime = QDateTime::fromString(json.value("date").toString(), Qt::ISODate);

Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit e07859fb3c)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:16:52 +01:00
Michael Schuster
73462e97aa Heavy refactoring: Windows workaround for >= 4k (4096 bit) client-cert SSL keys and large certs
With QtKeychain on Windows, storing larger keys or certs in one keychain entry causes the
following error due to limits in the Windows APIs:
    Error: "Credential size exceeds maximum size of 2560"

This fix implements the new wrapper class KeychainChunk with wrapper jobs ReadJob and WriteJob
to encapsulate the QKeychain handling of ReadPasswordJob and WritePasswordJob with binaryData
but split every supplied keychain entry's data into 2048 byte chunks, on Windows only.

The wrapper is used for all keychain operations in WebFlowCredentials, except for the server password.

All finished keychain jobs now get deleted properly, to avoid memory leaks.

For reference also see previous fixes:
- https://github.com/nextcloud/desktop/pull/1389
- https://github.com/nextcloud/desktop/pull/1394

This should finally fix the re-opened issue:
- https://github.com/nextcloud/desktop/issues/863

Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit 9b034a2eb0)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:16:52 +01:00
Michael Schuster
6f4144a464 Flow2AuthWidget: Minor fixes and improvements
- Improve status messages

- Add a counter to make sure that "Link copied to clipboard." is visible for
  three seconds and to not enable the buttons too early

- Add more space between buttons and status

Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit bd9652b24c)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:16:51 +01:00
Michael Schuster
9820464545 Flow2: Refactor UI into Flow2AuthWidget only and improve Flow2Auth
- Flow2AuthCredsPage:
  - Remove .ui file and embed Flow2AuthWidget into layout

- Flow2AuthWidget:
  - Make use generic for Flow2AuthCredsPage and WebFlowCredentialsDialog
  - Fix _errorLabel to render HTML tags instead of dumping them as plain text

- Flow2Auth:
  - Explicitly start auth with startAuth(account) instead of using constructor
  - Take control of copying the auth link to clipboard
  - Request a new auth link on copying, to avoid expiry invalidation
  - Use signals statusChanged() and result() to be more verbose (status, errors)
  - Change timer invocation and add safety bool's to avoid weird behaviour when
    the user triggers multiple link-copy calls (fetchNewToken)

Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit 8b5f09305c)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:16:51 +01:00
Michael Schuster
3ecd7823f9 Add new HeaderBanner class for WebFlowCredentialsDialog
New widget on top of the layout, based on Qt's own modern wizard header banner.

This should improve the user's perception of the dialog.

Encapsulate the existing layout into a container layout to allow the banner taking
the full width of the dialog.

Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit 6d033f2964)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:16:51 +01:00
Michael Schuster
9b504eaddd Make WebFlowCredentialsDialog cancellation- and deletion-safe
- Add new signal to let WebFlowCredentials know and emit asked() to also
  tell AccountState that the user won't authenticate, and triggering
  log-out state in the settings window.

- Use deleteLater() to safely delete WebFlowCredentialsDialog, so
  that Qt can free it at the right time and without crashes.
  Do the same with it's _webView and _flow2AuthWidget on closeEvent().

Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit 0bcac1882a)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:16:51 +01:00
Michael Schuster
838ca6cba0 WebFlowCredentialsDialog: Bring re-auth dialog to top (raise) on error
Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit b6b04aeff8)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:16:51 +01:00
Michael Schuster
7e5e40d5c4 Flow2: Make ProgressIndicator's background-aware (Dark-/Light-Mode switching)
Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit a69aed80e6)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:16:50 +01:00
Michael Schuster
abd8d1fda1 Flow2: Add poll status text, ProgressIndicator and countdown timer
Also enable / disable buttons during polling.

This aims to make the authentication status more transparent and should avoid the
impression that the client is perhaps doing nothing.

Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit e81f972270)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:16:50 +01:00
Michael Schuster
0a3491f332 Small fixes and code cleanup
Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit 542590db7c)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:16:50 +01:00
Michael Schuster
15d9ca2b00 Flow2: Poll for re-auth result upon WebFlowCredentialsDialog window activation
Since the default remote poll interval has been re-raised recently to 30 seconds,
the delay between clicking "Grant access" in the browser and fetch and showing success
in the dialog may seem erroneous to the users and tempt them to click "Re-open browser"
again, causing the whole login process to restart.

This commit implements an event handler to pass the dialog's window activation
event down to the Login Flow v2 widget, in order to allow it to poll earlier.

See previous commits for dependent implementation details.

Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit e04aae94bc)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:16:50 +01:00
Michael Schuster
7682749415 WebFlowCredentialsDialog: Bring re-auth dialog to top (raise) upon showing SettingsDialog
Purpose: The floating re-auth windows of the WebFlowCredentialsDialog often get hidden behind
the SettingsDialog, and the users have to minimize a lot of other windows to find them again.

See previous commit for dependent implementation details.

Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit aa18667905)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:16:50 +01:00
Michael Schuster
a18fc5b5c9 Add helper slots and signals to catch SettingsDialog's window activation events
Signal the SettingsDialog's window activation event down to ownCloudGui and Application,
so that other classes can hook in to get notified when the SettingsDialog is being shown
again.

This approach has been chosen because we otherwise would have to deal with new instance
pointers of the current SettingsWindow - but Application is already there ;-)

Purpose: The floating re-auth windows of the WebFlowCredentialsDialog often get hidden
behind the SettingsDialog, and the users have to minimize a lot of other windows to find
them again. This commit implements the preparation for the upcoming fix commit.

Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit addb27a085)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:16:50 +01:00
Michael Schuster
96783a9b80 Flow2: Use ownCloudGui::raiseDialog to bring account setup wizard to top (raise) on auth result
Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit be10d5200f)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:16:49 +01:00
Michael Schuster
258c2cee2e Flow2: Poll for auth result upon account setup wizard window activation
Since the default remote poll interval has been re-raised recently to 30 seconds,
the delay between clicking "Grant access" in the browser and fetch and showing success
in the wizard may seem erroneous to the users and tempt them to click "Re-open browser"
again, causing the whole login process to restart.

This commit implements an event handler to pass the wizard's window activation
event down to the Login Flow v2 page, in order to allow it to poll earlier.

Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit e8348612b4)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:16:49 +01:00
Michael Schuster
65ff3c0de1 Flow2: Bring account setup wizard to top (raise) on auth result
Show and raise the wizard on success / error in the Login Flow v2 auth.

Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit 3a160a4dce)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:16:49 +01:00
Michael Schuster
6879c9f1c9 Fix issue #1237: White text on almost-white background
Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit ccd20f0172)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:16:49 +01:00
Michael Schuster
fade8465e4 Fix folder opening in ActivityListModel
After fixing the crash in the previous commit, double-clicking on Activity list rows still didn't work.

This fix partly reverts commit 8546d53b05 in ActivityItemDelegate::PathRole
of ActivityListModel::data, but adds a new check for relPath's existence in line 74.

I'm assuming the previous change there has been done to shorten the code and avoid opening the user's home
folder upon clicking entries which file value is "App Password". The new path-check takes care of that too now.

Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit c03bc8540c)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:16:49 +01:00
Michael Schuster
76b5c6b6d4 Fix crash in ActivityListModel (fixes #1693)
Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit 003acb7254)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:16:49 +01:00
Michael Schuster
bdd0cc4dc3 Show date and time in activity log (fixes issue #1683)
Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit b961b683d6)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:16:48 +01:00
Michael Schuster
876b1e239e Fix build (missing refactoring)
Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit a7dade979c)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:15:53 +01:00
Michael Schuster
da2007c7f6 Mac and high-dpi displays: Add workaround in ActivityItemDelegate to show full uncropped activity's actionText and timeText
Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit 3a2caf61e5)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:15:53 +01:00
Michael Schuster
7a33cb97cd Make all ProgressIndicator's background-aware (Dark-/Light-Mode switching)
Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit b5ed16088a)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:15:53 +01:00
Michael Schuster
7a18a58fae Make OwncloudWizard and its pages background-aware (Dark-/Light-Mode switching)
Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit e4a20b9e72)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:15:53 +01:00
Michael Schuster
fcc9d02bcc Remove unnecessary string translation and copy in ActivityItemDelegate
Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit 37e5fe786f)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:15:52 +01:00
Michael Schuster
98238669fe Remove unnecessary breaks in ActivityListModel
Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit 643995528b)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:15:52 +01:00
Michael Schuster
a80de38517 Make ActivityItemDelegate background- and selection-aware (Dark-/Light-Mode switching)
Also implement cached member icons in ActivityListModel and return their enums to
ActivityItemDelegate instead of always recreating them for each call to paint().

Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit ecd17f2ea2)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:15:52 +01:00
Michael Schuster
4cb75d91f9 Refactor ActivitySettings: Rename member variable ui to _ui
Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit bf0bf2c1b6)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:15:52 +01:00
Michael Schuster
55e4dceeb7 Make AccountSettings and ActivitySettings background-aware (Dark-/Light-Mode switching)
Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit acedf362b6)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:15:52 +01:00
Michael Schuster
0006b35abf Change error link colour in AccountSettings::showConnectionLabel
Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit 3b580eeca7)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:15:51 +01:00
Michael Schuster
b153391cbf Add new Theme helper method to custom-colourize links
Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit 6adfff1f13)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:15:51 +01:00
Michael Schuster
c0659a3124 Change Dark Mode link colour
Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit 7d542d7989)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-24 09:15:49 +01:00
Michael Schuster
de8a7aa680 Fix Activity List: Add check to avoid first empty entry
Add checks to ActivityListModel::combineActivityLists in order to avoid adding
empty Activity entries to the _finalList.

The previous implementation always added an empty entry to the top of the list because
_notificationIgnoredFiles was appended without checking (_listOfIgnoredFiles.size() > 0).

Signed-off-by: Michael Schuster <michael@schuster.ms>
(cherry picked from commit cc21d175f1)
Signed-off-by: Michael Schuster <michael@schuster.ms>
2019-12-18 03:31:32 +01:00
141 changed files with 24462 additions and 27602 deletions

View File

@@ -198,7 +198,7 @@ X-GNOME-Autostart-Delay=3
# Translations
Icon[cs_CZ]=@NAZEV_IKONY_APLIKACE@
Icon[cs_CZ]=@APPLICATION_ICON_NAME@
Name[cs_CZ]=@APPLICATION_NAME@ synchronizační klient pro desktop
Comment[cs_CZ]=@APPLICATION_NAME@ synchronizační klient pro desktop
GenericName[cs_CZ]=Synchronizace složek

View File

@@ -198,4 +198,7 @@ X-GNOME-Autostart-Delay=3
# Translations
Icon[el]=@APPLICATION_ICON_NAME@
Name[el]=@APPLICATION_NAME@ πρόγραμμα συγχρονισμού
Comment[el]=@APPLICATION_NAME@ πρόγραμμα συγχρονισμού
GenericName[el]=Συγχρονισμός φακέλου

View File

@@ -0,0 +1,204 @@
[Desktop Entry]
Categories=Utility;X-SuSE-SyncUtility;
Type=Application
Exec=@APPLICATION_EXECUTABLE@
Name=@APPLICATION_NAME@ desktop sync client
Comment=@APPLICATION_NAME@ desktop synchronization client
GenericName=Folder Sync
Icon=@APPLICATION_ICON_NAME@
Keywords=@APPLICATION_NAME@;syncing;file;sharing;
X-GNOME-Autostart-Delay=3
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
Icon[es_AR]=@APPLICATION_ICON_NAME@
Name[es_AR]=@APPLICATION_NAME@ cliente de sincronización de escritorio
Comment[es_AR]=@APPLICATION_NAME@ cliente de sincronización de escritorio
GenericName[es_AR]=Sincronización de carpetas

View File

@@ -0,0 +1,201 @@
[Desktop Entry]
Categories=Utility;X-SuSE-SyncUtility;
Type=Application
Exec=@APPLICATION_EXECUTABLE@
Name=@APPLICATION_NAME@ desktop sync client
Comment=@APPLICATION_NAME@ desktop synchronization client
GenericName=Folder Sync
Icon=@APPLICATION_ICON_NAME@
Keywords=@APPLICATION_NAME@;syncing;file;sharing;
X-GNOME-Autostart-Delay=3
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
Comment[fa]=@ APPLICATION_NAME @ مشتری هماهنگ سازی دسکتاپ

View File

@@ -199,4 +199,6 @@ X-GNOME-Autostart-Delay=3
# Translations
Icon[sv]=@APPLICATION_ICON_NAME@
Name[sv]=@APPLICATION_NAME@ desktopssynkklient
Comment[sv]=@APPLICATION_NAME@ desktopssynkroniseringsklient
GenericName[sv]=Mappsynkronisering

View File

@@ -1,7 +1,7 @@
set( MIRALL_VERSION_MAJOR 2 )
set( MIRALL_VERSION_MINOR 6 )
set( MIRALL_VERSION_PATCH 2 )
set( MIRALL_VERSION_YEAR 2019 )
set( MIRALL_VERSION_PATCH 3 )
set( MIRALL_VERSION_YEAR 2020 )
set( MIRALL_SOVERSION 0 )
if ( NOT DEFINED MIRALL_VERSION_SUFFIX )

View File

@@ -18,11 +18,11 @@ if [ $SUFFIX != "master" ]; then
SUFFIX="PR-$SUFFIX"
fi
#QtKeyChain 0.9.1
#QtKeyChain master
cd /build
git clone https://github.com/frankosterfeld/qtkeychain.git
cd qtkeychain
git checkout v0.9.1
git checkout master
mkdir build
cd build
cmake -D CMAKE_INSTALL_PREFIX=/usr ../

View File

@@ -0,0 +1,4 @@
usr/bin
usr/share/applications
usr/share/icons
debian/101-sync-inotify.conf etc/sysctl.d

View File

@@ -1,4 +1,6 @@
usr/bin
usr/share/applications
usr/share/cloud-providers/
usr/share/dbus-1/services/
usr/share/icons
debian/101-sync-inotify.conf etc/sysctl.d

View File

@@ -27,7 +27,7 @@
<key>CFBundleShortVersionString</key>
<string>@MIRALL_VERSION_STRING@</string>
<key>NSHumanReadableCopyright</key>
<string>(C) 2014-2019 @APPLICATION_VENDOR@</string>
<string>(C) 2014-2020 @APPLICATION_VENDOR@</string>
<key>NSSupportsAutomaticGraphicsSwitching</key>
<true/>
<key>SUShowReleaseNotes</key>

View File

@@ -87,7 +87,7 @@ To prevent automatic updates and disallow manual overrides:
1. Edit this Registry key:
``HKEY_LOCAL_MACHINE\Software\Policies\Nextcloud\Nextcloud``
``HKEY_LOCAL_MACHINE\Software\Policies\Nextcloud GmbH\Nextcloud``
2. Add the key ``skipUpdateCheck`` (of type DWORD).

View File

@@ -27,7 +27,7 @@ download page.
System Requirements
----------------------------------
- Windows 7+
- Windows 8.1+
- macOS 10.7+ (**64-bit only**)
- CentOS 6 & 7 (64-bit only)
- Debian 8.0 & 9.0

View File

@@ -1,4 +1,4 @@
[D-BUS Service]
Name=@LIBCLOUDPROVIDERS_DBUS_BUS_NAME@
Exec=@APPLICATION_EXECUTABLE@
Exec=@APPLICATION_EXECUTABLE@ --background

View File

@@ -35,12 +35,11 @@ set(client_UI_SRCS
addcertificatedialog.ui
proxyauthdialog.ui
mnemonicdialog.ui
wizard/flow2authwidget.ui
wizard/owncloudadvancedsetuppage.ui
wizard/owncloudconnectionmethoddialog.ui
wizard/owncloudhttpcredspage.ui
wizard/owncloudoauthcredspage.ui
wizard/flow2authcredspage.ui
wizard/flow2authwidget.ui
wizard/owncloudsetupnocredspage.ui
wizard/owncloudwizardresultpage.ui
wizard/webview.ui
@@ -91,7 +90,7 @@ set(client_SRCS
syncrunfilelog.cpp
systray.cpp
thumbnailjob.cpp
quotainfo.cpp
userinfo.cpp
accountstate.cpp
addcertificatedialog.cpp
authenticationdialog.cpp
@@ -103,12 +102,14 @@ set(client_SRCS
servernotificationhandler.cpp
guiutility.cpp
elidedlabel.cpp
headerbanner.cpp
iconjob.cpp
remotewipe.cpp
creds/credentialsfactory.cpp
creds/httpcredentialsgui.cpp
creds/oauth.cpp
creds/flow2auth.cpp
creds/keychainchunk.cpp
creds/webflowcredentials.cpp
creds/webflowcredentialsdialog.cpp
wizard/postfixlineedit.cpp

View File

@@ -26,7 +26,7 @@
#include "configfile.h"
#include "account.h"
#include "accountstate.h"
#include "quotainfo.h"
#include "userinfo.h"
#include "accountmanager.h"
#include "owncloudsetupwizard.h"
#include "creds/abstractcredentials.h"
@@ -109,13 +109,13 @@ protected:
AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent)
: QWidget(parent)
, ui(new Ui::AccountSettings)
, _ui(new Ui::AccountSettings)
, _wasDisabledBefore(false)
, _accountState(accountState)
, _quotaInfo(accountState)
, _userInfo(accountState, false, true)
, _menuShown(false)
{
ui->setupUi(this);
_ui->setupUi(this);
_model = new FolderStatusModel;
_model->setAccountState(_accountState);
@@ -123,37 +123,40 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent)
FolderStatusDelegate *delegate = new FolderStatusDelegate;
delegate->setParent(this);
ui->_folderList->header()->hide();
ui->_folderList->setItemDelegate(delegate);
ui->_folderList->setModel(_model);
// Connect styleChanged events to our widgets, so they can adapt (Dark-/Light-Mode switching)
connect(this, &AccountSettings::styleChanged, delegate, &FolderStatusDelegate::slotStyleChanged);
_ui->_folderList->header()->hide();
_ui->_folderList->setItemDelegate(delegate);
_ui->_folderList->setModel(_model);
#if defined(Q_OS_MAC)
ui->_folderList->setMinimumWidth(400);
_ui->_folderList->setMinimumWidth(400);
#else
ui->_folderList->setMinimumWidth(300);
_ui->_folderList->setMinimumWidth(300);
#endif
new ToolTipUpdater(ui->_folderList);
new ToolTipUpdater(_ui->_folderList);
auto mouseCursorChanger = new MouseCursorChanger(this);
mouseCursorChanger->folderList = ui->_folderList;
mouseCursorChanger->folderList = _ui->_folderList;
mouseCursorChanger->model = _model;
ui->_folderList->setMouseTracking(true);
ui->_folderList->setAttribute(Qt::WA_Hover, true);
ui->_folderList->installEventFilter(mouseCursorChanger);
_ui->_folderList->setMouseTracking(true);
_ui->_folderList->setAttribute(Qt::WA_Hover, true);
_ui->_folderList->installEventFilter(mouseCursorChanger);
createAccountToolbox();
connect(AccountManager::instance(), &AccountManager::accountAdded,
this, &AccountSettings::slotAccountAdded);
connect(this, &AccountSettings::removeAccountFolders,
AccountManager::instance(), &AccountManager::removeAccountFolders);
connect(ui->_folderList, &QWidget::customContextMenuRequested,
connect(_ui->_folderList, &QWidget::customContextMenuRequested,
this, &AccountSettings::slotCustomContextMenuRequested);
connect(ui->_folderList, &QAbstractItemView::clicked,
connect(_ui->_folderList, &QAbstractItemView::clicked,
this, &AccountSettings::slotFolderListClicked);
connect(ui->_folderList, &QTreeView::expanded, this, &AccountSettings::refreshSelectiveSyncStatus);
connect(ui->_folderList, &QTreeView::collapsed, this, &AccountSettings::refreshSelectiveSyncStatus);
connect(ui->selectiveSyncNotification, &QLabel::linkActivated,
connect(_ui->_folderList, &QTreeView::expanded, this, &AccountSettings::refreshSelectiveSyncStatus);
connect(_ui->_folderList, &QTreeView::collapsed, this, &AccountSettings::refreshSelectiveSyncStatus);
connect(_ui->selectiveSyncNotification, &QLabel::linkActivated,
this, &AccountSettings::slotLinkActivated);
connect(_model, &FolderStatusModel::suggestExpand, ui->_folderList, &QTreeView::expand);
connect(_model, &FolderStatusModel::suggestExpand, _ui->_folderList, &QTreeView::expand);
connect(_model, &FolderStatusModel::dirtyChanged, this, &AccountSettings::refreshSelectiveSyncStatus);
refreshSelectiveSyncStatus();
connect(_model, &QAbstractItemModel::rowsInserted,
@@ -170,25 +173,26 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent)
addAction(syncNowWithRemoteDiscovery);
connect(ui->selectiveSyncApply, &QAbstractButton::clicked, _model, &FolderStatusModel::slotApplySelectiveSync);
connect(ui->selectiveSyncCancel, &QAbstractButton::clicked, _model, &FolderStatusModel::resetFolders);
connect(ui->bigFolderApply, &QAbstractButton::clicked, _model, &FolderStatusModel::slotApplySelectiveSync);
connect(ui->bigFolderSyncAll, &QAbstractButton::clicked, _model, &FolderStatusModel::slotSyncAllPendingBigFolders);
connect(ui->bigFolderSyncNone, &QAbstractButton::clicked, _model, &FolderStatusModel::slotSyncNoPendingBigFolders);
connect(_ui->selectiveSyncApply, &QAbstractButton::clicked, _model, &FolderStatusModel::slotApplySelectiveSync);
connect(_ui->selectiveSyncCancel, &QAbstractButton::clicked, _model, &FolderStatusModel::resetFolders);
connect(_ui->bigFolderApply, &QAbstractButton::clicked, _model, &FolderStatusModel::slotApplySelectiveSync);
connect(_ui->bigFolderSyncAll, &QAbstractButton::clicked, _model, &FolderStatusModel::slotSyncAllPendingBigFolders);
connect(_ui->bigFolderSyncNone, &QAbstractButton::clicked, _model, &FolderStatusModel::slotSyncNoPendingBigFolders);
connect(FolderMan::instance(), &FolderMan::folderListChanged, _model, &FolderStatusModel::resetFolders);
connect(this, &AccountSettings::folderChanged, _model, &FolderStatusModel::resetFolders);
QColor color = palette().highlight().color();
ui->quotaProgressBar->setStyleSheet(QString::fromLatin1(progressBarStyleC).arg(color.name()));
// quotaProgressBar style now set in customizeStyle()
/*QColor color = palette().highlight().color();
_ui->quotaProgressBar->setStyleSheet(QString::fromLatin1(progressBarStyleC).arg(color.name()));*/
ui->connectLabel->setText(tr("No account configured."));
_ui->connectLabel->setText(tr("No account configured."));
connect(_accountState, &AccountState::stateChanged, this, &AccountSettings::slotAccountStateChanged);
slotAccountStateChanged();
connect(&_quotaInfo, &QuotaInfo::quotaUpdated,
connect(&_userInfo, &UserInfo::quotaUpdated,
this, &AccountSettings::slotUpdateQuota);
// Connect E2E stuff
@@ -200,7 +204,7 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent)
{
slotNewMnemonicGenerated();
} else {
ui->encryptionMessage->hide();
_ui->encryptionMessage->hide();
}
customizeStyle();
@@ -225,9 +229,9 @@ void AccountSettings::createAccountToolbox()
menu->addAction(action);
connect(action, &QAction::triggered, this, &AccountSettings::slotDeleteAccount);
ui->_accountToolbox->setText(tr("Account") + QLatin1Char(' '));
ui->_accountToolbox->setMenu(menu);
ui->_accountToolbox->setPopupMode(QToolButton::InstantPopup);
_ui->_accountToolbox->setText(tr("Account") + QLatin1Char(' '));
_ui->_accountToolbox->setMenu(menu);
_ui->_accountToolbox->setPopupMode(QToolButton::InstantPopup);
slotAccountAdded(_accountState);
}
@@ -235,14 +239,14 @@ void AccountSettings::createAccountToolbox()
void AccountSettings::slotNewMnemonicGenerated()
{
ui->encryptionMessage->setText(tr("This account supports end-to-end encryption"));
_ui->encryptionMessage->setText(tr("This account supports end-to-end encryption"));
QAction *mnemonic = new QAction(tr("Enable encryption"), this);
connect(mnemonic, &QAction::triggered, this, &AccountSettings::requesetMnemonic);
connect(mnemonic, &QAction::triggered, ui->encryptionMessage, &KMessageWidget::hide);
connect(mnemonic, &QAction::triggered, _ui->encryptionMessage, &KMessageWidget::hide);
ui->encryptionMessage->addAction(mnemonic);
ui->encryptionMessage->show();
_ui->encryptionMessage->addAction(mnemonic);
_ui->encryptionMessage->show();
}
void AccountSettings::slotMenuBeforeShow() {
@@ -250,7 +254,7 @@ void AccountSettings::slotMenuBeforeShow() {
return;
}
auto menu = ui->_accountToolbox->menu();
auto menu = _ui->_accountToolbox->menu();
// We can't check this during the initial creation as there is no account yet then
if (_accountState->account()->capabilities().clientSideEncryptionAvaliable()) {
@@ -265,7 +269,7 @@ void AccountSettings::slotMenuBeforeShow() {
QString AccountSettings::selectedFolderAlias() const
{
QModelIndex selected = ui->_folderList->selectionModel()->currentIndex();
QModelIndex selected = _ui->_folderList->selectionModel()->currentIndex();
if (!selected.isValid())
return "";
return _model->data(selected, FolderStatusDelegate::FolderAliasRole).toString();
@@ -294,7 +298,7 @@ void AccountSettings::slotToggleSignInState()
void AccountSettings::doExpand()
{
ui->_folderList->expandToDepth(0);
_ui->_folderList->expandToDepth(0);
}
void AccountSettings::slotShowMnemonic(const QString &mnemonic) {
@@ -542,7 +546,7 @@ void AccountSettings::slotEditCurrentIgnoredFiles()
void AccountSettings::slotEditCurrentLocalIgnoredFiles()
{
QModelIndex selected = ui->_folderList->selectionModel()->currentIndex();
QModelIndex selected = _ui->_folderList->selectionModel()->currentIndex();
if (!selected.isValid() || _model->classify(selected) != FolderStatusModel::SubFolder)
return;
QString fileName = _model->data(selected, FolderStatusDelegate::FolderPathRole).toString();
@@ -614,7 +618,7 @@ void AccountSettings::slotSubfolderContextMenuRequested(const QModelIndex& index
void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos)
{
QTreeView *tv = ui->_folderList;
QTreeView *tv = _ui->_folderList;
QModelIndex index = tv->indexAt(pos);
if (!index.isValid()) {
return;
@@ -645,7 +649,7 @@ void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos)
ac = menu->addAction(tr("Edit Ignored Files"));
connect(ac, &QAction::triggered, this, &AccountSettings::slotEditCurrentIgnoredFiles);
if (!ui->_folderList->isExpanded(index)) {
if (!_ui->_folderList->isExpanded(index)) {
ac = menu->addAction(tr("Choose what to sync"));
ac->setEnabled(folderConnected);
connect(ac, &QAction::triggered, this, &AccountSettings::doExpand);
@@ -684,7 +688,7 @@ void AccountSettings::slotFolderListClicked(const QModelIndex &indx)
}
if (_model->classify(indx) == FolderStatusModel::RootFolder) {
// tries to find if we clicked on the '...' button.
QTreeView *tv = ui->_folderList;
QTreeView *tv = _ui->_folderList;
auto pos = tv->mapFromGlobal(QCursor::pos());
if (FolderStatusDelegate::optionsButtonRect(tv->visualRect(indx), layoutDirection()).contains(pos)) {
slotCustomContextMenuRequested(pos);
@@ -697,8 +701,8 @@ void AccountSettings::slotFolderListClicked(const QModelIndex &indx)
// Expand root items on single click
if (_accountState && _accountState->state() == AccountState::Connected) {
bool expanded = !(ui->_folderList->isExpanded(indx));
ui->_folderList->setExpanded(indx, expanded);
bool expanded = !(_ui->_folderList->isExpanded(indx));
_ui->_folderList->setExpanded(indx, expanded);
}
}
}
@@ -780,7 +784,7 @@ void AccountSettings::slotRemoveCurrentFolder()
{
FolderMan *folderMan = FolderMan::instance();
auto folder = folderMan->folder(selectedFolderAlias());
QModelIndex selected = ui->_folderList->selectionModel()->currentIndex();
QModelIndex selected = _ui->_folderList->selectionModel()->currentIndex();
if (selected.isValid() && folder) {
int row = selected.row();
@@ -822,7 +826,7 @@ void AccountSettings::slotOpenCurrentFolder()
void AccountSettings::slotOpenCurrentLocalSubFolder()
{
QModelIndex selected = ui->_folderList->selectionModel()->currentIndex();
QModelIndex selected = _ui->_folderList->selectionModel()->currentIndex();
if (!selected.isValid() || _model->classify(selected) != FolderStatusModel::SubFolder)
return;
QString fileName = _model->data(selected, FolderStatusDelegate::FolderPathRole).toString();
@@ -838,19 +842,19 @@ void AccountSettings::showConnectionLabel(const QString &message, QStringList er
if (errors.isEmpty()) {
QString msg = message;
Theme::replaceLinkColorStringBackgroundAware(msg);
ui->connectLabel->setText(msg);
ui->connectLabel->setToolTip(QString());
ui->connectLabel->setStyleSheet(QString());
_ui->connectLabel->setText(msg);
_ui->connectLabel->setToolTip(QString());
_ui->connectLabel->setStyleSheet(QString());
} else {
errors.prepend(message);
QString msg = errors.join(QLatin1String("\n"));
qCDebug(lcAccountSettings) << msg;
Theme::replaceLinkColorStringBackgroundAware(msg, QColor("#bb4d4d"));
ui->connectLabel->setText(msg);
ui->connectLabel->setToolTip(QString());
ui->connectLabel->setStyleSheet(errStyle);
Theme::replaceLinkColorString(msg, QColor("#c1c8e6"));
_ui->connectLabel->setText(msg);
_ui->connectLabel->setToolTip(QString());
_ui->connectLabel->setStyleSheet(errStyle);
}
ui->accountStatus->setVisible(!message.isEmpty());
_ui->accountStatus->setVisible(!message.isEmpty());
}
void AccountSettings::slotEnableCurrentFolder()
@@ -948,29 +952,29 @@ void AccountSettings::slotOpenOC()
void AccountSettings::slotUpdateQuota(qint64 total, qint64 used)
{
if (total > 0) {
ui->quotaProgressBar->setVisible(true);
ui->quotaProgressBar->setEnabled(true);
_ui->quotaProgressBar->setVisible(true);
_ui->quotaProgressBar->setEnabled(true);
// workaround the label only accepting ints (which may be only 32 bit wide)
const double percent = used / (double)total * 100;
const int percentInt = qMin(qRound(percent), 100);
ui->quotaProgressBar->setValue(percentInt);
_ui->quotaProgressBar->setValue(percentInt);
QString usedStr = Utility::octetsToString(used);
QString totalStr = Utility::octetsToString(total);
QString percentStr = Utility::compactFormatDouble(percent, 1);
QString toolTip = tr("%1 (%3%) of %2 in use. Some folders, including network mounted or shared folders, might have different limits.").arg(usedStr, totalStr, percentStr);
ui->quotaInfoLabel->setText(tr("%1 of %2 in use").arg(usedStr, totalStr));
ui->quotaInfoLabel->setToolTip(toolTip);
ui->quotaProgressBar->setToolTip(toolTip);
_ui->quotaInfoLabel->setText(tr("%1 of %2 in use").arg(usedStr, totalStr));
_ui->quotaInfoLabel->setToolTip(toolTip);
_ui->quotaProgressBar->setToolTip(toolTip);
} else {
ui->quotaProgressBar->setVisible(false);
ui->quotaInfoLabel->setToolTip(QString());
_ui->quotaProgressBar->setVisible(false);
_ui->quotaInfoLabel->setToolTip(QString());
/* -1 means not computed; -2 means unknown; -3 means unlimited (#3940)*/
if (total == 0 || total == -1) {
ui->quotaInfoLabel->setText(tr("Currently there is no storage usage information available."));
_ui->quotaInfoLabel->setText(tr("Currently there is no storage usage information available."));
} else {
QString usedStr = Utility::octetsToString(used);
ui->quotaInfoLabel->setText(tr("%1 in use").arg(usedStr));
_ui->quotaInfoLabel->setText(tr("%1 in use").arg(usedStr));
}
}
}
@@ -979,7 +983,7 @@ void AccountSettings::slotAccountStateChanged()
{
int state = _accountState ? _accountState->state() : AccountState::Disconnected;
if (_accountState) {
ui->sslButton->updateAccountState(_accountState);
_ui->sslButton->updateAccountState(_accountState);
AccountPtr account = _accountState->account();
QUrl safeUrl(account->url());
safeUrl.setPassword(QString()); // Remove the password from the URL to avoid showing it in the UI
@@ -1038,14 +1042,14 @@ void AccountSettings::slotAccountStateChanged()
}
/* Allow to expand the item if the account is connected. */
ui->_folderList->setItemsExpandable(state == AccountState::Connected);
_ui->_folderList->setItemsExpandable(state == AccountState::Connected);
if (state != AccountState::Connected) {
/* check if there are expanded root items, if so, close them */
int i;
for (i = 0; i < _model->rowCount(); ++i) {
if (ui->_folderList->isExpanded(_model->index(i)))
ui->_folderList->setExpanded(_model->index(i), false);
if (_ui->_folderList->isExpanded(_model->index(i)))
_ui->_folderList->setExpanded(_model->index(i), false);
}
} else if (_model->isDirty()) {
// If we connect and have pending changes, show the list.
@@ -1089,21 +1093,21 @@ void AccountSettings::slotLinkActivated(const QString &link)
// Make sure the folder itself is expanded
Folder *f = FolderMan::instance()->folder(alias);
QModelIndex folderIndx = _model->indexForPath(f, QString());
if (!ui->_folderList->isExpanded(folderIndx)) {
ui->_folderList->setExpanded(folderIndx, true);
if (!_ui->_folderList->isExpanded(folderIndx)) {
_ui->_folderList->setExpanded(folderIndx, true);
}
QModelIndex indx = _model->indexForPath(f, myFolder);
if (indx.isValid()) {
// make sure all the parents are expanded
for (auto i = indx.parent(); i.isValid(); i = i.parent()) {
if (!ui->_folderList->isExpanded(i)) {
ui->_folderList->setExpanded(i, true);
if (!_ui->_folderList->isExpanded(i)) {
_ui->_folderList->setExpanded(i, true);
}
}
ui->_folderList->setSelectionMode(QAbstractItemView::SingleSelection);
ui->_folderList->setCurrentIndex(indx);
ui->_folderList->scrollTo(indx);
_ui->_folderList->setSelectionMode(QAbstractItemView::SingleSelection);
_ui->_folderList->setCurrentIndex(indx);
_ui->_folderList->scrollTo(indx);
} else {
qCWarning(lcAccountSettings) << "Unable to find a valid index for " << myFolder;
}
@@ -1112,7 +1116,7 @@ void AccountSettings::slotLinkActivated(const QString &link)
AccountSettings::~AccountSettings()
{
delete ui;
delete _ui;
}
void AccountSettings::refreshSelectiveSyncStatus()
@@ -1150,8 +1154,8 @@ void AccountSettings::refreshSelectiveSyncStatus()
}
if (msg.isEmpty()) {
ui->selectiveSyncButtons->setVisible(true);
ui->bigFolderUi->setVisible(false);
_ui->selectiveSyncButtons->setVisible(true);
_ui->bigFolderUi->setVisible(false);
} else {
ConfigFile cfg;
QString info = !cfg.confirmExternalStorage()
@@ -1160,27 +1164,27 @@ void AccountSettings::refreshSelectiveSyncStatus()
? 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: ");
ui->selectiveSyncNotification->setText(info + msg);
ui->selectiveSyncButtons->setVisible(false);
ui->bigFolderUi->setVisible(true);
_ui->selectiveSyncNotification->setText(info + msg);
_ui->selectiveSyncButtons->setVisible(false);
_ui->bigFolderUi->setVisible(true);
shouldBeVisible = true;
}
ui->selectiveSyncApply->setEnabled(_model->isDirty() || !msg.isEmpty());
bool wasVisible = !ui->selectiveSyncStatus->isHidden();
_ui->selectiveSyncApply->setEnabled(_model->isDirty() || !msg.isEmpty());
bool wasVisible = !_ui->selectiveSyncStatus->isHidden();
if (wasVisible != shouldBeVisible) {
QSize hint = ui->selectiveSyncStatus->sizeHint();
QSize hint = _ui->selectiveSyncStatus->sizeHint();
if (shouldBeVisible) {
ui->selectiveSyncStatus->setMaximumHeight(0);
ui->selectiveSyncStatus->setVisible(true);
_ui->selectiveSyncStatus->setMaximumHeight(0);
_ui->selectiveSyncStatus->setVisible(true);
}
auto anim = new QPropertyAnimation(ui->selectiveSyncStatus, "maximumHeight", ui->selectiveSyncStatus);
auto anim = new QPropertyAnimation(_ui->selectiveSyncStatus, "maximumHeight", _ui->selectiveSyncStatus);
anim->setEndValue(shouldBeVisible ? hint.height() : 0);
anim->start(QAbstractAnimation::DeleteWhenStopped);
connect(anim, &QPropertyAnimation::finished, [this, shouldBeVisible]() {
ui->selectiveSyncStatus->setMaximumHeight(QWIDGETSIZE_MAX);
_ui->selectiveSyncStatus->setMaximumHeight(QWIDGETSIZE_MAX);
if (!shouldBeVisible) {
ui->selectiveSyncStatus->hide();
_ui->selectiveSyncStatus->hide();
}
});
}
@@ -1234,13 +1238,13 @@ void AccountSettings::slotDeleteAccount()
bool AccountSettings::event(QEvent *e)
{
if (e->type() == QEvent::Hide || e->type() == QEvent::Show) {
_quotaInfo.setActive(isVisible());
_userInfo.setActive(isVisible());
}
if (e->type() == QEvent::Show) {
// Expand the folder automatically only if there's only one, see #4283
// The 2 is 1 folder + 1 'add folder' button
if (_model->rowCount() <= 2) {
ui->_folderList->setExpanded(_model->index(0, 0), true);
_ui->_folderList->setExpanded(_model->index(0, 0), true);
}
}
return QWidget::event(e);
@@ -1249,13 +1253,19 @@ bool AccountSettings::event(QEvent *e)
void AccountSettings::slotStyleChanged()
{
customizeStyle();
// Notify the other widgets (Dark-/Light-Mode switching)
emit styleChanged();
}
void AccountSettings::customizeStyle()
{
QString msg = ui->connectLabel->text();
QString msg = _ui->connectLabel->text();
Theme::replaceLinkColorStringBackgroundAware(msg);
ui->connectLabel->setText(msg);
_ui->connectLabel->setText(msg);
QColor color = palette().highlight().color();
_ui->quotaProgressBar->setStyleSheet(QString::fromLatin1(progressBarStyleC).arg(color.name()));
}
} // namespace OCC

View File

@@ -22,7 +22,7 @@
#include <QTimer>
#include "folder.h"
#include "quotainfo.h"
#include "userinfo.h"
#include "progressdispatcher.h"
#include "owncloudgui.h"
#include "folderstatusmodel.h"
@@ -64,6 +64,7 @@ signals:
void showIssuesList(AccountState *account);
void requesetMnemonic();
void removeAccountFolders(AccountState *account);
void styleChanged();
public slots:
void slotOpenOC();
@@ -135,13 +136,13 @@ private:
/// Returns the alias of the selected folder, empty string if none
QString selectedFolderAlias() const;
Ui::AccountSettings *ui;
Ui::AccountSettings *_ui;
FolderStatusModel *_model;
QUrl _OCUrl;
bool _wasDisabledBefore;
AccountState *_accountState;
QuotaInfo _quotaInfo;
UserInfo _userInfo;
QAction *_toggleSignInOutAction;
QAction *_addAccountAction;

View File

@@ -208,7 +208,7 @@
<string/>
</property>
<property name="text">
<string>Storage space: ...</string>
<string>Storage space: </string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>

View File

@@ -229,7 +229,7 @@ void AccountState::checkConnectivity()
return;
}
ConnectionValidator *conValidator = new ConnectionValidator(account());
ConnectionValidator *conValidator = new ConnectionValidator(AccountStatePtr(this));
_connectionValidator = conValidator;
connect(conValidator, &ConnectionValidator::connectionResult,
this, &AccountState::slotConnectionValidatorResult);

View File

@@ -15,6 +15,7 @@
*/
#include "activityitemdelegate.h"
#include "activitylistmodel.h"
#include "folderstatusmodel.h"
#include "folderman.h"
#include "accountstate.h"
@@ -26,6 +27,12 @@
#include <QPainter>
#include <QApplication>
#define FIXME_USE_HIGH_DPI_RATIO
#ifdef FIXME_USE_HIGH_DPI_RATIO
// FIXME: Find a better way to calculate the text width on high-dpi displays (Retina).
#include <QDesktopWidget>
#endif
#define HASQT5_11 (QT_VERSION >= QT_VERSION_CHECK(5,11,0))
namespace OCC {
@@ -40,6 +47,12 @@ int ActivityItemDelegate::_buttonHeight = 0;
const QString ActivityItemDelegate::_remote_share("remote_share");
const QString ActivityItemDelegate::_call("call");
ActivityItemDelegate::ActivityItemDelegate()
: QStyledItemDelegate()
{
customizeStyle();
}
int ActivityItemDelegate::iconHeight()
{
if (_iconHeight == 0) {
@@ -89,10 +102,26 @@ void ActivityItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
int iconSize = 16;
int iconOffset = qRound(fm.height() / 4.0 * 7.0);
int offset = 4;
const bool isSelected = (option.state & QStyle::State_Selected);
#ifdef FIXME_USE_HIGH_DPI_RATIO
// FIXME: Find a better way to calculate the text width on high-dpi displays (Retina).
const int device_pixel_ration = QApplication::desktop()->devicePixelRatio();
int pixel_ratio = (device_pixel_ration > 1 ? device_pixel_ration : 1);
#endif
// get the data
Activity::Type activityType = qvariant_cast<Activity::Type>(index.data(ActionRole));
QIcon actionIcon = qvariant_cast<QIcon>(index.data(ActionIconRole));
QIcon actionIcon;
const ActivityListModel::ActionIcon icn = qvariant_cast<ActivityListModel::ActionIcon>(index.data(ActionIconRole));
switch(icn.iconType) {
case ActivityListModel::ActivityIconType::iconUseCached: actionIcon = icn.cachedIcon; break;
case ActivityListModel::ActivityIconType::iconActivity: actionIcon = (isSelected ? _iconActivity_sel : _iconActivity); break;
case ActivityListModel::ActivityIconType::iconBell: actionIcon = (isSelected ? _iconBell_sel : _iconBell); break;
case ActivityListModel::ActivityIconType::iconStateError: actionIcon = _iconStateError; break;
case ActivityListModel::ActivityIconType::iconStateWarning: actionIcon = _iconStateWarning; break;
case ActivityListModel::ActivityIconType::iconStateInfo: actionIcon = _iconStateInfo; break;
case ActivityListModel::ActivityIconType::iconStateSync: actionIcon = _iconStateSync; break;
}
QString objectType = qvariant_cast<QString>(index.data(ObjectTypeRole));
QString actionText = qvariant_cast<QString>(index.data(ActionTextRole));
QString messageText = qvariant_cast<QString>(index.data(MessageRole));
@@ -116,6 +145,10 @@ void ActivityItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
actionTextBox.setTop(option.rect.top() + margin + offset/2);
actionTextBox.setHeight(fm.height());
actionTextBox.setLeft(actionIconRect.right() + margin);
#ifdef FIXME_USE_HIGH_DPI_RATIO
// FIXME: Find a better way to calculate the text width on high-dpi displays (Retina).
actionTextBoxWidth *= pixel_ratio;
#endif
actionTextBox.setRight(actionTextBox.left() + actionTextBoxWidth + margin);
// message text rect
@@ -138,11 +171,10 @@ void ActivityItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
// time box rect
QRect timeBox = messageTextBox;
QString timeStr = tr("%1").arg(timeText);
#if (HASQT5_11)
int timeTextWidth = fm.horizontalAdvance(timeStr);
int timeTextWidth = fm.horizontalAdvance(timeText);
#else
int timeTextWidth = fm.width(timeStr);
int timeTextWidth = fm.width(timeText);
#endif
int timeTop = option.rect.top() + fm.height() + fm.height() + margin + offset/2;
if(messageText.isEmpty() || actionText.isEmpty())
@@ -150,6 +182,10 @@ void ActivityItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
timeBox.setTop(timeTop);
timeBox.setHeight(fm.height());
timeBox.setBottom(timeBox.top() + fm.height());
#ifdef FIXME_USE_HIGH_DPI_RATIO
// FIXME: Find a better way to calculate the text width on high-dpi displays (Retina).
timeTextWidth *= pixel_ratio;
#endif
timeBox.setRight(timeBox.left() + timeTextWidth + margin);
// buttons - default values
@@ -184,9 +220,9 @@ void ActivityItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
if(activityType == Activity::Type::NotificationType){
// Secondary will be 'Dismiss' or '...' multiple options button
secondaryButton.icon = QIcon(QLatin1String(":/client/resources/close.svg"));
secondaryButton.icon = (isSelected ? _iconClose_sel : _iconClose);
if(customList.size() > 1)
secondaryButton.icon = QIcon(QLatin1String(":/client/resources/more.svg"));
secondaryButton.icon = (isSelected ? _iconMore_sel : _iconMore);
secondaryButton.iconSize = QSize(iconSize, iconSize);
// Primary button will be 'More Information' or 'Accept'
@@ -209,7 +245,7 @@ void ActivityItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
} else if(activityType == Activity::SyncResultType){
// Secondary will be 'open file manager' with the folder icon
secondaryButton.icon = QIcon(QLatin1String(":/client/resources/folder.svg"));
secondaryButton.icon = _iconFolder;
secondaryButton.iconSize = QSize(iconSize, iconSize);
// Primary button will be 'open browser'
@@ -230,7 +266,7 @@ void ActivityItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
} else if(activityType == Activity::SyncFileItemType){
// Secondary will be 'open file manager' with the folder icon
secondaryButton.icon = QIcon(QLatin1String(":/client/resources/folder.svg"));
secondaryButton.icon = _iconFolder;
secondaryButton.iconSize = QSize(iconSize, iconSize);
// No primary button on this case
@@ -255,7 +291,7 @@ void ActivityItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
p.setCurrentColorGroup(QPalette::Disabled);
// change pen color if the line is selected
if (option.state & QStyle::State_Selected)
if (isSelected)
painter->setPen(p.color(QPalette::HighlightedText));
else
painter->setPen(p.color(QPalette::Text));
@@ -269,8 +305,15 @@ void ActivityItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
painter->drawText(actionTextBox, elidedAction);
// draw the buttons
if(activityType == Activity::Type::NotificationType || activityType == Activity::Type::SyncResultType)
if(activityType == Activity::Type::NotificationType || activityType == Activity::Type::SyncResultType) {
primaryButton.palette = p;
if (isSelected)
primaryButton.palette.setColor(QPalette::ButtonText, p.color(QPalette::HighlightedText));
else
primaryButton.palette.setColor(QPalette::ButtonText, p.color(QPalette::Text));
QApplication::style()->drawControl(QStyle::CE_PushButton, &primaryButton, painter);
}
// Since they are errors on local syncing, there is nothing to do in the server
if(activityType != Activity::Type::ActivityType)
@@ -284,13 +327,13 @@ void ActivityItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
}
// change pen color for the time
if (option.state & QStyle::State_Selected)
if (isSelected)
painter->setPen(p.color(QPalette::Disabled, QPalette::HighlightedText));
else
painter->setPen(p.color(QPalette::Disabled, QPalette::Text));
// draw the time
const QString elidedTime = fm.elidedText(timeStr, Qt::ElideRight, spaceLeftForText);
const QString elidedTime = fm.elidedText(timeText, Qt::ElideRight, spaceLeftForText);
painter->drawText(timeBox, elidedTime);
painter->restore();
@@ -333,4 +376,32 @@ bool ActivityItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
return QStyledItemDelegate::editorEvent(event, model, option, index);
}
void ActivityItemDelegate::slotStyleChanged()
{
customizeStyle();
}
void ActivityItemDelegate::customizeStyle()
{
QPalette pal;
pal.setColor(QPalette::Base, QColor(0,0,0)); // use dark background colour to invert icons
_iconClose = Theme::createColorAwareIcon(QLatin1String(":/client/resources/close.svg"));
_iconClose_sel = Theme::createColorAwareIcon(QLatin1String(":/client/resources/close.svg"), pal);
_iconMore = Theme::createColorAwareIcon(QLatin1String(":/client/resources/more.svg"));
_iconMore_sel = Theme::createColorAwareIcon(QLatin1String(":/client/resources/more.svg"), pal);
_iconFolder = QIcon(QLatin1String(":/client/resources/folder.svg"));
_iconActivity = Theme::createColorAwareIcon(QLatin1String(":/client/resources/activity.png"));
_iconActivity_sel = Theme::createColorAwareIcon(QLatin1String(":/client/resources/activity.png"), pal);
_iconBell = Theme::createColorAwareIcon(QLatin1String(":/client/resources/bell.svg"));
_iconBell_sel = Theme::createColorAwareIcon(QLatin1String(":/client/resources/bell.svg"), pal);
_iconStateError = QIcon(QLatin1String(":/client/resources/state-error.svg"));
_iconStateWarning = QIcon(QLatin1String(":/client/resources/state-warning.svg"));
_iconStateInfo = QIcon(QLatin1String(":/client/resources/state-info.svg"));
_iconStateSync = QIcon(QLatin1String(":/client/resources/state-sync.svg"));
}
} // namespace OCC

View File

@@ -43,6 +43,8 @@ public:
AccountConnectedRole,
SyncFileStatusRole };
ActivityItemDelegate();
void paint(QPainter *, const QStyleOptionViewItem &, const QModelIndex &) const override;
QSize sizeHint(const QStyleOptionViewItem &, const QModelIndex &) const override;
bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option,
@@ -51,11 +53,16 @@ public:
static int rowHeight();
static int iconHeight();
public slots:
void slotStyleChanged();
signals:
void primaryButtonClickedOnItemView(const QModelIndex &index);
void secondaryButtonClickedOnItemView(const QModelIndex &index);
private:
void customizeStyle();
static int _margin;
static int _iconHeight;
static int _primaryButtonWidth;
@@ -65,6 +72,23 @@ private:
static int _buttonHeight;
static const QString _remote_share;
static const QString _call;
QIcon _iconClose;
QIcon _iconClose_sel;
QIcon _iconMore;
QIcon _iconMore_sel;
QIcon _iconFolder;
QIcon _iconActivity;
QIcon _iconActivity_sel;
QIcon _iconBell;
QIcon _iconBell_sel;
QIcon _iconStateError;
QIcon _iconStateWarning;
QIcon _iconStateInfo;
QIcon _iconStateSync;
};
} // namespace OCC

View File

@@ -64,10 +64,19 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
case ActivityItemDelegate::PathRole:
if(!a._file.isEmpty()){
auto folder = FolderMan::instance()->folder(a._folder);
list = FolderMan::instance()->findFileInLocalFolders(folder->remotePath(), ast->account());
QString relPath(a._file);
if(folder) relPath.prepend(folder->remotePath());
list = FolderMan::instance()->findFileInLocalFolders(relPath, ast->account());
if (list.count() > 0) {
return QVariant(list.at(0));
}
// File does not exist anymore? Let's try to open its path
if(QFileInfo(relPath).exists()) {
list = FolderMan::instance()->findFileInLocalFolders(QFileInfo(relPath).path(), ast->account());
if (list.count() > 0) {
return QVariant(list.at(0));
}
}
}
return QVariant();
case ActivityItemDelegate::ActionsLinksRole:{
@@ -79,59 +88,60 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
}
return customList;
}
case ActivityItemDelegate::ActionIconRole:
case ActivityItemDelegate::ActionIconRole:{
ActionIcon actionIcon;
if(a._type == Activity::NotificationType){
QIcon cachedIcon = ServerNotificationHandler::iconCache.value(a._id);
if(!cachedIcon.isNull())
return cachedIcon;
else return QIcon(QLatin1String(":/client/resources/bell.svg"));
if(!cachedIcon.isNull()) {
actionIcon.iconType = ActivityIconType::iconUseCached;
actionIcon.cachedIcon = cachedIcon;
} else {
actionIcon.iconType = ActivityIconType::iconBell;
}
} else if(a._type == Activity::SyncResultType){
return QIcon(QLatin1String(":/client/resources/state-error.svg"));
actionIcon.iconType = ActivityIconType::iconStateError;
} else if(a._type == Activity::SyncFileItemType){
if(a._status == SyncFileItem::NormalError
|| a._status == SyncFileItem::FatalError
|| a._status == SyncFileItem::DetailError
|| a._status == SyncFileItem::BlacklistedError) {
return QIcon(QLatin1String(":/client/resources/state-error.svg"));
actionIcon.iconType = ActivityIconType::iconStateError;
} else if(a._status == SyncFileItem::SoftError
|| a._status == SyncFileItem::Conflict
|| a._status == SyncFileItem::Restoration
|| a._status == SyncFileItem::FileLocked){
return QIcon(QLatin1String(":/client/resources/state-warning.svg"));
actionIcon.iconType = ActivityIconType::iconStateWarning;
} else if(a._status == SyncFileItem::FileIgnored){
return QIcon(QLatin1String(":/client/resources/state-info.svg"));
actionIcon.iconType = ActivityIconType::iconStateInfo;
} else {
actionIcon.iconType = ActivityIconType::iconStateSync;
}
return QIcon(QLatin1String(":/client/resources/state-sync.svg"));
} else {
actionIcon.iconType = ActivityIconType::iconActivity;
}
return QIcon(QLatin1String(":/client/resources/activity.png"));
break;
QVariant icn;
icn.setValue(actionIcon);
return icn;
}
case ActivityItemDelegate::ObjectTypeRole:
return a._objectType;
break;
case ActivityItemDelegate::ActionRole:{
QVariant type;
type.setValue(a._type);
return type;
break;
}
case ActivityItemDelegate::ActionTextRole:
return a._subject;
break;
case ActivityItemDelegate::MessageRole:
return a._message;
break;
case ActivityItemDelegate::LinkRole:
return a._link;
break;
case ActivityItemDelegate::AccountRole:
return a._accName;
break;
case ActivityItemDelegate::PointInTimeRole:
return Utility::timeAgoInWords(a._dateTime);
break;
return QString("%1 (%2)").arg(a._dateTime.toLocalTime().toString(Qt::DefaultLocaleShortDate), Utility::timeAgoInWords(a._dateTime.toLocalTime()));
case ActivityItemDelegate::AccountConnectedRole:
return (ast && ast->isConnected());
break;
default:
return QVariant();
}
@@ -256,7 +266,7 @@ void ActivityListModel::clearNotifications() {
}
void ActivityListModel::removeActivityFromActivityList(int row) {
Activity activity = _finalList.at(row);
Activity activity = _finalList.at(row);
removeActivityFromActivityList(activity);
combineActivityLists();
}
@@ -294,18 +304,27 @@ void ActivityListModel::combineActivityLists()
{
ActivityList resultList;
std::sort(_notificationErrorsLists.begin(), _notificationErrorsLists.end());
resultList.append(_notificationErrorsLists);
resultList.append(_notificationIgnoredFiles);
if(_notificationErrorsLists.count() > 0) {
std::sort(_notificationErrorsLists.begin(), _notificationErrorsLists.end());
resultList.append(_notificationErrorsLists);
}
if(_listOfIgnoredFiles.size() > 0)
resultList.append(_notificationIgnoredFiles);
std::sort(_notificationLists.begin(), _notificationLists.end());
resultList.append(_notificationLists);
if(_notificationLists.count() > 0) {
std::sort(_notificationLists.begin(), _notificationLists.end());
resultList.append(_notificationLists);
}
std::sort(_syncFileItemLists.begin(), _syncFileItemLists.end());
resultList.append(_syncFileItemLists);
if(_syncFileItemLists.count() > 0) {
std::sort(_syncFileItemLists.begin(), _syncFileItemLists.end());
resultList.append(_syncFileItemLists);
}
std::sort(_activityLists.begin(), _activityLists.end());
resultList.append(_activityLists);
if(_activityLists.count() > 0) {
std::sort(_activityLists.begin(), _activityLists.end());
resultList.append(_activityLists);
}
beginResetModel();
_finalList.clear();

View File

@@ -38,6 +38,20 @@ class ActivityListModel : public QAbstractListModel
{
Q_OBJECT
public:
enum ActivityIconType {
iconUseCached = 0,
iconActivity,
iconBell,
iconStateError,
iconStateWarning,
iconStateInfo,
iconStateSync
};
struct ActionIcon {
ActivityIconType iconType;
QIcon cachedIcon;
};
explicit ActivityListModel(AccountState *accountState, QWidget *parent = nullptr);
QVariant data(const QModelIndex &index, int role) const override;
@@ -84,4 +98,7 @@ private:
int _currentItem = 0;
};
}
Q_DECLARE_METATYPE(OCC::ActivityListModel::ActionIcon)
#endif // ACTIVITYLISTMODEL_H

View File

@@ -89,6 +89,9 @@ ActivityWidget::ActivityWidget(AccountState *accountState, QWidget *parent)
this, &ActivityWidget::addError);
_removeTimer.setInterval(1000);
// Connect styleChanged events to our widgets, so they can adapt (Dark-/Light-Mode switching)
connect(this, &ActivityWidget::styleChanged, delegate, &ActivityItemDelegate::slotStyleChanged);
}
ActivityWidget::~ActivityWidget()
@@ -176,7 +179,7 @@ void ActivityWidget::slotItemCompleted(const QString &folder, const SyncFileItem
Activity activity;
activity._type = Activity::SyncFileItemType; //client activity
activity._status = item->_status;
activity._dateTime = QDateTime::fromString(QDateTime::currentDateTime().toString(), Qt::ISODate);
activity._dateTime = QDateTime::currentDateTime();
activity._message = item->_originalFile;
activity._link = folderInstance->accountState()->account()->url();
activity._accName = folderInstance->accountState()->account()->displayName();
@@ -548,6 +551,12 @@ void ActivityWidget::slotNotifyServerFinished(const QString &reply, int replyCod
qCInfo(lcActivity) << "Server Notification reply code" << replyCode << reply;
}
void ActivityWidget::slotStyleChanged()
{
// Notify the other widgets (Dark-/Light-Mode switching)
emit styleChanged();
}
/* ==================================================================== */
ActivitySettings::ActivitySettings(AccountState *accountState, QWidget *parent)
@@ -570,6 +579,9 @@ ActivitySettings::ActivitySettings(AccountState *accountState, QWidget *parent)
// connect a model signal to stop the animation
connect(_activityWidget, &ActivityWidget::rowsInserted, _progressIndicator, &QProgressIndicator::stopAnimation);
connect(_activityWidget, &ActivityWidget::rowsInserted, this, &ActivitySettings::slotDisplayActivities);
// Connect styleChanged events to our widgets, so they can adapt (Dark-/Light-Mode switching)
connect(this, &ActivitySettings::styleChanged, _activityWidget, &ActivityWidget::slotStyleChanged);
}
void ActivitySettings::slotDisplayActivities(){
@@ -628,4 +640,14 @@ bool ActivitySettings::event(QEvent *e)
ActivitySettings::~ActivitySettings()
{
}
void ActivitySettings::slotStyleChanged()
{
if(_progressIndicator)
_progressIndicator->setColor(QGuiApplication::palette().color(QPalette::Text));
// Notify the other widgets (Dark-/Light-Mode switching)
emit styleChanged();
}
}

View File

@@ -78,12 +78,14 @@ public slots:
void addError(const QString &folderAlias, const QString &message, ErrorCategory category);
void slotProgressInfo(const QString &folder, const ProgressInfo &progress);
void slotItemCompleted(const QString &folder, const SyncFileItemPtr &item);
void slotStyleChanged();
signals:
void guiLog(const QString &, const QString &);
void rowsInserted();
void hideActivityTab(bool);
void sendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row);
void styleChanged();
private slots:
void slotBuildNotificationDisplay(const ActivityList &list);
@@ -96,6 +98,7 @@ private slots:
void slotSecondaryButtonClickedOnListView(const QModelIndex &index);
private:
void customizeStyle();
void showLabels();
QString timeString(QDateTime dt, QLocale::FormatType format) const;
Ui::ActivityWidget *_ui;
@@ -137,6 +140,7 @@ public slots:
void slotRefresh();
void slotRemoveAccount();
void setNotificationRefreshInterval(std::chrono::milliseconds interval);
void slotStyleChanged();
private slots:
void slotRegularNotificationCheck();
@@ -144,6 +148,7 @@ private slots:
signals:
void guiLog(const QString &, const QString &);
void styleChanged();
private:
bool event(QEvent *e) override;

View File

@@ -48,7 +48,7 @@
<item>
<widget class="QPushButton" name="pushButtonBrowseCertificate">
<property name="text">
<string>Browse...</string>
<string>Browse</string>
</property>
</widget>
</item>

View File

@@ -254,6 +254,9 @@ Application::Application(int &argc, char **argv)
// Cleanup at Quit.
connect(this, &QCoreApplication::aboutToQuit, this, &Application::slotCleanup);
// Allow other classes to hook into isShowingSettingsDialog() signals (re-auth widgets, for example)
connect(_gui.data(), &ownCloudGui::isShowingSettingsDialog, this, &Application::slotGuiIsShowingSettings);
}
Application::~Application()
@@ -648,5 +651,9 @@ void Application::showSettingsDialog()
_gui->slotShowSettings();
}
void Application::slotGuiIsShowingSettings()
{
emit isShowingSettingsDialog();
}
} // namespace OCC

View File

@@ -82,6 +82,7 @@ protected:
signals:
void folderRemoved();
void folderStateChanged(Folder *);
void isShowingSettingsDialog();
protected slots:
void slotParseMessage(const QString &, QObject *);
@@ -91,6 +92,7 @@ protected slots:
void slotAccountStateAdded(AccountState *accountState);
void slotAccountStateRemoved(AccountState *accountState);
void slotSystemOnlineConfigurationChanged(QNetworkConfiguration);
void slotGuiIsShowingSettings();
private:
void setHelp();

View File

@@ -53,7 +53,7 @@ bool ClientProxy::isUsingSystemDefault()
return cfg.proxyType() == QNetworkProxy::DefaultProxy;
}
return false;
return true;
}
QString printQNetworkProxy(const QNetworkProxy &proxy)

View File

@@ -52,8 +52,8 @@ void CloudProviderManager::registerSignals()
CloudProviderManager::CloudProviderManager(QObject *parent) : QObject(parent)
{
_map = new QMap<QString, CloudProviderWrapper*>();
QString busName = QString(LIBCLOUDPROVIDERS_DBUS_BUS_NAME);
g_bus_own_name (G_BUS_TYPE_SESSION, busName.toAscii().data(), G_BUS_NAME_OWNER_FLAGS_NONE, nullptr, on_name_acquired, nullptr, this, nullptr);
_folder_index = 0;
g_bus_own_name (G_BUS_TYPE_SESSION, LIBCLOUDPROVIDERS_DBUS_BUS_NAME, G_BUS_NAME_OWNER_FLAGS_NONE, nullptr, on_name_acquired, nullptr, this, nullptr);
}
void CloudProviderManager::slotFolderListChanged(const Folder::Map &folderMap)
@@ -72,7 +72,7 @@ void CloudProviderManager::slotFolderListChanged(const Folder::Map &folderMap)
while (j.hasNext()) {
j.next();
if (!_map->contains(j.key())) {
auto *cpo = new CloudProviderWrapper(this, j.value(), _providerExporter);
auto *cpo = new CloudProviderWrapper(this, j.value(), _folder_index++, _providerExporter);
_map->insert(j.key(), cpo);
}
}

View File

@@ -36,6 +36,7 @@ public slots:
private:
QMap<QString, CloudProviderWrapper*> *_map;
unsigned int _folder_index;
};
#endif // CLOUDPROVIDERMANAGER_H

View File

@@ -33,13 +33,13 @@ using namespace OCC;
GSimpleActionGroup *actionGroup = nullptr;
CloudProviderWrapper::CloudProviderWrapper(QObject *parent, Folder *folder, CloudProvidersProviderExporter* cloudprovider) : QObject(parent)
CloudProviderWrapper::CloudProviderWrapper(QObject *parent, Folder *folder, int folderId, CloudProvidersProviderExporter* cloudprovider) : QObject(parent)
, _folder(folder)
{
GMenuModel *model;
GActionGroup *action_group;
_recentlyChanged = new QList<QPair<QString, QString>>();
QString accountName = QString("Account%1Folder%2").arg(folder->alias(), folder->accountState()->account()->id());
QString accountName = QString("Folder/%1").arg(folderId);
_cloudProvider = CLOUD_PROVIDERS_PROVIDER_EXPORTER(cloudprovider);
_cloudProviderAccount = cloud_providers_account_exporter_new(_cloudProvider, accountName.toUtf8().data());

View File

@@ -38,7 +38,7 @@ class CloudProviderWrapper : public QObject
{
Q_OBJECT
public:
explicit CloudProviderWrapper(QObject *parent = nullptr, Folder *folder = nullptr, CloudProvidersProviderExporter* cloudprovider = nullptr);
explicit CloudProviderWrapper(QObject *parent = nullptr, Folder *folder = nullptr, int folderId = 0, CloudProvidersProviderExporter* cloudprovider = nullptr);
~CloudProviderWrapper();
CloudProvidersAccountExporter* accountExporter();
Folder* folder();

View File

@@ -22,6 +22,8 @@
#include "connectionvalidator.h"
#include "account.h"
#include "accountstate.h"
#include "userinfo.h"
#include "networkjobs.h"
#include "clientproxy.h"
#include <creds/abstractcredentials.h>
@@ -34,9 +36,10 @@ Q_LOGGING_CATEGORY(lcConnectionValidator, "nextcloud.sync.connectionvalidator",
// This makes sure we get tried often enough without "ConnectionValidator already running"
static qint64 timeoutToUseMsec = qMax(1000, ConnectionValidator::DefaultCallingIntervalMsec - 5 * 1000);
ConnectionValidator::ConnectionValidator(AccountPtr account, QObject *parent)
ConnectionValidator::ConnectionValidator(AccountStatePtr accountState, QObject *parent)
: QObject(parent)
, _account(account)
, _accountState(accountState)
, _account(accountState->account())
, _isCheckingServerAndAuth(false)
{
}
@@ -44,7 +47,7 @@ ConnectionValidator::ConnectionValidator(AccountPtr account, QObject *parent)
void ConnectionValidator::checkServerAndAuth()
{
if (!_account) {
_errors << tr("No ownCloud account configured");
_errors << tr("No Nextcloud account configured");
reportResult(NotConfigured);
return;
}
@@ -265,10 +268,9 @@ void ConnectionValidator::ocsConfigReceived(const QJsonDocument &json, AccountPt
void ConnectionValidator::fetchUser()
{
JsonApiJob *job = new JsonApiJob(_account, QLatin1String("ocs/v1.php/cloud/user"), this);
job->setTimeout(timeoutToUseMsec);
QObject::connect(job, &JsonApiJob::jsonReceived, this, &ConnectionValidator::slotUserFetched);
job->start();
UserInfo *userInfo = new UserInfo(_accountState.data(), true, true, this);
QObject::connect(userInfo, &UserInfo::fetchedLastInfo, this, &ConnectionValidator::slotUserFetched);
userInfo->setActive(true);
}
bool ConnectionValidator::setAndCheckServerVersion(const QString &version)
@@ -300,34 +302,22 @@ bool ConnectionValidator::setAndCheckServerVersion(const QString &version)
return true;
}
void ConnectionValidator::slotUserFetched(const QJsonDocument &json)
void ConnectionValidator::slotUserFetched(UserInfo *userInfo)
{
QString user = json.object().value("ocs").toObject().value("data").toObject().value("id").toString();
if (!user.isEmpty()) {
_account->setDavUser(user);
}
QString displayName = json.object().value("ocs").toObject().value("data").toObject().value("display-name").toString();
if (!displayName.isEmpty()) {
_account->setDavDisplayName(displayName);
if(userInfo) {
userInfo->setActive(false);
userInfo->deleteLater();
}
#ifndef TOKEN_AUTH_ONLY
AvatarJob *job = new AvatarJob(_account, _account->davUser(), 128, this);
job->setTimeout(20 * 1000);
QObject::connect(job, &AvatarJob::avatarPixmap, this, &ConnectionValidator::slotAvatarImage);
job->start();
connect(_account->e2e(), &ClientSideEncryption::initializationFinished, this, &ConnectionValidator::reportConnected);
_account->e2e()->initialize();
#else
reportResult(Connected);
#endif
}
#ifndef TOKEN_AUTH_ONLY
void ConnectionValidator::slotAvatarImage(const QImage &img)
{
_account->setAvatar(img);
connect(_account->e2e(), &ClientSideEncryption::initializationFinished, this, &ConnectionValidator::reportConnected);
_account->e2e()->initialize();
}
void ConnectionValidator::reportConnected() {
reportResult(Connected);
}

View File

@@ -67,23 +67,20 @@ namespace OCC {
+---------------------------------+
|
fetchUser
PropfindJob
|
+-> slotUserFetched
AvatarJob
|
+-> slotAvatarImage -->
Utilizes the UserInfo class to fetch the user and avatar image
+-----------------------------------+
|
+-> Client Side Encryption Checks --+ --reportResult()
\endcode
*/
class UserInfo;
class ConnectionValidator : public QObject
{
Q_OBJECT
public:
explicit ConnectionValidator(AccountPtr account, QObject *parent = nullptr);
explicit ConnectionValidator(AccountStatePtr accountState, QObject *parent = nullptr);
enum Status {
Undefined,
@@ -125,13 +122,12 @@ protected slots:
void slotAuthSuccess();
void slotCapabilitiesRecieved(const QJsonDocument &);
void slotUserFetched(const QJsonDocument &);
#ifndef TOKEN_AUTH_ONLY
void slotAvatarImage(const QImage &img);
#endif
void slotUserFetched(UserInfo *userInfo);
private:
#ifndef TOKEN_AUTH_ONLY
void reportConnected();
#endif
void reportResult(Status status);
void checkServerCapabilities();
void fetchUser();
@@ -144,6 +140,7 @@ private:
bool setAndCheckServerVersion(const QString &version);
QStringList _errors;
AccountStatePtr _accountState;
AccountPtr _account;
bool _isCheckingServerAndAuth;
};

View File

@@ -14,10 +14,12 @@
*/
#include <QDesktopServices>
#include <QApplication>
#include <QClipboard>
#include <QTimer>
#include <QBuffer>
#include "account.h"
#include "creds/flow2auth.h"
#include "flow2auth.h"
#include <QJsonObject>
#include <QJsonDocument>
#include "theme.h"
@@ -28,6 +30,17 @@ namespace OCC {
Q_LOGGING_CATEGORY(lcFlow2auth, "nextcloud.sync.credentials.flow2auth", QtInfoMsg)
Flow2Auth::Flow2Auth(Account *account, QObject *parent)
: QObject(parent)
, _account(account)
, _isBusy(false)
, _hasToken(false)
{
_pollTimer.setInterval(1000);
QObject::connect(&_pollTimer, &QTimer::timeout, this, &Flow2Auth::slotPollTimerTimeout);
}
Flow2Auth::~Flow2Auth()
{
}
@@ -47,7 +60,23 @@ QUrl Flow2Auth::authorisationLink() const
void Flow2Auth::openBrowser()
{
_pollTimer.stop();
fetchNewToken(TokenAction::actionOpenBrowser);
}
void Flow2Auth::copyLinkToClipboard()
{
fetchNewToken(TokenAction::actionCopyLinkToClipboard);
}
void Flow2Auth::fetchNewToken(const TokenAction action)
{
if(_isBusy)
return;
_isBusy = true;
_hasToken = false;
emit statusChanged(PollStatus::statusFetchToken, 0);
// Step 1: Initiate a login, do an anonymous POST request
QUrl url = Utility::concatUrlPath(_account->url().toString(), QLatin1String("/index.php/login/v2"));
@@ -59,14 +88,18 @@ void Flow2Auth::openBrowser()
auto job = _account->sendRequest("POST", url, req);
job->setTimeout(qMin(30 * 1000ll, job->timeoutMsec()));
QObject::connect(job, &SimpleNetworkJob::finishedSignal, this, [this](QNetworkReply *reply) {
QObject::connect(job, &SimpleNetworkJob::finishedSignal, this, [this, action](QNetworkReply *reply) {
auto jsonData = reply->readAll();
QJsonParseError jsonParseError;
QJsonObject json = QJsonDocument::fromJson(jsonData, &jsonParseError).object();
QString pollToken, pollEndpoint, loginUrl;
QString pollToken = json.value("poll").toObject().value("token").toString();
QString pollEndpoint = json.value("poll").toObject().value("endpoint").toString();
QUrl loginUrl = json["login"].toString();
if (reply->error() == QNetworkReply::NoError && jsonParseError.error == QJsonParseError::NoError
&& !json.isEmpty()) {
pollToken = json.value("poll").toObject().value("token").toString();
pollEndpoint = json.value("poll").toObject().value("endpoint").toString();
loginUrl = json["login"].toString();
}
if (reply->error() != QNetworkReply::NoError || jsonParseError.error != QJsonParseError::NoError
|| json.isEmpty() || pollToken.isEmpty() || pollEndpoint.isEmpty() || loginUrl.isEmpty()) {
@@ -85,7 +118,9 @@ void Flow2Auth::openBrowser()
errorReason = tr("The reply from the server did not contain all expected fields");
}
qCWarning(lcFlow2auth) << "Error when getting the loginUrl" << json << errorReason;
emit result(Error);
emit result(Error, errorReason);
_pollTimer.stop();
_isBusy = false;
return;
}
@@ -99,23 +134,50 @@ void Flow2Auth::openBrowser()
ConfigFile cfg;
std::chrono::milliseconds polltime = cfg.remotePollInterval();
qCInfo(lcFlow2auth) << "setting remote poll timer interval to" << polltime.count() << "msec";
_pollTimer.setInterval(polltime.count());
QObject::connect(&_pollTimer, &QTimer::timeout, this, &Flow2Auth::slotPollTimerTimeout);
_pollTimer.start();
_secondsInterval = (polltime.count() / 1000);
_secondsLeft = _secondsInterval;
emit statusChanged(PollStatus::statusPollCountdown, _secondsLeft);
// Try to open Browser
if (!QDesktopServices::openUrl(authorisationLink())) {
// We cannot open the browser, then we claim we don't support Flow2Auth.
// Our UI callee should ask the user to copy and open the link.
emit result(NotSupported, QString());
if(!_pollTimer.isActive()) {
_pollTimer.start();
}
switch(action)
{
case actionOpenBrowser:
// Try to open Browser
if (!QDesktopServices::openUrl(authorisationLink())) {
// We cannot open the browser, then we claim we don't support Flow2Auth.
// Our UI callee will ask the user to copy and open the link.
emit result(NotSupported);
}
break;
case actionCopyLinkToClipboard:
QApplication::clipboard()->setText(authorisationLink().toString(QUrl::FullyEncoded));
emit statusChanged(PollStatus::statusCopyLinkToClipboard, 0);
break;
}
_isBusy = false;
_hasToken = true;
});
}
void Flow2Auth::slotPollTimerTimeout()
{
_pollTimer.stop();
if(_isBusy || !_hasToken)
return;
_isBusy = true;
_secondsLeft--;
if(_secondsLeft > 0) {
emit statusChanged(PollStatus::statusPollCountdown, _secondsLeft);
_isBusy = false;
return;
}
emit statusChanged(PollStatus::statusPollNow, 0);
// Step 2: Poll
QNetworkRequest req;
@@ -132,10 +194,15 @@ void Flow2Auth::slotPollTimerTimeout()
auto jsonData = reply->readAll();
QJsonParseError jsonParseError;
QJsonObject json = QJsonDocument::fromJson(jsonData, &jsonParseError).object();
QUrl serverUrl;
QString loginName, appPassword;
QUrl serverUrl = json["server"].toString();
QString loginName = json["loginName"].toString();
QString appPassword = json["appPassword"].toString();
if (reply->error() == QNetworkReply::NoError && jsonParseError.error == QJsonParseError::NoError
&& !json.isEmpty()) {
serverUrl = json["server"].toString();
loginName = json["loginName"].toString();
appPassword = json["appPassword"].toString();
}
if (reply->error() != QNetworkReply::NoError || jsonParseError.error != QJsonParseError::NoError
|| json.isEmpty() || serverUrl.isEmpty() || loginName.isEmpty() || appPassword.isEmpty()) {
@@ -155,26 +222,50 @@ void Flow2Auth::slotPollTimerTimeout()
}
qCDebug(lcFlow2auth) << "Error when polling for the appPassword" << json << errorReason;
// We get a 404 until authentication is done, so don't show this error in the GUI.
if(reply->error() != QNetworkReply::ContentNotFoundError)
emit result(Error, errorReason);
// Forget sensitive data
appPassword.clear();
loginName.clear();
// Failed: poll again
_pollTimer.start();
_secondsLeft = _secondsInterval;
_isBusy = false;
return;
}
_pollTimer.stop();
// Success
qCInfo(lcFlow2auth) << "Success getting the appPassword for user: " << loginName << ", server: " << serverUrl.toString();
_account->setUrl(serverUrl);
emit result(LoggedIn, loginName, appPassword);
emit result(LoggedIn, QString(), loginName, appPassword);
// Forget sensitive data
appPassword.clear();
loginName.clear();
_loginUrl.clear();
_pollToken.clear();
_pollEndpoint.clear();
_isBusy = false;
_hasToken = false;
});
}
void Flow2Auth::slotPollNow()
{
// poll now if we're not already doing so
if(_isBusy || !_hasToken)
return;
_secondsLeft = 1;
slotPollTimerTimeout();
}
} // namespace OCC

View File

@@ -25,17 +25,23 @@ namespace OCC {
* Job that does the authorization, grants and fetches the access token via Login Flow v2
*
* See: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2
*
*/
class Flow2Auth : public QObject
{
Q_OBJECT
public:
Flow2Auth(Account *account, QObject *parent)
: QObject(parent)
, _account(account)
{
}
enum TokenAction {
actionOpenBrowser = 1,
actionCopyLinkToClipboard
};
enum PollStatus {
statusPollCountdown = 1,
statusPollNow,
statusFetchToken,
statusCopyLinkToClipboard
};
Flow2Auth(Account *account, QObject *parent);
~Flow2Auth();
enum Result { NotSupported,
@@ -44,6 +50,7 @@ public:
Q_ENUM(Result);
void start();
void openBrowser();
void copyLinkToClipboard();
QUrl authorisationLink() const;
signals:
@@ -51,18 +58,29 @@ signals:
* The state has changed.
* when logged in, appPassword has the value of the app password.
*/
void result(Flow2Auth::Result result, const QString &user = QString(), const QString &appPassword = QString());
void result(Flow2Auth::Result result, const QString &errorString = QString(),
const QString &user = QString(), const QString &appPassword = QString());
void statusChanged(const PollStatus status, int secondsLeft);
public slots:
void slotPollNow();
private slots:
void slotPollTimerTimeout();
private:
void fetchNewToken(const TokenAction action);
Account *_account;
QUrl _loginUrl;
QString _pollToken;
QString _pollEndpoint;
QTimer _pollTimer;
int _secondsLeft;
int _secondsInterval;
bool _isBusy;
bool _hasToken;
};
} // namespace OCC

View File

@@ -0,0 +1,221 @@
/*
* Copyright (C) by Michael Schuster <michael@nextcloud.com>
*
* 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.
*/
#include "account.h"
#include "keychainchunk.h"
#include "theme.h"
#include "networkjobs.h"
#include "configfile.h"
#include "creds/abstractcredentials.h"
using namespace QKeychain;
namespace OCC {
Q_LOGGING_CATEGORY(lcKeychainChunk, "nextcloud.sync.credentials.keychainchunk", QtInfoMsg)
namespace KeychainChunk {
#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK)
static void addSettingsToJob(Account *account, QKeychain::Job *job)
{
Q_UNUSED(account)
auto settings = ConfigFile::settingsWithGroup(Theme::instance()->appName());
settings->setParent(job); // make the job parent to make setting deleted properly
job->setSettings(settings.release());
}
#endif
/*
* Job
*/
Job::Job(QObject *parent)
: QObject(parent)
{
_serviceName = Theme::instance()->appName();
}
/*
* WriteJob
*/
WriteJob::WriteJob(Account *account, const QString &key, const QByteArray &data, QObject *parent)
: Job(parent)
{
_account = account;
_key = key;
// Windows workaround: Split the private key into chunks of 2048 bytes,
// to allow 4k (4096 bit) keys to be saved (obey Windows's limits)
_chunkBuffer = data;
_chunkCount = 0;
}
void WriteJob::start()
{
slotWriteJobDone(nullptr);
}
void WriteJob::slotWriteJobDone(QKeychain::Job *incomingJob)
{
QKeychain::WritePasswordJob *writeJob = static_cast<QKeychain::WritePasswordJob *>(incomingJob);
// errors?
if (writeJob) {
_error = writeJob->error();
_errorString = writeJob->errorString();
if (writeJob->error() != NoError) {
qCWarning(lcKeychainChunk) << "Error while writing" << writeJob->key() << "chunk" << writeJob->errorString();
_chunkBuffer.clear();
}
}
// write a chunk if there is any in the buffer
if (!_chunkBuffer.isEmpty()) {
#if defined(Q_OS_WIN)
// Windows workaround: Split the data into chunks of 2048 bytes,
// to allow 4k (4096 bit) keys to be saved (obey Windows's limits)
auto chunk = _chunkBuffer.left(KeychainChunk::ChunkSize);
_chunkBuffer = _chunkBuffer.right(_chunkBuffer.size() - chunk.size());
#else
// write full data in one chunk on non-Windows, as usual
auto chunk = _chunkBuffer;
_chunkBuffer.clear();
#endif
auto index = (_chunkCount++);
// keep the limit
if (_chunkCount > KeychainChunk::MaxChunks) {
qCWarning(lcKeychainChunk) << "Maximum chunk count exceeded while writing" << writeJob->key() << "chunk" << QString::number(index) << "cutting off after" << QString::number(KeychainChunk::MaxChunks) << "chunks";
writeJob->deleteLater();
_chunkBuffer.clear();
emit finished(this);
return;
}
const QString kck = AbstractCredentials::keychainKey(
_account->url().toString(),
_key + (index > 0 ? (QString(".") + QString::number(index)) : QString()),
_account->id());
QKeychain::WritePasswordJob *job = new QKeychain::WritePasswordJob(_serviceName);
#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK)
addSettingsToJob(_account, job);
#endif
job->setInsecureFallback(_insecureFallback);
connect(job, &QKeychain::Job::finished, this, &KeychainChunk::WriteJob::slotWriteJobDone);
// only add the key's (sub)"index" after the first element, to stay compatible with older versions and non-Windows
job->setKey(kck);
job->setBinaryData(chunk);
job->start();
chunk.clear();
} else {
emit finished(this);
}
writeJob->deleteLater();
}
/*
* ReadJob
*/
ReadJob::ReadJob(Account *account, const QString &key, const bool &keychainMigration, QObject *parent)
: Job(parent)
{
_account = account;
_key = key;
_keychainMigration = keychainMigration;
_chunkCount = 0;
_chunkBuffer.clear();
}
void ReadJob::start()
{
_chunkCount = 0;
_chunkBuffer.clear();
const QString kck = AbstractCredentials::keychainKey(
_account->url().toString(),
_key,
_keychainMigration ? QString() : _account->id());
QKeychain::ReadPasswordJob *job = new QKeychain::ReadPasswordJob(_serviceName);
#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK)
addSettingsToJob(_account, job);
#endif
job->setInsecureFallback(_insecureFallback);
job->setKey(kck);
connect(job, &QKeychain::Job::finished, this, &KeychainChunk::ReadJob::slotReadJobDone);
job->start();
}
void ReadJob::slotReadJobDone(QKeychain::Job *incomingJob)
{
// Errors or next chunk?
QKeychain::ReadPasswordJob *readJob = static_cast<QKeychain::ReadPasswordJob *>(incomingJob);
if (readJob) {
if (readJob->error() == NoError && readJob->binaryData().length() > 0) {
_chunkBuffer.append(readJob->binaryData());
_chunkCount++;
#if defined(Q_OS_WIN)
// try to fetch next chunk
if (_chunkCount < KeychainChunk::MaxChunks) {
const QString kck = AbstractCredentials::keychainKey(
_account->url().toString(),
_key + QString(".") + QString::number(_chunkCount),
_keychainMigration ? QString() : _account->id());
QKeychain::ReadPasswordJob *job = new QKeychain::ReadPasswordJob(_serviceName);
#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK)
addSettingsToJob(_account, job);
#endif
job->setInsecureFallback(_insecureFallback);
job->setKey(kck);
connect(job, &QKeychain::Job::finished, this, &KeychainChunk::ReadJob::slotReadJobDone);
job->start();
readJob->deleteLater();
return;
} else {
qCWarning(lcKeychainChunk) << "Maximum chunk count for" << readJob->key() << "reached, ignoring after" << KeychainChunk::MaxChunks;
}
#endif
} else {
if (readJob->error() != QKeychain::Error::EntryNotFound ||
((readJob->error() == QKeychain::Error::EntryNotFound) && _chunkCount == 0)) {
_error = readJob->error();
_errorString = readJob->errorString();
qCWarning(lcKeychainChunk) << "Unable to read" << readJob->key() << "chunk" << QString::number(_chunkCount) << readJob->errorString();
}
}
readJob->deleteLater();
}
emit finished(this);
}
} // namespace KeychainChunk
} // namespace OCC

View File

@@ -0,0 +1,120 @@
/*
* Copyright (C) by Michael Schuster <michael@nextcloud.com>
*
* 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.
*/
#pragma once
#ifndef KEYCHAINCHUNK_H
#define KEYCHAINCHUNK_H
#include <QObject>
#include <keychain.h>
#include "accountfwd.h"
// We don't support insecure fallback
// #define KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK
namespace OCC {
namespace KeychainChunk {
/*
* Workaround for Windows:
*
* Split the keychain entry's data into chunks of 2048 bytes,
* to allow 4k (4096 bit) keys / large certs to be saved (see limits in webflowcredentials.h)
*/
static constexpr int ChunkSize = 2048;
static constexpr int MaxChunks = 10;
/*
* @brief: Abstract base class for KeychainChunk jobs.
*/
class Job : public QObject {
Q_OBJECT
public:
Job(QObject *parent = nullptr);
const QKeychain::Error error() const {
return _error;
}
const QString errorString() const {
return _errorString;
}
QByteArray binaryData() const {
return _chunkBuffer;
}
const bool insecureFallback() const {
return _insecureFallback;
}
// If we use it but don't support insecure fallback, give us nice compilation errors ;p
#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK)
void setInsecureFallback(const bool &insecureFallback)
{
_insecureFallback = insecureFallback;
}
#endif
protected:
QString _serviceName;
Account *_account;
QString _key;
bool _insecureFallback = false;
bool _keychainMigration = false;
QKeychain::Error _error = QKeychain::NoError;
QString _errorString;
int _chunkCount = 0;
QByteArray _chunkBuffer;
}; // class Job
/*
* @brief: Simple wrapper class for QKeychain::WritePasswordJob, splits too large keychain entry's data into chunks on Windows
*/
class WriteJob : public KeychainChunk::Job {
Q_OBJECT
public:
WriteJob(Account *account, const QString &key, const QByteArray &data, QObject *parent = nullptr);
void start();
signals:
void finished(KeychainChunk::WriteJob *incomingJob);
private slots:
void slotWriteJobDone(QKeychain::Job *incomingJob);
}; // class WriteJob
/*
* @brief: Simple wrapper class for QKeychain::ReadPasswordJob, splits too large keychain entry's data into chunks on Windows
*/
class ReadJob : public KeychainChunk::Job {
Q_OBJECT
public:
ReadJob(Account *account, const QString &key, const bool &keychainMigration, QObject *parent = nullptr);
void start();
signals:
void finished(KeychainChunk::ReadJob *incomingJob);
private slots:
void slotReadJobDone(QKeychain::Job *incomingJob);
}; // class ReadJob
} // namespace KeychainChunk
} // namespace OCC
#endif // KEYCHAINCHUNK_H

View File

@@ -18,6 +18,7 @@
#include "theme.h"
#include "wizard/webview.h"
#include "webflowcredentialsdialog.h"
#include "keychainchunk.h"
using namespace QKeychain;
@@ -75,6 +76,7 @@ private:
QPointer<const WebFlowCredentials> _cred;
};
#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK)
static void addSettingsToJob(Account *account, QKeychain::Job *job)
{
Q_UNUSED(account)
@@ -82,6 +84,7 @@ static void addSettingsToJob(Account *account, QKeychain::Job *job)
settings->setParent(job); // make the job parent to make setting deleted properly
job->setSettings(settings.release());
}
#endif
WebFlowCredentials::WebFlowCredentials()
: _ready(false)
@@ -170,6 +173,7 @@ void WebFlowCredentials::askFromUser() {
_askDialog->show();
connect(_askDialog, &WebFlowCredentialsDialog::urlCatched, this, &WebFlowCredentials::slotAskFromUserCredentialsProvided);
connect(_askDialog, &WebFlowCredentialsDialog::onClose, this, &WebFlowCredentials::slotAskFromUserCancelled);
});
job->start();
@@ -205,10 +209,18 @@ void WebFlowCredentials::slotAskFromUserCredentialsProvided(const QString &user,
emit asked();
_askDialog->close();
delete _askDialog;
_askDialog->deleteLater();
_askDialog = nullptr;
}
void WebFlowCredentials::slotAskFromUserCancelled() {
qCDebug(lcWebFlowCredentials()) << "User cancelled reauth!";
emit asked();
_askDialog->deleteLater();
_askDialog = nullptr;
}
bool WebFlowCredentials::stillValid(QNetworkReply *reply) {
if (reply->error() != QNetworkReply::NoError) {
@@ -229,86 +241,32 @@ void WebFlowCredentials::persist() {
// write cert if there is one
if (!_clientSslCertificate.isNull()) {
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
addSettingsToJob(_account, job);
job->setInsecureFallback(false);
connect(job, &Job::finished, this, &WebFlowCredentials::slotWriteClientCertPEMJobDone);
job->setKey(keychainKey(_account->url().toString(), _user + clientCertificatePEMC, _account->id()));
job->setBinaryData(_clientSslCertificate.toPem());
auto *job = new KeychainChunk::WriteJob(_account,
_user + clientCertificatePEMC,
_clientSslCertificate.toPem());
connect(job, &KeychainChunk::WriteJob::finished, this, &WebFlowCredentials::slotWriteClientCertPEMJobDone);
job->start();
} else {
// no cert, just write credentials
slotWriteClientCertPEMJobDone();
slotWriteClientCertPEMJobDone(nullptr);
}
}
void WebFlowCredentials::slotWriteClientCertPEMJobDone()
void WebFlowCredentials::slotWriteClientCertPEMJobDone(KeychainChunk::WriteJob *writeJob)
{
if(writeJob)
writeJob->deleteLater();
// write ssl key if there is one
if (!_clientSslKey.isNull()) {
// Windows workaround: Split the private key into chunks of 2048 bytes,
// to allow 4k (4096 bit) keys to be saved (obey Windows's limits)
_clientSslKeyChunkBufferPEM = _clientSslKey.toPem();
_clientSslKeyChunkCount = 0;
writeSingleClientKeyChunkPEM(nullptr);
auto *job = new KeychainChunk::WriteJob(_account,
_user + clientKeyPEMC,
_clientSslKey.toPem());
connect(job, &KeychainChunk::WriteJob::finished, this, &WebFlowCredentials::slotWriteClientKeyPEMJobDone);
job->start();
} else {
// no key, just write credentials
slotWriteClientKeyPEMJobDone();
}
}
void WebFlowCredentials::writeSingleClientKeyChunkPEM(QKeychain::Job *incomingJob)
{
// errors?
if (incomingJob) {
WritePasswordJob *writeJob = static_cast<WritePasswordJob *>(incomingJob);
if (writeJob->error() != NoError) {
qCWarning(lcWebFlowCredentials) << "Error while writing client CA key chunk" << writeJob->errorString();
_clientSslKeyChunkBufferPEM.clear();
}
}
// write a key chunk if there is any in the buffer
if (!_clientSslKeyChunkBufferPEM.isEmpty()) {
#if defined(Q_OS_WIN)
// Windows workaround: Split the private key into chunks of 2048 bytes,
// to allow 4k (4096 bit) keys to be saved (obey Windows's limits)
auto chunk = _clientSslKeyChunkBufferPEM.left(_clientSslKeyChunkSize);
_clientSslKeyChunkBufferPEM = _clientSslKeyChunkBufferPEM.right(_clientSslKeyChunkBufferPEM.size() - chunk.size());
#else
// write full key in one slot on non-Windows, as usual
auto chunk = _clientSslKeyChunkBufferPEM;
_clientSslKeyChunkBufferPEM.clear();
#endif
auto index = (_clientSslKeyChunkCount++);
// keep the limit
if (_clientSslKeyChunkCount > _clientSslKeyMaxChunks) {
qCWarning(lcWebFlowCredentials) << "Maximum client key chunk count exceeded while writing slot" << QString::number(index) << "cutting off after" << QString::number(_clientSslKeyMaxChunks) << "chunks";
_clientSslKeyChunkBufferPEM.clear();
slotWriteClientKeyPEMJobDone();
return;
}
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
addSettingsToJob(_account, job);
job->setInsecureFallback(false);
connect(job, &Job::finished, this, &WebFlowCredentials::writeSingleClientKeyChunkPEM);
// only add the key's (sub)"index" after the first element, to stay compatible with older versions and non-Windows
job->setKey(keychainKey(_account->url().toString(), _user + clientKeyPEMC + (index > 0 ? (QString(".") + QString::number(index)) : QString()), _account->id()));
job->setBinaryData(chunk);
job->start();
chunk.clear();
} else {
slotWriteClientKeyPEMJobDone();
slotWriteClientKeyPEMJobDone(nullptr);
}
}
@@ -331,20 +289,21 @@ void WebFlowCredentials::writeSingleClientCaCertPEM()
return;
}
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
addSettingsToJob(_account, job);
job->setInsecureFallback(false);
connect(job, &Job::finished, this, &WebFlowCredentials::slotWriteClientCaCertsPEMJobDone);
job->setKey(keychainKey(_account->url().toString(), _user + clientCaCertificatePEMC + QString::number(index), _account->id()));
job->setBinaryData(cert.toPem());
auto *job = new KeychainChunk::WriteJob(_account,
_user + clientCaCertificatePEMC + QString::number(index),
cert.toPem());
connect(job, &KeychainChunk::WriteJob::finished, this, &WebFlowCredentials::slotWriteClientCaCertsPEMJobDone);
job->start();
} else {
slotWriteClientCaCertsPEMJobDone(nullptr);
}
}
void WebFlowCredentials::slotWriteClientKeyPEMJobDone()
void WebFlowCredentials::slotWriteClientKeyPEMJobDone(KeychainChunk::WriteJob *writeJob)
{
if(writeJob)
writeJob->deleteLater();
_clientSslCaCertificatesWriteQueue.clear();
// write ca certs if there are any
@@ -359,16 +318,16 @@ void WebFlowCredentials::slotWriteClientKeyPEMJobDone()
}
}
void WebFlowCredentials::slotWriteClientCaCertsPEMJobDone(QKeychain::Job *incomingJob)
void WebFlowCredentials::slotWriteClientCaCertsPEMJobDone(KeychainChunk::WriteJob *writeJob)
{
// errors / next ca cert?
if (incomingJob && !_clientSslCaCertificates.isEmpty()) {
WritePasswordJob *writeJob = static_cast<WritePasswordJob *>(incomingJob);
if (writeJob && !_clientSslCaCertificates.isEmpty()) {
if (writeJob->error() != NoError) {
qCWarning(lcWebFlowCredentials) << "Error while writing client CA cert" << writeJob->errorString();
}
writeJob->deleteLater();
if (!_clientSslCaCertificatesWriteQueue.isEmpty()) {
// next ca cert
writeSingleClientCaCertPEM();
@@ -378,7 +337,9 @@ void WebFlowCredentials::slotWriteClientCaCertsPEMJobDone(QKeychain::Job *incomi
// done storing ca certs, time for the password
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK)
addSettingsToJob(_account, job);
#endif
job->setInsecureFallback(false);
connect(job, &Job::finished, this, &WebFlowCredentials::slotWriteJobDone);
job->setKey(keychainKey(_account->url().toString(), _user, _account->id()));
@@ -428,6 +389,10 @@ void WebFlowCredentials::forgetSensitiveData() {
DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName());
job->setInsecureFallback(false);
job->setKey(kck);
connect(job, &Job::finished, this, [](QKeychain::Job *job) {
DeletePasswordJob *djob = qobject_cast<DeletePasswordJob *>(job);
djob->deleteLater();
});
job->start();
invalidateToken();
@@ -478,29 +443,23 @@ void WebFlowCredentials::slotFinished(QNetworkReply *reply) {
void WebFlowCredentials::fetchFromKeychainHelper() {
// Read client cert from keychain
const QString kck = keychainKey(
_account->url().toString(),
_user + clientCertificatePEMC,
_keychainMigration ? QString() : _account->id());
ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
addSettingsToJob(_account, job);
job->setInsecureFallback(false);
job->setKey(kck);
connect(job, &Job::finished, this, &WebFlowCredentials::slotReadClientCertPEMJobDone);
auto *job = new KeychainChunk::ReadJob(_account,
_user + clientCertificatePEMC,
_keychainMigration);
connect(job, &KeychainChunk::ReadJob::finished, this, &WebFlowCredentials::slotReadClientCertPEMJobDone);
job->start();
}
void WebFlowCredentials::slotReadClientCertPEMJobDone(QKeychain::Job *incomingJob)
void WebFlowCredentials::slotReadClientCertPEMJobDone(KeychainChunk::ReadJob *readJob)
{
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
Q_ASSERT(!incomingJob->insecureFallback()); // If insecureFallback is set, the next test would be pointless
if (_retryOnKeyChainError && (incomingJob->error() == QKeychain::NoBackendAvailable
|| incomingJob->error() == QKeychain::OtherError)) {
Q_ASSERT(!readJob->insecureFallback()); // If insecureFallback is set, the next test would be pointless
if (_retryOnKeyChainError && (readJob->error() == QKeychain::NoBackendAvailable
|| readJob->error() == QKeychain::OtherError)) {
// Could be that the backend was not yet available. Wait some extra seconds.
// (Issues #4274 and #6522)
// (For kwallet, the error is OtherError instead of NoBackendAvailable, maybe a bug in QtKeychain)
qCInfo(lcWebFlowCredentials) << "Backend unavailable (yet?) Retrying in a few seconds." << incomingJob->errorString();
qCInfo(lcWebFlowCredentials) << "Backend unavailable (yet?) Retrying in a few seconds." << readJob->errorString();
QTimer::singleShot(10000, this, &WebFlowCredentials::fetchFromKeychainHelper);
_retryOnKeyChainError = false;
return;
@@ -509,7 +468,6 @@ void WebFlowCredentials::slotReadClientCertPEMJobDone(QKeychain::Job *incomingJo
#endif
// Store PEM in memory
ReadPasswordJob *readJob = static_cast<ReadPasswordJob *>(incomingJob);
if (readJob->error() == NoError && readJob->binaryData().length() > 0) {
QList<QSslCertificate> sslCertificateList = QSslCertificate::fromData(readJob->binaryData(), QSsl::Pem);
if (sslCertificateList.length() >= 1) {
@@ -517,79 +475,40 @@ void WebFlowCredentials::slotReadClientCertPEMJobDone(QKeychain::Job *incomingJo
}
}
readJob->deleteLater();
// Load key too
_clientSslKeyChunkCount = 0;
_clientSslKeyChunkBufferPEM.clear();
const QString kck = keychainKey(
_account->url().toString(),
_user + clientKeyPEMC,
_keychainMigration ? QString() : _account->id());
ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
addSettingsToJob(_account, job);
job->setInsecureFallback(false);
job->setKey(kck);
connect(job, &Job::finished, this, &WebFlowCredentials::slotReadClientKeyPEMJobDone);
auto *job = new KeychainChunk::ReadJob(_account,
_user + clientKeyPEMC,
_keychainMigration);
connect(job, &KeychainChunk::ReadJob::finished, this, &WebFlowCredentials::slotReadClientKeyPEMJobDone);
job->start();
}
void WebFlowCredentials::slotReadClientKeyPEMJobDone(QKeychain::Job *incomingJob)
void WebFlowCredentials::slotReadClientKeyPEMJobDone(KeychainChunk::ReadJob *readJob)
{
// Errors or next key chunk?
ReadPasswordJob *readJob = static_cast<ReadPasswordJob *>(incomingJob);
if (readJob) {
if (readJob->error() == NoError && readJob->binaryData().length() > 0) {
_clientSslKeyChunkBufferPEM.append(readJob->binaryData());
_clientSslKeyChunkCount++;
#if defined(Q_OS_WIN)
// try to fetch next chunk
if (_clientSslKeyChunkCount < _clientSslKeyMaxChunks) {
const QString kck = keychainKey(
_account->url().toString(),
_user + clientKeyPEMC + QString(".") + QString::number(_clientSslKeyChunkCount),
_keychainMigration ? QString() : _account->id());
ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
addSettingsToJob(_account, job);
job->setInsecureFallback(false);
job->setKey(kck);
connect(job, &Job::finished, this, &WebFlowCredentials::slotReadClientKeyPEMJobDone);
job->start();
return;
} else {
qCWarning(lcWebFlowCredentials) << "Maximum client key chunk count reached, ignoring after" << _clientSslKeyMaxChunks;
}
#endif
} else {
if (readJob->error() != QKeychain::Error::EntryNotFound ||
((readJob->error() == QKeychain::Error::EntryNotFound) && _clientSslKeyChunkCount == 0)) {
qCWarning(lcWebFlowCredentials) << "Unable to read client key chunk slot" << QString::number(_clientSslKeyChunkCount) << readJob->errorString();
}
}
}
// Store key in memory
if (_clientSslKeyChunkBufferPEM.size() > 0) {
if (readJob->error() == NoError && readJob->binaryData().length() > 0) {
QByteArray clientKeyPEM = readJob->binaryData();
// FIXME Unfortunately Qt has a bug and we can't just use QSsl::Opaque to let it
// load whatever we have. So we try until it works.
_clientSslKey = QSslKey(_clientSslKeyChunkBufferPEM, QSsl::Rsa);
_clientSslKey = QSslKey(clientKeyPEM, QSsl::Rsa);
if (_clientSslKey.isNull()) {
_clientSslKey = QSslKey(_clientSslKeyChunkBufferPEM, QSsl::Dsa);
_clientSslKey = QSslKey(clientKeyPEM, QSsl::Dsa);
}
if (_clientSslKey.isNull()) {
_clientSslKey = QSslKey(_clientSslKeyChunkBufferPEM, QSsl::Ec);
_clientSslKey = QSslKey(clientKeyPEM, QSsl::Ec);
}
if (_clientSslKey.isNull()) {
qCWarning(lcWebFlowCredentials) << "Could not load SSL key into Qt!";
}
// clear key chunk buffer, but don't set _clientSslKeyChunkCount to zero because we need it for deleteKeychainEntries
_clientSslKeyChunkBufferPEM.clear();
clientKeyPEM.clear();
} else {
qCWarning(lcWebFlowCredentials) << "Unable to read client key" << readJob->errorString();
}
readJob->deleteLater();
// Start fetching client CA certs
_clientSslCaCertificates.clear();
@@ -600,16 +519,10 @@ void WebFlowCredentials::readSingleClientCaCertPEM()
{
// try to fetch a client ca cert
if (_clientSslCaCertificates.count() < _clientSslCaCertificatesMaxCount) {
const QString kck = keychainKey(
_account->url().toString(),
_user + clientCaCertificatePEMC + QString::number(_clientSslCaCertificates.count()),
_keychainMigration ? QString() : _account->id());
ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
addSettingsToJob(_account, job);
job->setInsecureFallback(false);
job->setKey(kck);
connect(job, &Job::finished, this, &WebFlowCredentials::slotReadClientCaCertsPEMJobDone);
auto *job = new KeychainChunk::ReadJob(_account,
_user + clientCaCertificatePEMC + QString::number(_clientSslCaCertificates.count()),
_keychainMigration);
connect(job, &KeychainChunk::ReadJob::finished, this, &WebFlowCredentials::slotReadClientCaCertsPEMJobDone);
job->start();
} else {
qCWarning(lcWebFlowCredentials) << "Maximum client CA cert count exceeded while reading, ignoring after" << _clientSslCaCertificatesMaxCount;
@@ -618,10 +531,8 @@ void WebFlowCredentials::readSingleClientCaCertPEM()
}
}
void WebFlowCredentials::slotReadClientCaCertsPEMJobDone(QKeychain::Job *incomingJob) {
// Store key in memory
ReadPasswordJob *readJob = static_cast<ReadPasswordJob *>(incomingJob);
void WebFlowCredentials::slotReadClientCaCertsPEMJobDone(KeychainChunk::ReadJob *readJob) {
// Store cert in memory
if (readJob) {
if (readJob->error() == NoError && readJob->binaryData().length() > 0) {
QList<QSslCertificate> sslCertificateList = QSslCertificate::fromData(readJob->binaryData(), QSsl::Pem);
@@ -629,6 +540,8 @@ void WebFlowCredentials::slotReadClientCaCertsPEMJobDone(QKeychain::Job *incomin
_clientSslCaCertificates.append(sslCertificateList.at(0));
}
readJob->deleteLater();
// try next cert
readSingleClientCaCertPEM();
return;
@@ -638,6 +551,8 @@ void WebFlowCredentials::slotReadClientCaCertsPEMJobDone(QKeychain::Job *incomin
qCWarning(lcWebFlowCredentials) << "Unable to read client CA cert slot" << QString::number(_clientSslCaCertificates.count()) << readJob->errorString();
}
}
readJob->deleteLater();
}
// Now fetch the actual server password
@@ -647,7 +562,9 @@ void WebFlowCredentials::slotReadClientCaCertsPEMJobDone(QKeychain::Job *incomin
_keychainMigration ? QString() : _account->id());
ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK)
addSettingsToJob(_account, job);
#endif
job->setInsecureFallback(false);
job->setKey(kck);
connect(job, &Job::finished, this, &WebFlowCredentials::slotReadPasswordJobDone);
@@ -655,7 +572,7 @@ void WebFlowCredentials::slotReadClientCaCertsPEMJobDone(QKeychain::Job *incomin
}
void WebFlowCredentials::slotReadPasswordJobDone(Job *incomingJob) {
QKeychain::ReadPasswordJob *job = static_cast<ReadPasswordJob *>(incomingJob);
QKeychain::ReadPasswordJob *job = qobject_cast<ReadPasswordJob *>(incomingJob);
QKeychain::Error error = job->error();
// If we could not find the entry try the old entries
@@ -678,6 +595,8 @@ void WebFlowCredentials::slotReadPasswordJobDone(Job *incomingJob) {
}
emit fetched();
job->deleteLater();
// If keychain data was read from legacy location, wipe these entries and store new ones
if (_keychainMigration && _ready) {
_keychainMigration = false;
@@ -688,13 +607,20 @@ void WebFlowCredentials::slotReadPasswordJobDone(Job *incomingJob) {
}
void WebFlowCredentials::deleteKeychainEntries(bool oldKeychainEntries) {
auto startDeleteJob = [this, oldKeychainEntries](QString user) {
auto startDeleteJob = [this, oldKeychainEntries](QString key) {
DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName());
#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK)
addSettingsToJob(_account, job);
#endif
job->setInsecureFallback(false);
job->setKey(keychainKey(_account->url().toString(),
user,
key,
oldKeychainEntries ? QString() : _account->id()));
connect(job, &Job::finished, this, [](QKeychain::Job *job) {
DeletePasswordJob *djob = qobject_cast<DeletePasswordJob *>(job);
djob->deleteLater();
});
job->start();
};
@@ -719,9 +645,17 @@ void WebFlowCredentials::deleteKeychainEntries(bool oldKeychainEntries) {
}
#if defined(Q_OS_WIN)
// also delete key sub-chunks (Windows workaround)
for (auto i = 1; i < _clientSslKeyChunkCount; i++) {
startDeleteJob(_user + clientKeyPEMC + QString(".") + QString::number(i));
// Also delete key / cert sub-chunks (Windows workaround)
// The first chunk (0) has no suffix, to stay compatible with older versions and non-Windows
for (auto chunk = 1; chunk < KeychainChunk::MaxChunks; chunk++) {
const QString strChunkSuffix = QString(".") + QString::number(chunk);
startDeleteJob(_user + clientKeyPEMC + strChunkSuffix);
startDeleteJob(_user + clientCertificatePEMC + strChunkSuffix);
for (auto i = 0; i < _clientSslCaCertificates.count(); i++) {
startDeleteJob(_user + clientCaCertificatePEMC + QString::number(i));
}
}
#endif
// FIXME MS@2019-12-07 -->
@@ -729,4 +663,4 @@ void WebFlowCredentials::deleteKeychainEntries(bool oldKeychainEntries) {
// <-- FIXME MS@2019-12-07
}
}
} // namespace OCC

View File

@@ -19,6 +19,11 @@ namespace QKeychain {
namespace OCC {
namespace KeychainChunk {
class ReadJob;
class WriteJob;
}
class WebFlowCredentialsDialog;
class WebFlowCredentials : public AbstractCredentials
@@ -61,15 +66,16 @@ private slots:
void slotFinished(QNetworkReply *reply);
void slotAskFromUserCredentialsProvided(const QString &user, const QString &pass, const QString &host);
void slotAskFromUserCancelled();
void slotReadClientCertPEMJobDone(QKeychain::Job *incomingJob);
void slotReadClientKeyPEMJobDone(QKeychain::Job *incomingJob);
void slotReadClientCaCertsPEMJobDone(QKeychain::Job *incommingJob);
void slotReadClientCertPEMJobDone(KeychainChunk::ReadJob *readJob);
void slotReadClientKeyPEMJobDone(KeychainChunk::ReadJob *readJob);
void slotReadClientCaCertsPEMJobDone(KeychainChunk::ReadJob *readJob);
void slotReadPasswordJobDone(QKeychain::Job *incomingJob);
void slotWriteClientCertPEMJobDone();
void slotWriteClientKeyPEMJobDone();
void slotWriteClientCaCertsPEMJobDone(QKeychain::Job *incomingJob);
void slotWriteClientCertPEMJobDone(KeychainChunk::WriteJob *writeJob);
void slotWriteClientKeyPEMJobDone(KeychainChunk::WriteJob *writeJob);
void slotWriteClientCaCertsPEMJobDone(KeychainChunk::WriteJob *writeJob);
void slotWriteJobDone(QKeychain::Job *);
private:
@@ -91,19 +97,6 @@ private:
static constexpr int _clientSslCaCertificatesMaxCount = 10;
QQueue<QSslCertificate> _clientSslCaCertificatesWriteQueue;
/*
* Workaround: ...and this time only on Windows:
*
* Split the private key into chunks of 2048 bytes,
* to allow 4k (4096 bit) keys to be saved (see limits above)
*/
void writeSingleClientKeyChunkPEM(QKeychain::Job *incomingJob);
static constexpr int _clientSslKeyChunkSize = 2048;
static constexpr int _clientSslKeyMaxChunks = 10;
int _clientSslKeyChunkCount = 0;
QByteArray _clientSslKeyChunkBufferPEM;
protected:
/** Reads data from keychain locations
*
@@ -134,6 +127,6 @@ protected:
WebFlowCredentialsDialog *_askDialog;
};
}
} // namespace OCC
#endif // WEBFLOWCREDENTIALS_H

View File

@@ -4,6 +4,9 @@
#include <QLabel>
#include "theme.h"
#include "application.h"
#include "owncloudgui.h"
#include "headerbanner.h"
#include "wizard/owncloudwizardcommon.h"
#include "wizard/webview.h"
#include "wizard/flow2authwidget.h"
@@ -19,31 +22,59 @@ WebFlowCredentialsDialog::WebFlowCredentialsDialog(Account *account, bool useFlo
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
_layout = new QVBoxLayout(this);
int spacing = _layout->spacing();
int margin = _layout->margin();
_layout->setSpacing(0);
_layout->setMargin(0);
if(_useFlow2) {
_headerBanner = new HeaderBanner(this);
_layout->addWidget(_headerBanner);
Theme *theme = Theme::instance();
_headerBanner->setup(tr("Log in"), theme->wizardHeaderLogo(), theme->wizardHeaderBanner(),
Qt::AutoText, QString::fromLatin1("color:#fff;"));
}
_containerLayout = new QVBoxLayout(this);
_containerLayout->setSpacing(spacing);
_containerLayout->setMargin(margin);
//QString msg = tr("You have been logged out of %1 as user %2, please login again")
// .arg(_account->displayName(), _user);
_infoLabel = new QLabel();
_layout->addWidget(_infoLabel);
_containerLayout->addWidget(_infoLabel);
if (_useFlow2) {
_flow2AuthWidget = new Flow2AuthWidget(account);
_layout->addWidget(_flow2AuthWidget);
_flow2AuthWidget = new Flow2AuthWidget();
_containerLayout->addWidget(_flow2AuthWidget);
connect(_flow2AuthWidget, &Flow2AuthWidget::urlCatched, this, &WebFlowCredentialsDialog::urlCatched);
connect(_flow2AuthWidget, &Flow2AuthWidget::authResult, this, &WebFlowCredentialsDialog::slotFlow2AuthResult);
// Connect styleChanged events to our widgets, so they can adapt (Dark-/Light-Mode switching)
connect(this, &WebFlowCredentialsDialog::styleChanged, _flow2AuthWidget, &Flow2AuthWidget::slotStyleChanged);
// allow Flow2 page to poll on window activation
connect(this, &WebFlowCredentialsDialog::onActivate, _flow2AuthWidget, &Flow2AuthWidget::slotPollNow);
_flow2AuthWidget->startAuth(account);
} else {
_webView = new WebView();
_layout->addWidget(_webView);
_containerLayout->addWidget(_webView);
connect(_webView, &WebView::urlCatched, this, &WebFlowCredentialsDialog::urlCatched);
}
auto app = static_cast<Application *>(qApp);
connect(app, &Application::isShowingSettingsDialog, this, &WebFlowCredentialsDialog::slotShowSettingsDialog);
_errorLabel = new QLabel();
_errorLabel->hide();
_layout->addWidget(_errorLabel);
_containerLayout->addWidget(_errorLabel);
WizardCommon::initErrorLabel(_errorLabel);
_layout->addLayout(_containerLayout);
setLayout(_layout);
customizeStyle();
}
void WebFlowCredentialsDialog::closeEvent(QCloseEvent* e) {
@@ -52,11 +83,17 @@ void WebFlowCredentialsDialog::closeEvent(QCloseEvent* e) {
if (_webView) {
// Force calling WebView::~WebView() earlier so that _profile and _page are
// deleted in the correct order.
delete _webView;
_webView->deleteLater();
_webView = nullptr;
}
if (_flow2AuthWidget)
delete _flow2AuthWidget;
if (_flow2AuthWidget) {
_flow2AuthWidget->resetAuth();
_flow2AuthWidget->deleteLater();
_flow2AuthWidget = nullptr;
}
emit onClose();
}
void WebFlowCredentialsDialog::setUrl(const QUrl &url) {
@@ -69,6 +106,9 @@ void WebFlowCredentialsDialog::setInfo(const QString &msg) {
}
void WebFlowCredentialsDialog::setError(const QString &error) {
// bring window to top
slotShowSettingsDialog();
if (_useFlow2 && _flow2AuthWidget) {
_flow2AuthWidget->setError(error);
return;
@@ -82,4 +122,49 @@ void WebFlowCredentialsDialog::setError(const QString &error) {
}
}
void WebFlowCredentialsDialog::changeEvent(QEvent *e)
{
switch (e->type()) {
case QEvent::StyleChange:
case QEvent::PaletteChange:
case QEvent::ThemeChange:
customizeStyle();
// Notify the other widgets (Dark-/Light-Mode switching)
emit styleChanged();
break;
case QEvent::ActivationChange:
if(isActiveWindow())
emit onActivate();
break;
default:
break;
}
QDialog::changeEvent(e);
}
void WebFlowCredentialsDialog::customizeStyle()
{
// HINT: Customize dialog's own style here, if necessary in the future (Dark-/Light-Mode switching)
}
void WebFlowCredentialsDialog::slotShowSettingsDialog()
{
// bring window to top but slightly delay, to avoid being hidden behind the SettingsDialog
QTimer::singleShot(100, this, [this] {
ownCloudGui::raiseDialog(this);
});
}
void WebFlowCredentialsDialog::slotFlow2AuthResult(Flow2Auth::Result r, const QString &errorString, const QString &user, const QString &appPassword)
{
if(r == Flow2Auth::LoggedIn) {
emit urlCatched(user, appPassword, QString());
} else {
// bring window to top
slotShowSettingsDialog();
}
}
} // namespace OCC

View File

@@ -5,12 +5,14 @@
#include <QUrl>
#include "accountfwd.h"
#include "creds/flow2auth.h"
class QLabel;
class QVBoxLayout;
namespace OCC {
class HeaderBanner;
class WebView;
class Flow2AuthWidget;
@@ -30,11 +32,21 @@ public:
protected:
void closeEvent(QCloseEvent * e) override;
void changeEvent(QEvent *) override;
public slots:
void slotFlow2AuthResult(Flow2Auth::Result, const QString &errorString, const QString &user, const QString &appPassword);
void slotShowSettingsDialog();
signals:
void urlCatched(const QString user, const QString pass, const QString host);
void styleChanged();
void onActivate();
void onClose();
private:
void customizeStyle();
bool _useFlow2;
Flow2AuthWidget *_flow2AuthWidget;
@@ -43,8 +55,10 @@ private:
QLabel *_errorLabel;
QLabel *_infoLabel;
QVBoxLayout *_layout;
QVBoxLayout *_containerLayout;
HeaderBanner *_headerBanner;
};
}
} // namespace OCC
#endif // WEBFLOWCREDENTIALSDIALOG_H

View File

@@ -40,7 +40,7 @@ namespace OCC {
FolderStatusDelegate::FolderStatusDelegate()
: QStyledItemDelegate()
{
m_moreIcon = QIcon(QLatin1String(":/client/resources/more.svg"));
customizeStyle();
}
QString FolderStatusDelegate::addFolderText()
@@ -273,6 +273,11 @@ void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
rect.setHeight(texts.count() * subFm.height() + 2 * margin);
rect.setRight(option.rect.right() - margin);
// save previous state to not mess up colours with the background (fixes issue: https://github.com/nextcloud/desktop/issues/1237)
auto oldBrush = painter->brush();
auto oldPen = painter->pen();
auto oldFont = painter->font();
painter->setBrush(color);
painter->setPen(QColor(0xaa, 0xaa, 0xaa));
painter->drawRoundedRect(QStyle::visualRect(option.direction, option.rect, rect),
@@ -290,6 +295,11 @@ void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
textRect.translate(0, textRect.height());
}
// restore previous state
painter->setBrush(oldBrush);
painter->setPen(oldPen);
painter->setFont(oldFont);
h = rect.bottom() + margin;
};
@@ -349,7 +359,7 @@ void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
btnOpt.arrowType = Qt::NoArrow;
btnOpt.subControls = QStyle::SC_ToolButton;
btnOpt.rect = optionsButtonVisualRect;
btnOpt.icon = m_moreIcon;
btnOpt.icon = _iconMore;
int e = QApplication::style()->pixelMetric(QStyle::PM_ButtonIconSize);
btnOpt.iconSize = QSize(e,e);
QApplication::style()->drawComplexControl(QStyle::CC_ToolButton, &btnOpt, painter);
@@ -423,5 +433,14 @@ QRect FolderStatusDelegate::errorsListRect(QRect within)
return within;
}
void FolderStatusDelegate::slotStyleChanged()
{
customizeStyle();
}
void FolderStatusDelegate::customizeStyle()
{
_iconMore = Theme::createColorAwareIcon(QLatin1String(":/client/resources/more.svg"));
}
} // namespace OCC

View File

@@ -26,7 +26,6 @@ class FolderStatusDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
QIcon m_moreIcon;
FolderStatusDelegate();
enum datarole { FolderAliasRole = Qt::UserRole + 100,
@@ -62,9 +61,16 @@ public:
static QRect errorsListRect(QRect within);
static int rootFolderHeightWithoutErrors(const QFontMetrics &fm, const QFontMetrics &aliasFm);
public slots:
void slotStyleChanged();
private:
void customizeStyle();
static QString addFolderText();
QPersistentModelIndex _pressedIndex;
QIcon _iconMore;
};
} // namespace OCC

View File

@@ -41,7 +41,7 @@
<item>
<widget class="QPushButton" name="localFolderChooseBtn">
<property name="text">
<string>&amp;Choose...</string>
<string>&amp;Choose</string>
</property>
</widget>
</item>

146
src/gui/headerbanner.cpp Normal file
View File

@@ -0,0 +1,146 @@
/*
* Copyright (C) by Michael Schuster <michael@nextcloud.com>
*
* 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.
*/
/****************************************************************************
**
** Based on Qt sourcecode:
** qt5/qtbase/src/widgets/dialogs/qwizard.cpp
**
** https://code.qt.io/cgit/qt/qtbase.git/tree/src/widgets/dialogs/qwizard.cpp?h=v5.13.0
**
** Original license:
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtWidgets module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "headerbanner.h"
#include <QVBoxLayout>
#include <QLabel>
#include <QPainter>
#include <QStyle>
#include <QGuiApplication>
namespace OCC {
// These fudge terms were needed a few places to obtain pixel-perfect results
const int GapBetweenLogoAndRightEdge = 5;
const int ModernHeaderTopMargin = 2;
HeaderBanner::HeaderBanner(QWidget *parent)
: QWidget(parent)
{
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
setBackgroundRole(QPalette::Base);
titleLabel = new QLabel(this);
titleLabel->setBackgroundRole(QPalette::Base);
logoLabel = new QLabel(this);
QFont font = titleLabel->font();
font.setBold(true);
titleLabel->setFont(font);
layout = new QGridLayout(this);
layout->setContentsMargins(QMargins());
layout->setSpacing(0);
layout->setRowMinimumHeight(3, 1);
layout->setRowStretch(4, 1);
layout->setColumnStretch(2, 1);
layout->setColumnMinimumWidth(4, 2 * GapBetweenLogoAndRightEdge);
layout->setColumnMinimumWidth(6, GapBetweenLogoAndRightEdge);
layout->addWidget(titleLabel, 1, 1, 5, 1);
layout->addWidget(logoLabel, 1, 5, 5, 1);
}
void HeaderBanner::setup(const QString &title, const QPixmap &logo, const QPixmap &banner,
const Qt::TextFormat titleFormat, const QString &styleSheet)
{
QStyle *style = parentWidget()->style();
//const int layoutHorizontalSpacing = style->pixelMetric(QStyle::PM_LayoutHorizontalSpacing);
int topLevelMarginLeft = style->pixelMetric(QStyle::PM_LayoutLeftMargin, 0, parentWidget());
int topLevelMarginRight = style->pixelMetric(QStyle::PM_LayoutRightMargin, 0, parentWidget());
int topLevelMarginTop = style->pixelMetric(QStyle::PM_LayoutTopMargin, 0, parentWidget());
//int topLevelMarginBottom = style->pixelMetric(QStyle::PM_LayoutBottomMargin, 0, parentWidget());
layout->setRowMinimumHeight(0, ModernHeaderTopMargin);
layout->setRowMinimumHeight(1, topLevelMarginTop - ModernHeaderTopMargin - 1);
layout->setRowMinimumHeight(6, 3);
int minColumnWidth0 = topLevelMarginLeft + topLevelMarginRight;
int minColumnWidth1 = topLevelMarginLeft + topLevelMarginRight + 1;
layout->setColumnMinimumWidth(0, minColumnWidth0);
layout->setColumnMinimumWidth(1, minColumnWidth1);
titleLabel->setTextFormat(titleFormat);
titleLabel->setText(title);
if(!styleSheet.isEmpty())
titleLabel->setStyleSheet(styleSheet);
logoLabel->setPixmap(logo);
bannerPixmap = banner;
if (bannerPixmap.isNull()) {
QSize size = layout->totalMinimumSize();
setMinimumSize(size);
setMaximumSize(QWIDGETSIZE_MAX, size.height());
} else {
setFixedHeight(banner.height() + 2);
}
updateGeometry();
}
void HeaderBanner::paintEvent(QPaintEvent * /* event */)
{
QPainter painter(this);
painter.drawPixmap(0, 0, width(), bannerPixmap.height(), bannerPixmap);
int x = width() - 2;
int y = height() - 2;
const QPalette &pal = QGuiApplication::palette();
painter.setPen(pal.mid().color());
painter.drawLine(0, y, x, y);
painter.setPen(pal.base().color());
painter.drawPoint(x + 1, y);
painter.drawLine(0, y + 1, x + 1, y + 1);
}
} // namespace OCC

93
src/gui/headerbanner.h Normal file
View File

@@ -0,0 +1,93 @@
/*
* Copyright (C) by Michael Schuster <michael@nextcloud.com>
*
* 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.
*/
/****************************************************************************
**
** Based on Qt sourcecode:
** qt5/qtbase/src/widgets/dialogs/qwizard.cpp
**
** https://code.qt.io/cgit/qt/qtbase.git/tree/src/widgets/dialogs/qwizard.cpp?h=v5.13.0
**
** Original license:
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtWidgets module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#ifndef HEADERBANNER_H
#define HEADERBANNER_H
#include <QWidget>
class QLabel;
class QGridLayout;
class QPixmap;
namespace OCC {
class HeaderBanner : public QWidget
{
Q_OBJECT
public:
HeaderBanner(QWidget *parent = 0);
void setup(const QString &title, const QPixmap &logo, const QPixmap &banner,
const Qt::TextFormat titleFormat, const QString &styleSheet);
protected:
void paintEvent(QPaintEvent *event) override;
private:
QLabel *titleLabel;
QLabel *logoLabel;
QGridLayout *layout;
QPixmap bannerPixmap;
};
} // namespace OCC
#endif // HEADERBANNER_H

View File

@@ -24,7 +24,7 @@ LegalNotice::LegalNotice(QDialog *parent)
{
_ui->setupUi(this);
QString notice = tr("<p>Copyright 2017-2019 Nextcloud GmbH<br />"
QString notice = tr("<p>Copyright 2017-2020 Nextcloud GmbH<br />"
"Copyright 2012-2018 ownCloud GmbH</p>");
notice += tr("<p>Licensed under the GNU General Public License (GPL) Version 2.0 or any later version.</p>");

View File

@@ -32,6 +32,11 @@ NavigationPaneHelper::NavigationPaneHelper(FolderMan *folderMan)
_updateCloudStorageRegistryTimer.setSingleShot(true);
connect(&_updateCloudStorageRegistryTimer, &QTimer::timeout, this, &NavigationPaneHelper::updateCloudStorageRegistry);
// Ensure that the folder integration stays persistent in Explorer,
// the uninstaller removes the folder upon updating the client.
_showInExplorerNavigationPane = !_showInExplorerNavigationPane;
setShowInExplorerNavigationPane(!_showInExplorerNavigationPane);
}
void NavigationPaneHelper::setShowInExplorerNavigationPane(bool show)
@@ -139,7 +144,9 @@ void NavigationPaneHelper::updateCloudStorageRegistry()
#else
// This code path should only occur on Windows (the config will be false, and the checkbox invisible on other platforms).
// Add runtime checks rather than #ifdefing out the whole code to help catch breakages when developing on other platforms.
Q_ASSERT(false);
// Don't crash, by any means!
// Q_ASSERT(false);
#endif
}
}

View File

@@ -1075,6 +1075,11 @@ void ownCloudGui::slotShowSettings()
raiseDialog(_settingsDialog.data());
}
void ownCloudGui::slotSettingsDialogActivated()
{
emit isShowingSettingsDialog();
}
void ownCloudGui::slotShowSyncProtocol()
{
slotShowSettings();

View File

@@ -70,6 +70,7 @@ public:
signals:
void setupProxy();
void serverError(int code, const QString &message);
void isShowingSettingsDialog();
public slots:
void setupContextMenu();
@@ -93,6 +94,7 @@ public slots:
void slotToggleLogBrowser();
void slotOpenOwnCloud();
void slotOpenSettingsDialog();
void slotSettingsDialogActivated();
void slotHelp();
void slotOpenPath(const QString &path);
void slotAccountStateChanged();

View File

@@ -407,7 +407,8 @@ void OwncloudSetupWizard::slotAuthError()
errorMsg = tr("There was an invalid response to an authenticated webdav request");
}
_ocWizard->show();
// bring wizard to top
_ocWizard->bringToTop();
if (_ocWizard->currentId() == WizardCommon::Page_ShibbolethCreds || _ocWizard->currentId() == WizardCommon::Page_OAuthCreds || _ocWizard->currentId() == WizardCommon::Page_Flow2AuthCreds) {
_ocWizard->back();
}

View File

@@ -1,121 +0,0 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* 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.
*/
#include "quotainfo.h"
#include "account.h"
#include "accountstate.h"
#include "networkjobs.h"
#include "folderman.h"
#include "creds/abstractcredentials.h"
#include <theme.h>
#include <QTimer>
namespace OCC {
namespace {
static const int defaultIntervalT = 30 * 1000;
static const int failIntervalT = 5 * 1000;
}
QuotaInfo::QuotaInfo(AccountState *accountState, QObject *parent)
: QObject(parent)
, _accountState(accountState)
, _lastQuotaTotalBytes(0)
, _lastQuotaUsedBytes(0)
, _active(false)
{
connect(accountState, &AccountState::stateChanged,
this, &QuotaInfo::slotAccountStateChanged);
connect(&_jobRestartTimer, &QTimer::timeout, this, &QuotaInfo::slotCheckQuota);
_jobRestartTimer.setSingleShot(true);
}
void QuotaInfo::setActive(bool active)
{
_active = active;
slotAccountStateChanged();
}
void QuotaInfo::slotAccountStateChanged()
{
if (canGetQuota()) {
auto elapsed = _lastQuotaRecieved.msecsTo(QDateTime::currentDateTime());
if (_lastQuotaRecieved.isNull() || elapsed >= defaultIntervalT) {
slotCheckQuota();
} else {
_jobRestartTimer.start(defaultIntervalT - elapsed);
}
} else {
_jobRestartTimer.stop();
}
}
void QuotaInfo::slotRequestFailed()
{
_lastQuotaTotalBytes = 0;
_lastQuotaUsedBytes = 0;
_jobRestartTimer.start(failIntervalT);
}
bool QuotaInfo::canGetQuota() const
{
if (!_accountState || !_active) {
return false;
}
AccountPtr account = _accountState->account();
return _accountState->isConnected()
&& account->credentials()
&& account->credentials()->ready();
}
QString QuotaInfo::quotaBaseFolder() const
{
return Theme::instance()->quotaBaseFolder();
}
void QuotaInfo::slotCheckQuota()
{
if (!canGetQuota()) {
return;
}
if (_job) {
// The previous job was not finished? Then we cancel it!
_job->deleteLater();
}
AccountPtr account = _accountState->account();
_job = new PropfindJob(account, quotaBaseFolder(), this);
_job->setProperties(QList<QByteArray>() << "quota-available-bytes"
<< "quota-used-bytes");
connect(_job.data(), &PropfindJob::result, this, &QuotaInfo::slotUpdateLastQuota);
connect(_job.data(), &AbstractNetworkJob::networkError, this, &QuotaInfo::slotRequestFailed);
_job->start();
}
void QuotaInfo::slotUpdateLastQuota(const QVariantMap &result)
{
// The server can return fractional bytes (#1374)
// <d:quota-available-bytes>1374532061.2</d:quota-available-bytes>
qint64 avail = result["quota-available-bytes"].toDouble();
_lastQuotaUsedBytes = result["quota-used-bytes"].toDouble();
// negative value of the available quota have special meaning (#3940)
_lastQuotaTotalBytes = avail >= 0 ? _lastQuotaUsedBytes + avail : avail;
emit quotaUpdated(_lastQuotaTotalBytes, _lastQuotaUsedBytes);
_jobRestartTimer.start(defaultIntervalT);
_lastQuotaRecieved = QDateTime::currentDateTime();
}
}

View File

@@ -127,6 +127,8 @@ SettingsDialog::SettingsDialog(ownCloudGui *gui, QWidget *parent)
connect(showLogWindow, &QAction::triggered, gui, &ownCloudGui::slotToggleLogBrowser);
addAction(showLogWindow);
connect(this, &SettingsDialog::onActivate, gui, &ownCloudGui::slotSettingsDialogActivated);
customizeStyle();
cfg.restoreGeometry(this);
@@ -163,6 +165,10 @@ void SettingsDialog::changeEvent(QEvent *e)
// Notify the other widgets (Dark-/Light-Mode switching)
emit styleChanged();
break;
case QEvent::ActivationChange:
if(isActiveWindow())
emit onActivate();
break;
default:
break;
}
@@ -254,9 +260,6 @@ void SettingsDialog::accountAdded(AccountState *s)
_actionGroupWidgets.insert(accountAction, accountSettings);
_actionForAccount.insert(s->account().data(), accountAction);
accountAction->trigger();
// Connect styleChanged event, to adapt (Dark-/Light-Mode switching)
connect(this, &SettingsDialog::styleChanged, accountSettings, &AccountSettings::slotStyleChanged);
connect(accountSettings, &AccountSettings::folderChanged, _gui, &ownCloudGui::slotFoldersChanged);
connect(accountSettings, &AccountSettings::openFolderAlias,
@@ -268,6 +271,10 @@ void SettingsDialog::accountAdded(AccountState *s)
// Refresh immediatly when getting online
connect(s, &AccountState::isConnectedChanged, this, &SettingsDialog::slotRefreshActivityAccountStateSender);
// Connect styleChanged event, to adapt (Dark-/Light-Mode switching)
connect(this, &SettingsDialog::styleChanged, accountSettings, &AccountSettings::slotStyleChanged);
connect(this, &SettingsDialog::styleChanged, _activitySettings[s], &ActivitySettings::slotStyleChanged);
activityAdded(s);
slotRefreshActivity(s);
}

View File

@@ -65,6 +65,7 @@ public slots:
signals:
void styleChanged();
void onActivate();
protected:
void reject() override;

View File

@@ -28,6 +28,7 @@
#include <QFileInfo>
#include <QFileIconProvider>
#include <QInputDialog>
#include <QPointer>
#include <QPushButton>
#include <QFrame>
@@ -137,6 +138,7 @@ ShareDialog::ShareDialog(QPointer<AccountState> accountState,
_manager = new ShareManager(accountState->account(), this);
connect(_manager, &ShareManager::sharesFetched, this, &ShareDialog::slotSharesFetched);
connect(_manager, &ShareManager::linkShareCreated, this, &ShareDialog::slotAddLinkShareWidget);
connect(_manager, &ShareManager::linkShareRequiresPassword, this, &ShareDialog::slotLinkShareRequiresPassword);
}
}
@@ -303,6 +305,24 @@ void ShareDialog::slotCreateLinkShare()
_manager->createLinkShare(_sharePath, QString(), QString());
}
void ShareDialog::slotLinkShareRequiresPassword()
{
bool ok;
QString password = QInputDialog::getText(this,
tr("Password for share required"),
tr("Please enter a password for your link share:"),
QLineEdit::Normal,
QString(),
&ok);
if (!ok) {
// The dialog was canceled so no need to do anything
return;
}
// Try to create the link share again with the newly entered password
_manager->createLinkShare(_sharePath, QString(), password);
}
void ShareDialog::slotDeleteShare()
{

View File

@@ -64,6 +64,7 @@ private slots:
void slotAddLinkShareWidget(const QSharedPointer<LinkShare> &linkShare);
void slotDeleteShare();
void slotCreateLinkShare();
void slotLinkShareRequiresPassword();
void slotAdjustScrollWidgetSize();
signals:

View File

@@ -516,6 +516,8 @@ void ShareLinkWidget::customizeStyle()
// _ui->confirmNote->setIcon(Theme::createColorAwareIcon(":/client/resources/confirm.svg"));
_ui->confirmPassword->setIcon(Theme::createColorAwareIcon(":/client/resources/confirm.svg"));
_ui->confirmExpirationDate->setIcon(Theme::createColorAwareIcon(":/client/resources/confirm.svg"));
_ui->progressIndicator->setColor(QGuiApplication::palette().color(QPalette::Text));
}
}

View File

@@ -33,7 +33,6 @@ namespace Ui {
}
class AbstractCredentials;
class QuotaInfo;
class SyncResult;
class LinkShare;
class Share;

View File

@@ -377,6 +377,12 @@ void ShareUserGroupWidget::slotStyleChanged()
void ShareUserGroupWidget::customizeStyle()
{
_ui->confirmShare->setIcon(Theme::createColorAwareIcon(":/client/resources/confirm.svg"));
_pi_sharee.setColor(QGuiApplication::palette().color(QPalette::Text));
foreach (auto pi, _parentScrollArea->findChildren<QProgressIndicator *>()) {
pi->setColor(QGuiApplication::palette().color(QPalette::Text));;
}
}
ShareUserLine::ShareUserLine(QSharedPointer<Share> share,

View File

@@ -38,7 +38,6 @@ namespace Ui {
}
class AbstractCredentials;
class QuotaInfo;
class SyncResult;
class Share;
class Sharee;

View File

@@ -56,7 +56,7 @@
<item>
<widget class="QLineEdit" name="shareeLineEdit">
<property name="placeholderText">
<string>Share with users or groups ...</string>
<string>Share with users or groups </string>
</property>
</widget>
</item>

View File

@@ -63,7 +63,7 @@
</sizepolicy>
</property>
<property name="text">
<string>User name</string>
<string>Username</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>

View File

@@ -49,6 +49,7 @@
#include <QLocalSocket>
#include <QStringBuilder>
#include <QMessageBox>
#include <QInputDialog>
#include <QClipboard>
@@ -477,6 +478,8 @@ public:
this, &GetOrCreatePublicLinkShare::linkShareCreated);
connect(&_shareManager, &ShareManager::serverError,
this, &GetOrCreatePublicLinkShare::serverError);
connect(&_shareManager, &ShareManager::linkShareRequiresPassword,
this, &GetOrCreatePublicLinkShare::passwordRequired);
}
void run()
@@ -512,6 +515,24 @@ private slots:
success(share->getLink().toString());
}
void passwordRequired() {
bool ok;
QString password = QInputDialog::getText(nullptr,
tr("Password for share required"),
tr("Please enter a password for your link share:"),
QLineEdit::Normal,
QString(),
&ok);
if (!ok) {
// The dialog was canceled so no need to do anything
return;
}
// Try to create the link share again with the newly entered password
_shareManager.createLinkShare(_localFile, QString(), password);
}
void serverError(int code, const QString &message)
{
qCWarning(lcPublicLink) << "Share fetch/create error" << code << message;

156
src/gui/userinfo.cpp Normal file
View File

@@ -0,0 +1,156 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
* Copyright (C) by Michael Schuster <michael@nextcloud.com>
*
* 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.
*/
#include "userinfo.h"
#include "account.h"
#include "accountstate.h"
#include "networkjobs.h"
#include "folderman.h"
#include "creds/abstractcredentials.h"
#include <theme.h>
#include <QTimer>
#include <QJsonDocument>
#include <QJsonObject>
namespace OCC {
namespace {
static const int defaultIntervalT = 30 * 1000;
static const int failIntervalT = 5 * 1000;
}
UserInfo::UserInfo(AccountState *accountState, bool allowDisconnectedAccountState, bool fetchAvatarImage, QObject *parent)
: QObject(parent)
, _accountState(accountState)
, _allowDisconnectedAccountState(allowDisconnectedAccountState)
, _fetchAvatarImage(fetchAvatarImage)
, _lastQuotaTotalBytes(0)
, _lastQuotaUsedBytes(0)
, _active(false)
{
connect(accountState, &AccountState::stateChanged,
this, &UserInfo::slotAccountStateChanged);
connect(&_jobRestartTimer, &QTimer::timeout, this, &UserInfo::slotFetchInfo);
_jobRestartTimer.setSingleShot(true);
}
void UserInfo::setActive(bool active)
{
_active = active;
slotAccountStateChanged();
}
void UserInfo::slotAccountStateChanged()
{
if (canGetInfo()) {
auto elapsed = _lastInfoReceived.msecsTo(QDateTime::currentDateTime());
if (_lastInfoReceived.isNull() || elapsed >= defaultIntervalT) {
slotFetchInfo();
} else {
_jobRestartTimer.start(defaultIntervalT - elapsed);
}
} else {
_jobRestartTimer.stop();
}
}
void UserInfo::slotRequestFailed()
{
_lastQuotaTotalBytes = 0;
_lastQuotaUsedBytes = 0;
_jobRestartTimer.start(failIntervalT);
}
bool UserInfo::canGetInfo() const
{
if (!_accountState || !_active) {
return false;
}
AccountPtr account = _accountState->account();
return (_accountState->isConnected() || _allowDisconnectedAccountState)
&& account->credentials()
&& account->credentials()->ready();
}
void UserInfo::slotFetchInfo()
{
if (!canGetInfo()) {
return;
}
if (_job) {
// The previous job was not finished? Then we cancel it!
_job->deleteLater();
}
AccountPtr account = _accountState->account();
_job = new JsonApiJob(account, QLatin1String("ocs/v1.php/cloud/user"), this);
_job->setTimeout(20 * 1000);
connect(_job.data(), &JsonApiJob::jsonReceived, this, &UserInfo::slotUpdateLastInfo);
connect(_job.data(), &AbstractNetworkJob::networkError, this, &UserInfo::slotRequestFailed);
_job->start();
}
void UserInfo::slotUpdateLastInfo(const QJsonDocument &json)
{
auto objData = json.object().value("ocs").toObject().value("data").toObject();
AccountPtr account = _accountState->account();
// User Info
QString user = objData.value("id").toString();
if (!user.isEmpty()) {
account->setDavUser(user);
}
QString displayName = objData.value("display-name").toString();
if (!displayName.isEmpty()) {
account->setDavDisplayName(displayName);
}
// Quota
auto objQuota = objData.value("quota").toObject();
qint64 used = objQuota.value("used").toDouble();
qint64 total = objQuota.value("total").toDouble();
if(_lastInfoReceived.isNull() || _lastQuotaUsedBytes != used || _lastQuotaTotalBytes != total) {
_lastQuotaUsedBytes = used;
_lastQuotaTotalBytes = total;
emit quotaUpdated(_lastQuotaTotalBytes, _lastQuotaUsedBytes);
}
_jobRestartTimer.start(defaultIntervalT);
_lastInfoReceived = QDateTime::currentDateTime();
// Avatar Image
if(_fetchAvatarImage) {
AvatarJob *job = new AvatarJob(account, account->davUser(), 128, this);
job->setTimeout(20 * 1000);
QObject::connect(job, &AvatarJob::avatarPixmap, this, &UserInfo::slotAvatarImage);
job->start();
}
else
emit fetchedLastInfo(this);
}
void UserInfo::slotAvatarImage(const QImage &img)
{
_accountState->account()->setAvatar(img);
emit fetchedLastInfo(this);
}
} // namespace OCC

View File

@@ -1,5 +1,6 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
* Copyright (C) by Michael Schuster <michael@nextcloud.com>
*
* 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
@@ -12,8 +13,8 @@
* for more details.
*/
#ifndef QUOTAINFO_H
#define QUOTAINFO_H
#ifndef USERINFO_H
#define USERINFO_H
#include <QObject>
#include <QPointer>
@@ -23,31 +24,51 @@
namespace OCC {
class AccountState;
class PropfindJob;
class JsonApiJob;
/**
* @brief handles getting the quota to display in the UI
* @brief handles getting the user info and quota to display in the UI
*
* It is typically owned by the AccountSetting page.
*
* The quota is requested if these 3 conditions are met:
* The user info and quota is requested if these 3 conditions are met:
* - This object is active via setActive() (typically if the settings page is visible.)
* - The account is connected.
* - Every 30 seconds (defaultIntervalT) or 5 seconds in case of failure (failIntervalT)
*
* We only request the quota when the UI is visible otherwise this might slow down the server with
* We only request the info when the UI is visible otherwise this might slow down the server with
* too many requests. But we still need to do it every 30 seconds otherwise user complains that the
* quota is not updated fast enough when changed on the server.
*
* If the quota job is not finished within 30 seconds, it is cancelled and another one is started
* If the fetch job is not finished within 30 seconds, it is cancelled and another one is started
*
* Constructor notes:
* - allowDisconnectedAccountState: set to true if you want to ignore AccountState's isConnected() state,
* this is used by ConnectionValidator (prior having a valid AccountState).
* - fetchAvatarImage: set to false if you don't want to fetch the avatar image
*
* @ingroup gui
*/
class QuotaInfo : public QObject
*
* Here follows the state machine
\code{.unparsed}
*---> slotFetchInfo
JsonApiJob (ocs/v1.php/cloud/user)
|
+-> slotUpdateLastInfo
AvatarJob (if _fetchAvatarImage is true)
|
+-> slotAvatarImage -->
+-----------------------------------+
|
+-> Client Side Encryption Checks --+ --reportResult()
\endcode
*/
class UserInfo : public QObject
{
Q_OBJECT
public:
explicit QuotaInfo(OCC::AccountState *accountState, QObject *parent = nullptr);
explicit UserInfo(OCC::AccountState *accountState, bool allowDisconnectedAccountState, bool fetchAvatarImage, QObject *parent = nullptr);
qint64 lastQuotaTotalBytes() const { return _lastQuotaTotalBytes; }
qint64 lastQuotaUsedBytes() const { return _lastQuotaUsedBytes; }
@@ -60,32 +81,34 @@ public:
void setActive(bool active);
public Q_SLOTS:
void slotCheckQuota();
void slotFetchInfo();
private Q_SLOTS:
void slotUpdateLastQuota(const QVariantMap &);
void slotUpdateLastInfo(const QJsonDocument &json);
void slotAccountStateChanged();
void slotRequestFailed();
void slotAvatarImage(const QImage &img);
Q_SIGNALS:
void quotaUpdated(qint64 total, qint64 used);
void fetchedLastInfo(UserInfo *userInfo);
private:
bool canGetQuota() const;
/// Returns the folder that quota shall be retrieved for
QString quotaBaseFolder() const;
bool canGetInfo() const;
QPointer<AccountState> _accountState;
bool _allowDisconnectedAccountState;
bool _fetchAvatarImage;
qint64 _lastQuotaTotalBytes;
qint64 _lastQuotaUsedBytes;
QTimer _jobRestartTimer;
QDateTime _lastQuotaRecieved; // the time at which the quota was received last
QDateTime _lastInfoReceived; // the time at which the user info and quota was received last
bool _active; // if we should check at regular interval (when the UI is visible)
QPointer<PropfindJob> _job; // the currently running job
QPointer<JsonApiJob> _job; // the currently running job
};
} // namespace OCC
#endif //QUOTAINFO_H
#endif //USERINFO_H

View File

@@ -14,40 +14,39 @@
*/
#include <QVariant>
#include <QMenu>
#include <QClipboard>
#include <QVBoxLayout>
#include "wizard/flow2authcredspage.h"
#include "flow2authcredspage.h"
#include "theme.h"
#include "account.h"
#include "cookiejar.h"
#include "wizard/owncloudwizardcommon.h"
#include "wizard/owncloudwizard.h"
#include "wizard/flow2authwidget.h"
#include "creds/credentialsfactory.h"
#include "creds/webflowcredentials.h"
namespace OCC {
Flow2AuthCredsPage::Flow2AuthCredsPage()
: AbstractCredentialsWizardPage()
: AbstractCredentialsWizardPage(),
_flow2AuthWidget(nullptr)
{
_ui.setupUi(this);
Theme *theme = Theme::instance();
_ui.topLabel->hide();
_ui.bottomLabel->hide();
QVariant variant = theme->customMedia(Theme::oCSetupTop);
WizardCommon::setupCustomMedia(variant, _ui.topLabel);
variant = theme->customMedia(Theme::oCSetupBottom);
WizardCommon::setupCustomMedia(variant, _ui.bottomLabel);
WizardCommon::initErrorLabel(_ui.errorLabel);
_layout = new QVBoxLayout(this);
setTitle(WizardCommon::titleTemplate().arg(tr("Connect to %1").arg(Theme::instance()->appNameGUI())));
setSubTitle(WizardCommon::subTitleTemplate().arg(tr("Login in your browser (Login Flow v2)")));
connect(_ui.openLinkButton, &QCommandLinkButton::clicked, this, &Flow2AuthCredsPage::slotOpenBrowser);
connect(_ui.copyLinkButton, &QCommandLinkButton::clicked, this, &Flow2AuthCredsPage::slotCopyLinkToClipboard);
_flow2AuthWidget = new Flow2AuthWidget();
_layout->addWidget(_flow2AuthWidget);
connect(_flow2AuthWidget, &Flow2AuthWidget::authResult, this, &Flow2AuthCredsPage::slotFlow2AuthResult);
// Connect styleChanged events to our widgets, so they can adapt (Dark-/Light-Mode switching)
connect(this, &Flow2AuthCredsPage::styleChanged, _flow2AuthWidget, &Flow2AuthWidget::slotStyleChanged);
// allow Flow2 page to poll on window activation
connect(this, &Flow2AuthCredsPage::pollNow, _flow2AuthWidget, &Flow2AuthWidget::slotPollNow);
}
void Flow2AuthCredsPage::initializePage()
@@ -55,9 +54,9 @@ void Flow2AuthCredsPage::initializePage()
OwncloudWizard *ocWizard = qobject_cast<OwncloudWizard *>(wizard());
Q_ASSERT(ocWizard);
ocWizard->account()->setCredentials(CredentialsFactory::create("http"));
_asyncAuth.reset(new Flow2Auth(ocWizard->account().data(), this));
connect(_asyncAuth.data(), &Flow2Auth::result, this, &Flow2AuthCredsPage::asyncAuthResult, Qt::QueuedConnection);
_asyncAuth->start();
if(_flow2AuthWidget)
_flow2AuthWidget->startAuth(ocWizard->account().data());
// Don't hide the wizard (avoid user confusion)!
//wizard()->hide();
@@ -67,21 +66,20 @@ void OCC::Flow2AuthCredsPage::cleanupPage()
{
// The next or back button was activated, show the wizard again
wizard()->show();
_asyncAuth.reset();
if(_flow2AuthWidget)
_flow2AuthWidget->resetAuth();
// Forget sensitive data
_appPassword.clear();
_user.clear();
}
void Flow2AuthCredsPage::asyncAuthResult(Flow2Auth::Result r, const QString &user,
const QString &appPassword)
void Flow2AuthCredsPage::slotFlow2AuthResult(Flow2Auth::Result r, const QString &errorString, const QString &user, const QString &appPassword)
{
switch (r) {
case Flow2Auth::NotSupported: {
/* Flow2Auth not supported (can't open browser) */
_ui.errorLabel->setText(tr("Unable to open the Browser, please copy the link to your Browser."));
_ui.errorLabel->show();
wizard()->show();
/* Don't fallback to HTTP credentials */
/*OwncloudWizard *ocWizard = qobject_cast<OwncloudWizard *>(wizard());
@@ -91,7 +89,6 @@ void Flow2AuthCredsPage::asyncAuthResult(Flow2Auth::Result r, const QString &use
}
case Flow2Auth::Error:
/* Error while getting the access token. (Timeout, or the server did not accept our client credentials */
_ui.errorLabel->show();
wizard()->show();
break;
case Flow2Auth::LoggedIn: {
@@ -113,7 +110,11 @@ int Flow2AuthCredsPage::nextId() const
void Flow2AuthCredsPage::setConnected()
{
wizard()->show();
OwncloudWizard *ocWizard = qobject_cast<OwncloudWizard *>(wizard());
Q_ASSERT(ocWizard);
// bring wizard to top
ocWizard->bringToTop();
}
AbstractCredentials *Flow2AuthCredsPage::getCredentials() const
@@ -134,19 +135,14 @@ bool Flow2AuthCredsPage::isComplete() const
return false; /* We can never go forward manually */
}
void Flow2AuthCredsPage::slotOpenBrowser()
void Flow2AuthCredsPage::slotPollNow()
{
if (_ui.errorLabel)
_ui.errorLabel->hide();
if (_asyncAuth)
_asyncAuth->openBrowser();
emit pollNow();
}
void Flow2AuthCredsPage::slotCopyLinkToClipboard()
void Flow2AuthCredsPage::slotStyleChanged()
{
if (_asyncAuth)
QApplication::clipboard()->setText(_asyncAuth->authorisationLink().toString(QUrl::FullyEncoded));
emit styleChanged();
}
} // namespace OCC

View File

@@ -25,11 +25,12 @@
#include "accountfwd.h"
#include "creds/flow2auth.h"
#include "ui_flow2authcredspage.h"
class QVBoxLayout;
class QProgressIndicator;
namespace OCC {
class Flow2AuthWidget;
class Flow2AuthCredsPage : public AbstractCredentialsWizardPage
{
@@ -46,20 +47,22 @@ public:
bool isComplete() const override;
public Q_SLOTS:
void asyncAuthResult(Flow2Auth::Result, const QString &user, const QString &appPassword);
void slotFlow2AuthResult(Flow2Auth::Result, const QString &errorString, const QString &user, const QString &appPassword);
void slotPollNow();
void slotStyleChanged();
signals:
void connectToOCUrl(const QString &);
void pollNow();
void styleChanged();
public:
QString _user;
QString _appPassword;
QScopedPointer<Flow2Auth> _asyncAuth;
Ui_Flow2AuthCredsPage _ui;
protected slots:
void slotOpenBrowser();
void slotCopyLinkToClipboard();
private:
Flow2AuthWidget *_flow2AuthWidget;
QVBoxLayout *_layout;
};
} // namespace OCC

View File

@@ -1,101 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Flow2AuthCredsPage</class>
<widget class="QWidget" name="Flow2AuthCredsPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>424</width>
<height>373</height>
</rect>
</property>
<property name="windowTitle">
<string>Browser Authentication</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="topLabel">
<property name="text">
<string notr="true">TextLabel</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Please switch to your browser to proceed.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="errorLabel">
<property name="text">
<string>An error occurred while connecting. Please try again.</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
</widget>
</item>
<item>
<widget class="QCommandLinkButton" name="openLinkButton">
<property name="text">
<string>Re-open Browser</string>
</property>
</widget>
</item>
<item>
<widget class="QCommandLinkButton" name="copyLinkButton">
<property name="font">
<font>
<family>Segoe UI</family>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Copy link</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>127</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="bottomLabel">
<property name="text">
<string notr="true">TextLabel</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -14,53 +14,66 @@
#include "flow2authwidget.h"
#include <QDesktopServices>
#include <QProgressBar>
#include <QLoggingCategory>
#include <QLocale>
#include <QMessageBox>
#include <QMenu>
#include <QClipboard>
#include "common/utility.h"
#include "account.h"
#include "theme.h"
#include "wizard/owncloudwizardcommon.h"
#include "QProgressIndicator.h"
namespace OCC {
Q_LOGGING_CATEGORY(lcFlow2AuthWidget, "gui.wizard.flow2authwidget", QtInfoMsg)
Flow2AuthWidget::Flow2AuthWidget(Account *account, QWidget *parent)
: QWidget(parent),
_account(account),
_ui()
Flow2AuthWidget::Flow2AuthWidget(QWidget *parent)
: QWidget(parent)
, _account(nullptr)
, _ui()
, _progressIndi(new QProgressIndicator(this))
, _statusUpdateSkipCount(0)
{
_ui.setupUi(this);
Theme *theme = Theme::instance();
_ui.topLabel->hide();
_ui.bottomLabel->hide();
QVariant variant = theme->customMedia(Theme::oCSetupTop);
WizardCommon::setupCustomMedia(variant, _ui.topLabel);
variant = theme->customMedia(Theme::oCSetupBottom);
WizardCommon::setupCustomMedia(variant, _ui.bottomLabel);
WizardCommon::initErrorLabel(_ui.errorLabel);
_ui.errorLabel->setTextFormat(Qt::RichText);
connect(_ui.openLinkButton, &QCommandLinkButton::clicked, this, &Flow2AuthWidget::slotOpenBrowser);
connect(_ui.copyLinkButton, &QCommandLinkButton::clicked, this, &Flow2AuthWidget::slotCopyLinkToClipboard);
_asyncAuth.reset(new Flow2Auth(_account, this));
connect(_asyncAuth.data(), &Flow2Auth::result, this, &Flow2AuthWidget::asyncAuthResult, Qt::QueuedConnection);
_asyncAuth->start();
_ui.horizontalLayout->addWidget(_progressIndi);
stopSpinner(false);
customizeStyle();
}
void Flow2AuthWidget::asyncAuthResult(Flow2Auth::Result r, const QString &user,
const QString &appPassword)
void Flow2AuthWidget::startAuth(Account *account)
{
Flow2Auth *oldAuth = _asyncAuth.take();
if(oldAuth)
oldAuth->deleteLater();
_statusUpdateSkipCount = 0;
if(account) {
_account = account;
_asyncAuth.reset(new Flow2Auth(_account, this));
connect(_asyncAuth.data(), &Flow2Auth::result, this, &Flow2AuthWidget::slotAuthResult, Qt::QueuedConnection);
connect(_asyncAuth.data(), &Flow2Auth::statusChanged, this, &Flow2AuthWidget::slotStatusChanged);
connect(this, &Flow2AuthWidget::pollNow, _asyncAuth.data(), &Flow2Auth::slotPollNow);
_asyncAuth->start();
}
}
void Flow2AuthWidget::resetAuth(Account *account)
{
startAuth(account);
}
void Flow2AuthWidget::slotAuthResult(Flow2Auth::Result r, const QString &errorString, const QString &user, const QString &appPassword)
{
stopSpinner(false);
switch (r) {
case Flow2Auth::NotSupported:
/* Flow2Auth can't open browser */
@@ -69,15 +82,16 @@ void Flow2AuthWidget::asyncAuthResult(Flow2Auth::Result r, const QString &user,
break;
case Flow2Auth::Error:
/* Error while getting the access token. (Timeout, or the server did not accept our client credentials */
_ui.errorLabel->setText(errorString);
_ui.errorLabel->show();
break;
case Flow2Auth::LoggedIn: {
_user = user;
_appPassword = appPassword;
emit urlCatched(_user, _appPassword, QString());
_ui.errorLabel->hide();
break;
}
}
emit authResult(r, errorString, user, appPassword);
}
void Flow2AuthWidget::setError(const QString &error) {
@@ -90,11 +104,8 @@ void Flow2AuthWidget::setError(const QString &error) {
}
Flow2AuthWidget::~Flow2AuthWidget() {
_asyncAuth.reset();
// Forget sensitive data
_appPassword.clear();
_user.clear();
_asyncAuth.reset();
}
void Flow2AuthWidget::slotOpenBrowser()
@@ -108,8 +119,79 @@ void Flow2AuthWidget::slotOpenBrowser()
void Flow2AuthWidget::slotCopyLinkToClipboard()
{
if (_ui.errorLabel)
_ui.errorLabel->hide();
if (_asyncAuth)
QApplication::clipboard()->setText(_asyncAuth->authorisationLink().toString(QUrl::FullyEncoded));
_asyncAuth->copyLinkToClipboard();
}
void Flow2AuthWidget::slotPollNow()
{
emit pollNow();
}
void Flow2AuthWidget::slotStatusChanged(Flow2Auth::PollStatus status, int secondsLeft)
{
switch(status)
{
case Flow2Auth::statusPollCountdown:
if(_statusUpdateSkipCount > 0) {
_statusUpdateSkipCount--;
break;
}
_ui.statusLabel->setText(tr("Waiting for authorization") + QString("… (%1)").arg(secondsLeft));
stopSpinner(true);
break;
case Flow2Auth::statusPollNow:
_statusUpdateSkipCount = 0;
_ui.statusLabel->setText(tr("Polling for authorization") + "");
startSpinner();
break;
case Flow2Auth::statusFetchToken:
_statusUpdateSkipCount = 0;
_ui.statusLabel->setText(tr("Starting authorization") + "");
startSpinner();
break;
case Flow2Auth::statusCopyLinkToClipboard:
_ui.statusLabel->setText(tr("Link copied to clipboard."));
_statusUpdateSkipCount = 3;
stopSpinner(true);
break;
}
}
void Flow2AuthWidget::startSpinner()
{
_ui.horizontalLayout->setEnabled(true);
_ui.statusLabel->setVisible(true);
_progressIndi->setVisible(true);
_progressIndi->startAnimation();
_ui.openLinkButton->setEnabled(false);
_ui.copyLinkButton->setEnabled(false);
}
void Flow2AuthWidget::stopSpinner(bool showStatusLabel)
{
_ui.horizontalLayout->setEnabled(false);
_ui.statusLabel->setVisible(showStatusLabel);
_progressIndi->setVisible(false);
_progressIndi->stopAnimation();
_ui.openLinkButton->setEnabled(_statusUpdateSkipCount == 0);
_ui.copyLinkButton->setEnabled(_statusUpdateSkipCount == 0);
}
void Flow2AuthWidget::slotStyleChanged()
{
customizeStyle();
}
void Flow2AuthWidget::customizeStyle()
{
if(_progressIndi)
_progressIndi->setColor(QGuiApplication::palette().color(QPalette::Text));
}
} // namespace OCC

View File

@@ -22,35 +22,49 @@
#include "ui_flow2authwidget.h"
class QProgressIndicator;
namespace OCC {
class Flow2AuthWidget : public QWidget
{
Q_OBJECT
public:
Flow2AuthWidget(Account *account, QWidget *parent = nullptr);
Flow2AuthWidget(QWidget *parent = nullptr);
virtual ~Flow2AuthWidget();
void startAuth(Account *account);
void resetAuth(Account *account = nullptr);
void setError(const QString &error);
public Q_SLOTS:
void asyncAuthResult(Flow2Auth::Result, const QString &user, const QString &appPassword);
void slotAuthResult(Flow2Auth::Result, const QString &errorString, const QString &user, const QString &appPassword);
void slotPollNow();
void slotStatusChanged(Flow2Auth::PollStatus status, int secondsLeft);
void slotStyleChanged();
signals:
void urlCatched(const QString user, const QString pass, const QString host);
void authResult(Flow2Auth::Result, const QString &errorString, const QString &user, const QString &appPassword);
void pollNow();
private:
Account *_account;
QString _user;
QString _appPassword;
QScopedPointer<Flow2Auth> _asyncAuth;
Ui_Flow2AuthWidget _ui;
protected slots:
void slotOpenBrowser();
void slotCopyLinkToClipboard();
private:
void startSpinner();
void stopSpinner(bool showStatusLabel);
void customizeStyle();
QProgressIndicator *_progressIndi;
int _statusUpdateSkipCount;
};
}
} // namespace OCC
#endif // FLOW2AUTHWIDGET_H

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>500</width>
<height>280</height>
<width>580</width>
<height>330</height>
</rect>
</property>
<property name="sizePolicy">
@@ -26,22 +26,6 @@
<string>Browser Authentication</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="topLabel">
<property name="text">
<string notr="true">TextLabel</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
@@ -73,7 +57,6 @@
<widget class="QCommandLinkButton" name="copyLinkButton">
<property name="font">
<font>
<family>Segoe UI</family>
<weight>50</weight>
<bold>false</bold>
</font>
@@ -83,6 +66,32 @@
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="statusLabel">
<property name="text">
<string notr="true">TextLabel</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout"/>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
@@ -91,21 +100,11 @@
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>127</height>
<height>107</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="bottomLabel">
<property name="text">
<string notr="true">TextLabel</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>

View File

@@ -389,4 +389,15 @@ QString OwncloudAdvancedSetupPage::checkLocalSpace(qint64 remoteSize) const
return (availableLocalSpace()>remoteSize) ? QString() : tr("There isn't enough free space in the local folder!");
}
void OwncloudAdvancedSetupPage::slotStyleChanged()
{
customizeStyle();
}
void OwncloudAdvancedSetupPage::customizeStyle()
{
if(_progressIndi)
_progressIndi->setColor(QGuiApplication::palette().color(QPalette::Text));
}
} // namespace OCC

View File

@@ -51,6 +51,7 @@ signals:
public slots:
void setErrorString(const QString &);
void slotStyleChanged();
private slots:
void slotSelectFolder();
@@ -67,6 +68,7 @@ private:
QUrl serverUrl() const;
qint64 availableLocalSpace() const;
QString checkLocalSpace(qint64 remoteSize) const;
void customizeStyle();
Ui_OwncloudAdvancedSetupPage _ui;
bool _checking;

View File

@@ -194,5 +194,15 @@ AbstractCredentials *OwncloudHttpCredsPage::getCredentials() const
return new HttpCredentialsGui(_ui.leUsername->text(), _ui.lePassword->text(), _ocWizard->_clientSslCertificate, _ocWizard->_clientSslKey);
}
void OwncloudHttpCredsPage::slotStyleChanged()
{
customizeStyle();
}
void OwncloudHttpCredsPage::customizeStyle()
{
if(_progressIndi)
_progressIndi->setColor(QGuiApplication::palette().color(QPalette::Text));
}
} // namespace OCC

View File

@@ -46,10 +46,14 @@ public:
Q_SIGNALS:
void connectToOCUrl(const QString &);
public slots:
void slotStyleChanged();
private:
void startSpinner();
void stopSpinner();
void setupCustomization();
void customizeStyle();
Ui_OwncloudHttpCredsPage _ui;
bool _connected;

View File

@@ -88,29 +88,15 @@ OwncloudSetupPage::OwncloudSetupPage(QWidget *parent)
connect(_ui.nextButton, &QPushButton::clicked, _ui.slideShow, &SlideShow::nextSlide);
connect(_ui.prevButton, &QPushButton::clicked, _ui.slideShow, &SlideShow::prevSlide);
auto widgetBgLightness = OwncloudSetupPage::palette().color(OwncloudSetupPage::backgroundRole()).lightness();
bool widgetHasDarkBg =
(widgetBgLightness >= 125)
? false
: true;
_ui.nextButton->setIcon(theme->uiThemeIcon(QString("control-next.svg"), widgetHasDarkBg));
_ui.prevButton->setIcon(theme->uiThemeIcon(QString("control-prev.svg"), widgetHasDarkBg));
// QPushButtons are a mess when it comes to consistent background coloring without stylesheets,
// so we do it here even though this is an exceptional styling method here
_ui.createAccountButton->setStyleSheet("QPushButton {background-color: #0082C9; color: white}");
_ui.slideShow->startShow();
QPalette pal = _ui.slideShow->palette();
pal.setColor(QPalette::WindowText, theme->wizardHeaderBackgroundColor());
_ui.slideShow->setPalette(pal);
#else
_ui.createAccountButton->hide();
_ui.loginButton->hide();
_ui.installLink->hide();
_ui.slideShow->hide();
#endif
customizeStyle();
}
void OwncloudSetupPage::setServerUrl(const QString &newUrl)
@@ -192,11 +178,11 @@ void OwncloudSetupPage::slotUrlChanged(const QString &url)
if (!url.startsWith(QLatin1String("https://"))) {
_ui.urlLabel->setPixmap(QPixmap(Theme::hidpiFileName(":/client/resources/lock-http.png")));
_ui.urlLabel->setToolTip(tr("This url is NOT secure as it is not encrypted.\n"
_ui.urlLabel->setToolTip(tr("This URL is NOT secure as it is not encrypted.\n"
"It is not advisable to use it."));
} else {
_ui.urlLabel->setPixmap(QPixmap(Theme::hidpiFileName(":/client/resources/lock-https.png")));
_ui.urlLabel->setToolTip(tr("This url is secure. You can use it."));
_ui.urlLabel->setToolTip(tr("This URL is secure. You can use it."));
}
}
@@ -433,4 +419,31 @@ OwncloudSetupPage::~OwncloudSetupPage()
{
}
void OwncloudSetupPage::slotStyleChanged()
{
customizeStyle();
}
void OwncloudSetupPage::customizeStyle()
{
#ifdef WITH_PROVIDERS
Theme *theme = Theme::instance();
bool widgetHasDarkBg = Theme::isDarkColor(QGuiApplication::palette().base().color());
_ui.nextButton->setIcon(theme->uiThemeIcon(QString("control-next.svg"), widgetHasDarkBg));
_ui.prevButton->setIcon(theme->uiThemeIcon(QString("control-prev.svg"), widgetHasDarkBg));
// QPushButtons are a mess when it comes to consistent background coloring without stylesheets,
// so we do it here even though this is an exceptional styling method here
_ui.createAccountButton->setStyleSheet("QPushButton {background-color: #0082C9; color: white}");
QPalette pal = _ui.slideShow->palette();
pal.setColor(QPalette::WindowText, theme->wizardHeaderBackgroundColor());
_ui.slideShow->setPalette(pal);
#endif
if(_progressIndi)
_progressIndi->setColor(QGuiApplication::palette().color(QPalette::Text));
}
} // namespace OCC

View File

@@ -62,6 +62,7 @@ public slots:
void startSpinner();
void stopSpinner();
void slotCertificateAccepted();
void slotStyleChanged();
protected slots:
void slotUrlChanged(const QString &);
@@ -78,6 +79,7 @@ signals:
private:
bool urlHasChanged();
void customizeStyle();
Ui_OwncloudSetupPage _ui;

View File

@@ -16,6 +16,7 @@
#include "account.h"
#include "configfile.h"
#include "theme.h"
#include "owncloudgui.h"
#include "wizard/owncloudwizard.h"
#include "wizard/owncloudsetuppage.h"
@@ -100,6 +101,17 @@ OwncloudWizard::OwncloudWizard(QWidget *parent)
setTitleFormat(Qt::RichText);
setSubTitleFormat(Qt::RichText);
setButtonText(QWizard::CustomButton1, tr("Skip folders configuration"));
// Connect styleChanged events to our widgets, so they can adapt (Dark-/Light-Mode switching)
connect(this, &OwncloudWizard::styleChanged, _setupPage, &OwncloudSetupPage::slotStyleChanged);
connect(this, &OwncloudWizard::styleChanged, _advancedSetupPage, &OwncloudAdvancedSetupPage::slotStyleChanged);
connect(this, &OwncloudWizard::styleChanged, _flow2CredsPage, &Flow2AuthCredsPage::slotStyleChanged);
customizeStyle();
// allow Flow2 page to poll on window activation
connect(this, &OwncloudWizard::onActivate, _flow2CredsPage, &Flow2AuthCredsPage::slotPollNow);
}
void OwncloudWizard::setAccount(AccountPtr account)
@@ -277,4 +289,37 @@ AbstractCredentials *OwncloudWizard::getCredentials() const
return nullptr;
}
void OwncloudWizard::changeEvent(QEvent *e)
{
switch (e->type()) {
case QEvent::StyleChange:
case QEvent::PaletteChange:
case QEvent::ThemeChange:
customizeStyle();
// Notify the other widgets (Dark-/Light-Mode switching)
emit styleChanged();
break;
case QEvent::ActivationChange:
if(isActiveWindow())
emit onActivate();
break;
default:
break;
}
QWizard::changeEvent(e);
}
void OwncloudWizard::customizeStyle()
{
// HINT: Customize wizard's own style here, if necessary in the future (Dark-/Light-Mode switching)
}
void OwncloudWizard::bringToTop()
{
// bring wizard to top
ownCloudGui::raiseDialog(this);
}
} // end namespace

View File

@@ -74,6 +74,8 @@ public:
void displayError(const QString &, bool retryHTTPonly);
AbstractCredentials *getCredentials() const;
void bringToTop();
// FIXME: Can those be local variables?
// Set from the OwncloudSetupPage, later used from OwncloudHttpCredsPage
QSslKey _clientSslKey;
@@ -96,8 +98,15 @@ signals:
void basicSetupFinished(int);
void skipFolderConfiguration();
void needCertificate();
void styleChanged();
void onActivate();
protected:
void changeEvent(QEvent *) override;
private:
void customizeStyle();
AccountPtr _account;
OwncloudSetupPage *_setupPage;
OwncloudHttpCredsPage *_httpCredsPage;

View File

@@ -52,7 +52,6 @@ namespace OCC {
class AbstractCredentials;
class Account;
typedef QSharedPointer<Account> AccountPtr;
class QuotaInfo;
class AccessManager;
class SimpleNetworkJob;
@@ -306,7 +305,6 @@ private:
Capabilities _capabilities;
QString _serverVersion;
QScopedPointer<AbstractSslErrorHandler> _sslErrorHandler;
QuotaInfo *_quotaInfo;
QSharedPointer<QNetworkAccessManager> _am;
QScopedPointer<AbstractCredentials> _credentials;
bool _http2Supported = false;

View File

@@ -384,7 +384,14 @@ void DiscoverySingleDirectoryJob::directoryListingIteratedSlot(QString file, con
propertyMapToFileStat(map, file_stat.get());
if (file_stat->type == ItemTypeDirectory)
file_stat->size = 0;
if (file_stat->type == ItemTypeSkip
if (file_stat->remotePerm.hasPermission(RemotePermissions::IsShared) && file_stat->etag.isEmpty()) {
/* Handle broken shared file error gracefully instead of stopping sync in the desktop client.
DO not set _error */
qCWarning(lcDiscovery)
<< "Missing path to a share :" << file << file_stat->path << file_stat->type << file_stat->size
<< file_stat->modtime << file_stat->remotePerm.toString()
<< file_stat->etag << file_stat->file_id;
} else if (file_stat->type == ItemTypeSkip
|| file_stat->size == -1
|| file_stat->remotePerm.isNull()
|| file_stat->etag.isEmpty()

View File

@@ -134,10 +134,6 @@ void GETFileJob::start()
_bandwidthManager->registerDownloadJob(this);
}
if (reply()->error() != QNetworkReply::NoError) {
qCWarning(lcGetJob) << " Network error: " << errorString();
}
connect(this, &AbstractNetworkJob::networkActivity, account().data(), &Account::propagatorNetworkActivity);
AbstractNetworkJob::start();
@@ -168,7 +164,6 @@ void GETFileJob::slotMetaDataChanged()
// If the status code isn't 2xx, don't write the reply body to the file.
// For any error: handle it when the job is finished, not here.
if (httpStatus / 100 != 2) {
_device->close();
return;
}
if (reply()->error() != QNetworkReply::NoError) {
@@ -295,15 +290,13 @@ void GETFileJob::slotReadyRead()
return;
}
if (_device->isOpen() && _saveBodyToFile) {
qint64 w = _device->write(buffer.constData(), r);
if (w != r) {
_errorString = _device->errorString();
_errorStatus = SyncFileItem::NormalError;
qCWarning(lcGetJob) << "Error while writing to file" << w << r << _errorString;
reply()->abort();
return;
}
qint64 w = _device->write(buffer.constData(), r);
if (w != r) {
_errorString = _device->errorString();
_errorStatus = SyncFileItem::NormalError;
qCWarning(lcGetJob) << "Error while writing to file" << w << r << _errorString;
reply()->abort();
return;
}
}

View File

@@ -574,7 +574,7 @@ bool Theme::isDarkColor(const QColor &color)
QColor Theme::getBackgroundAwareLinkColor(const QColor &backgroundColor)
{
return QColor((isDarkColor(backgroundColor) ? QColor("#aabdff") : QGuiApplication::palette().color(QPalette::Link)));
return QColor((isDarkColor(backgroundColor) ? QColor("#6193dc") : QGuiApplication::palette().color(QPalette::Link)));
}
QColor Theme::getBackgroundAwareLinkColor()
@@ -584,7 +584,7 @@ QColor Theme::getBackgroundAwareLinkColor()
void Theme::replaceLinkColorStringBackgroundAware(QString &linkString, const QColor &backgroundColor)
{
linkString.replace(QRegularExpression("(<a href|<a style='color:#([a-zA-Z0-9]{6});' href)"), QString::fromLatin1("<a style='color:%1;' href").arg(getBackgroundAwareLinkColor(backgroundColor).name()));
replaceLinkColorString(linkString, getBackgroundAwareLinkColor(backgroundColor));
}
void Theme::replaceLinkColorStringBackgroundAware(QString &linkString)
@@ -592,6 +592,11 @@ void Theme::replaceLinkColorStringBackgroundAware(QString &linkString)
replaceLinkColorStringBackgroundAware(linkString, QGuiApplication::palette().color(QPalette::Base));
}
void Theme::replaceLinkColorString(QString &linkString, const QColor &newColor)
{
linkString.replace(QRegularExpression("(<a href|<a style='color:#([a-zA-Z0-9]{6});' href)"), QString::fromLatin1("<a style='color:%1;' href").arg(newColor.name()));
}
QIcon Theme::createColorAwareIcon(const QString &name, const QPalette &palette)
{
QImage img(name);

View File

@@ -402,6 +402,15 @@ public:
*/
static void replaceLinkColorStringBackgroundAware(QString &linkString);
/**
* @brief Appends a CSS-style colour value to all HTML link tags in a given string, as specified by newColor.
*
* 2019/12/19: Implemented for the Dark Mode on macOS, because the app palette can not account for that (Qt 5.12.5).
*
* This way we also avoid having certain strings re-translated on Transifex.
*/
static void replaceLinkColorString(QString &linkString, const QColor &newColor);
/**
* @brief Creates a colour-aware icon based on the specified palette's base colour.
*

View File

@@ -64,6 +64,7 @@ list(APPEND FolderMan_SRC ../src/gui/syncrunfilelog.cpp )
list(APPEND FolderMan_SRC ../src/gui/lockwatcher.cpp )
list(APPEND FolderMan_SRC ../src/gui/guiutility.cpp )
list(APPEND FolderMan_SRC ../src/gui/navigationpanehelper.cpp )
list(APPEND FolderMan_SRC ../src/gui/userinfo.cpp )
list(APPEND FolderMan_SRC ../src/gui/connectionvalidator.cpp )
list(APPEND FolderMan_SRC ../src/gui/clientproxy.cpp )
list(APPEND FolderMan_SRC ../src/gui/accountstate.cpp )
@@ -75,6 +76,7 @@ nextcloud_add_test(FolderMan "${FolderMan_SRC}")
SET(RemoteWipe_SRC ../src/gui/remotewipe.cpp)
list(APPEND RemoteWipe_SRC ../src/gui/clientproxy.cpp )
list(APPEND RemoteWipe_SRC ../src/gui/guiutility.cpp )
list(APPEND RemoteWipe_SRC ../src/gui/userinfo.cpp )
list(APPEND RemoteWipe_SRC ../src/gui/connectionvalidator.cpp )
list(APPEND RemoteWipe_SRC ../src/gui/accountstate.cpp )
list(APPEND RemoteWipe_SRC ../src/gui/socketapi.cpp )

View File

@@ -716,22 +716,26 @@ class FakeErrorReply : public QNetworkReply
public:
FakeErrorReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request,
QObject *parent, int httpErrorCode)
: QNetworkReply{parent}, _httpErrorCode(httpErrorCode) {
: QNetworkReply{parent}, _httpErrorCode(httpErrorCode){
setRequest(request);
setUrl(request.url());
setOperation(op);
open(QIODevice::ReadOnly);
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, httpErrorCode);
setError(InternalServerError, "Internal Server Fake Error");
QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
}
Q_INVOKABLE void respond() {
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, _httpErrorCode);
setError(InternalServerError, "Internal Server Fake Error");
emit metaDataChanged();
emit readyRead();
// finishing can come strictly after readyRead was called
QTimer::singleShot(5, this, &FakeErrorReply::slotSetFinished);
}
// make public to give tests easy interface
using QNetworkReply::setError;
using QNetworkReply::setAttribute;
public slots:
void slotSetFinished() {

248
test/testdownload.cpp Normal file
View File

@@ -0,0 +1,248 @@
/*
* This software is in the public domain, furnished "as is", without technical
* support, and with no warranty, express or implied, as to its usefulness for
* any purpose.
*
*/
#include <QtTest>
#include "syncenginetestutils.h"
#include <syncengine.h>
#include <owncloudpropagator.h>
using namespace OCC;
static constexpr qint64 stopAfter = 3'123'668;
/* A FakeGetReply that sends max 'fakeSize' bytes, but whose ContentLength has the corect size */
class BrokenFakeGetReply : public FakeGetReply
{
Q_OBJECT
public:
using FakeGetReply::FakeGetReply;
int fakeSize = stopAfter;
qint64 bytesAvailable() const override
{
if (aborted)
return 0;
return std::min(size, fakeSize) + QIODevice::bytesAvailable();
}
qint64 readData(char *data, qint64 maxlen) override
{
qint64 len = std::min(qint64{ fakeSize }, maxlen);
std::fill_n(data, len, payload);
size -= len;
fakeSize -= len;
return len;
}
};
SyncFileItemPtr getItem(const QSignalSpy &spy, const QString &path)
{
for (const QList<QVariant> &args : spy) {
auto item = args[0].value<SyncFileItemPtr>();
if (item->destination() == path)
return item;
}
return {};
}
class TestDownload : public QObject
{
Q_OBJECT
private slots:
void testResume()
{
FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
fakeFolder.syncEngine().setIgnoreHiddenFiles(true);
QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
auto size = 30 * 1000 * 1000;
fakeFolder.remoteModifier().insert("A/a0", size);
// First, download only the first 3 MB of the file
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
if (op == QNetworkAccessManager::GetOperation && request.url().path().endsWith("A/a0")) {
return new BrokenFakeGetReply(fakeFolder.remoteModifier(), op, request, this);
}
return nullptr;
});
QVERIFY(!fakeFolder.syncOnce()); // The sync must fail because not all the file was downloaded
QCOMPARE(getItem(completeSpy, "A/a0")->_status, SyncFileItem::SoftError);
QCOMPARE(getItem(completeSpy, "A/a0")->_errorString, QString("The file could not be downloaded completely."));
QVERIFY(fakeFolder.syncEngine().isAnotherSyncNeeded());
// Now, we need to restart, this time, it should resume.
QByteArray ranges;
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
if (op == QNetworkAccessManager::GetOperation && request.url().path().endsWith("A/a0")) {
ranges = request.rawHeader("Range");
}
return nullptr;
});
QVERIFY(fakeFolder.syncOnce()); // now this succeeds
QCOMPARE(ranges, QByteArray("bytes=" + QByteArray::number(stopAfter) + "-"));
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
}
void testErrorMessage () {
// This test's main goal is to test that the error string from the server is shown in the UI
FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
fakeFolder.syncEngine().setIgnoreHiddenFiles(true);
QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
auto size = 3'500'000;
fakeFolder.remoteModifier().insert("A/broken", size);
QByteArray serverMessage = "The file was not downloaded because the tests wants so!";
// First, download only the first 3 MB of the file
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
if (op == QNetworkAccessManager::GetOperation && request.url().path().endsWith("A/broken")) {
return new FakeErrorReply(op, request, this, 400,
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
"<d:error xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\">\n"
"<s:exception>Sabre\\DAV\\Exception\\Forbidden</s:exception>\n"
"<s:message>"+serverMessage+"</s:message>\n"
"</d:error>");
}
return nullptr;
});
bool timedOut = false;
QTimer::singleShot(10000, &fakeFolder.syncEngine(), [&]() { timedOut = true; fakeFolder.syncEngine().abort(); });
QVERIFY(!fakeFolder.syncOnce()); // Fail because A/broken
QVERIFY(!timedOut);
QCOMPARE(getItem(completeSpy, "A/broken")->_status, SyncFileItem::NormalError);
QVERIFY(getItem(completeSpy, "A/broken")->_errorString.contains(serverMessage));
}
void serverMaintenence() {
// Server in maintenance must abort the sync.
FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
fakeFolder.remoteModifier().insert("A/broken");
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
if (op == QNetworkAccessManager::GetOperation) {
return new FakeErrorReply(op, request, this, 503,
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
"<d:error xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\">\n"
"<s:exception>Sabre\\DAV\\Exception\\ServiceUnavailable</s:exception>\n"
"<s:message>System in maintenance mode.</s:message>\n"
"</d:error>");
}
return nullptr;
});
QSignalSpy completeSpy(&fakeFolder.syncEngine(), &SyncEngine::itemCompleted);
QVERIFY(!fakeFolder.syncOnce()); // Fail because A/broken
// FatalError means the sync was aborted, which is what we want
QCOMPARE(getItem(completeSpy, "A/broken")->_status, SyncFileItem::FatalError);
QVERIFY(getItem(completeSpy, "A/broken")->_errorString.contains("System in maintenance mode"));
}
void testMoveFailsInAConflict() {
#ifdef Q_OS_WIN
QSKIP("Not run on windows because permission on directory does not do what is expected");
#endif
// Test for https://github.com/owncloud/client/issues/7015
// We want to test the case in which the renaming of the original to the conflict file succeeds,
// but renaming the temporary file fails.
// This tests uses the fact that a "touchedFile" notification will be sent at the right moment.
// Note that there will be first a notification on the file and the conflict file before.
FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
fakeFolder.syncEngine().setIgnoreHiddenFiles(true);
fakeFolder.remoteModifier().setContents("A/a1", 'A');
fakeFolder.localModifier().setContents("A/a1", 'B');
bool propConnected = false;
QString conflictFile;
auto transProgress = connect(&fakeFolder.syncEngine(), &SyncEngine::transmissionProgress,
[&](const ProgressInfo &pi) {
auto propagator = fakeFolder.syncEngine().getPropagator();
if (pi.status() != ProgressInfo::Propagation || propConnected || !propagator)
return;
propConnected = true;
connect(propagator.data(), &OwncloudPropagator::touchedFile, [&](const QString &s) {
if (s.contains("conflicted copy")) {
QCOMPARE(conflictFile, QString());
conflictFile = s;
return;
}
if (!conflictFile.isEmpty()) {
// Check that the temporary file is still there
QCOMPARE(QDir(fakeFolder.localPath() + "A/").entryList({"*.~*"}, QDir::Files | QDir::Hidden).count(), 1);
// Set the permission to read only on the folder, so the rename of the temporary file will fail
QFile(fakeFolder.localPath() + "A/").setPermissions(QFile::Permissions(0x5555));
}
});
});
QVERIFY(!fakeFolder.syncOnce()); // The sync must fail because the rename failed
QVERIFY(!conflictFile.isEmpty());
// restore permissions
QFile(fakeFolder.localPath() + "A/").setPermissions(QFile::Permissions(0x7777));
QObject::disconnect(transProgress);
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &, QIODevice *) -> QNetworkReply * {
if (op == QNetworkAccessManager::GetOperation)
QTest::qFail("There shouldn't be any download", __FILE__, __LINE__);
return nullptr;
});
QVERIFY(fakeFolder.syncOnce());
// The a1 file is still tere and have the right content
QVERIFY(fakeFolder.currentRemoteState().find("A/a1"));
QCOMPARE(fakeFolder.currentRemoteState().find("A/a1")->contentChar, 'A');
QVERIFY(QFile::remove(conflictFile)); // So the comparison succeeds;
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
}
void testHttp2Resend() {
FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
fakeFolder.remoteModifier().insert("A/resendme", 300);
QByteArray serverMessage = "Needs to be resend on a new connection!";
int resendActual = 0;
int resendExpected = 2;
// First, download only the first 3 MB of the file
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
if (op == QNetworkAccessManager::GetOperation && request.url().path().endsWith("A/resendme") && resendActual < resendExpected) {
auto errorReply = new FakeErrorReply(op, request, this, 400, "ignore this body");
errorReply->setError(QNetworkReply::ContentReSendError, serverMessage);
errorReply->setAttribute(QNetworkRequest::HTTP2WasUsedAttribute, true);
errorReply->setAttribute(QNetworkRequest::HttpStatusCodeAttribute, QVariant());
resendActual += 1;
return errorReply;
}
return nullptr;
});
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(resendActual, 2);
fakeFolder.remoteModifier().appendByte("A/resendme");
resendActual = 0;
resendExpected = 10;
QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
QVERIFY(!fakeFolder.syncOnce());
QCOMPARE(resendActual, 4); // the 4th fails because it only resends 3 times
QCOMPARE(getItem(completeSpy, "A/resendme")->_status, SyncFileItem::NormalError);
QVERIFY(getItem(completeSpy, "A/resendme")->_errorString.contains(serverMessage));
}
};
QTEST_GUILESS_MAIN(TestDownload)
#include "testdownload.moc"

4003
translations/client_af.ts Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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