Compare commits

...

160 Commits

Author SHA1 Message Date
b7ef6e1486 chore: Umshiang Bridge 3.5.1 changelog. 2023-09-27 13:18:23 +02:00
0d03f84711 fix(GODT-2963): Use multi error to report file removal errors
Do not abort removing files on first error. Collect errors and try to
remove as many as possible. This would cause some state files to not be
removed on windows.
2023-09-27 12:34:07 +02:00
949666724d chore: Umshiang Bridge 3.5.1 changelog. 2023-09-27 10:54:50 +02:00
bbe19bf960 fix(GODT-2956): Restore old deletion rules
When unlabeling a message from trash we have to check if this message is
present in another folder before perma-deleting.
2023-09-26 14:06:31 +02:00
bfe25e3a46 fix(GODT-2951): Negative WaitGroup Counter
Do not defer call to `wg.Done()` in `job.onJobFinished`. If there is an
error it will also call `wg.Done()`.
2023-09-26 13:58:46 +02:00
236c958703 fix(GODT-2590): Fix send on closed channel
Ensure periodic user tasks are terminated before the other user
services. The panic triggered due to the fact that the telemetry service
was shutdown before this periodic task.
2023-09-26 13:58:18 +02:00
e6b312b437 fix(GODT-2949): Fix close of close channel in event service
This issue is triggered due to the `Service.Close()` call after the
go-routine for the event service exists. It is possible that during this
period a recently added subscriber with `pendingOpAdd` gets cancelled
and closed.

However, the subscriber later also enqueues a `pendingOpRemove` which
gets processed again with a call in `user.eventService.Close()` leading
to the double close panic.

This patch simply removes the `s.Close()` from the service, and leaves
the cleanup to called externally from user.Close() or user.Logout().
2023-09-26 13:58:07 +02:00
45d2e9ea63 chore: update changelog. 2023-09-13 10:25:47 +02:00
86e8a566c7 chore: Umshiang Bridge 3.5.0 changelog. 2023-09-12 07:45:08 +02:00
7689139cb3 fix(GODT-2909): Remove Timeout on event publish
While good intentioned, this change causes issues when the computer goes
to sleep and a user resumes after the timeout interval.
2023-09-11 10:03:00 +02:00
6269b1ab88 fix(GODT-2913): Reduce the number of configuration failure detected. 2023-09-08 11:53:24 +00:00
79524185a8 feat(GODT-2734): Add testing steps to modify account settings. 2023-09-04 16:48:59 +02:00
635b81314a test(GODT-2746): polish the test code. 2023-09-01 07:17:21 +02:00
4c76e35a2d test(GODT-2746): Added tags to certain test scenarios
- Added tags to certain test scenarios so they are not ran
on each MR, just on the nightly.

GODT-2746
2023-09-01 04:52:42 +00:00
b0ac20425e test(GODT-2746): Updated test scenario
GODT-2746
2023-09-01 04:52:42 +00:00
21a56a0725 test(GODT-2746): Add integration test for logged out user
GODT-2746
2023-09-01 04:52:42 +00:00
c57b52ef23 test(GODT-2746): Integration tests for reporting a problem
- Add function for checking header in form-data request

Bug reports are sent with multipart/form-data, and the function parses this,
then compares the needed field with the wanted
Also, added the step definition.

- Add functions for reporting a bug with changes

Be able to report a bug by changing the value of a single field, or multiple fields through a JSON format

- Add integration tests for reporting a problem
2023-09-01 04:52:42 +00:00
44e103edd5 chore: Umshiang Bridge 3.5.0 changelog. 2023-08-31 16:39:13 +02:00
970de4c205 fix(GODT-2828): Fix negative report time 2023-08-31 11:52:48 +00:00
a189c35899 fix(GODT-2828): Fix sync progress report after restart
Number of already synced messages also needs to be multiplied by the
number of sync stages to have accurate progress reporting.
2023-08-31 11:52:48 +00:00
c9323a40c8 fix(GODT-2867): do not crash on timeout or context cancel. 2023-08-31 11:14:44 +00:00
7a192d50db feat(GODT-2891): Allow message create & delete during sync
Incoming messages which arrive into labels we know during sync are now
presented to the IMAP clients.

We also allow messages to be deleted while syncing if deleted on other
clients.

Other operations such as moving, marking messages as read and label
operations need to be considered in a follow up patch as they are far
more complex.
2023-08-31 10:47:47 +02:00
a5a9bd762d feat(GODT-2848): Decouple IMAP service from Event Loop
The IMAP service no longer blocks the event loop from progressing. When
syncing the event loop continues as usual, allow other parts of the
bridge to continue updating.

Once the sync is complete, it resets the event id back to the last
handled id before sync started and then we play catch up.
2023-08-30 14:10:36 +02:00
dd7db00f74 chore: Umshiang Bridge 3.5.0 changelog. 2023-08-29 14:35:49 +02:00
a6c20f698c feat(GODT-2828): Increase sync progress report frequency
We now report sync progress after a batch completes each stage.
2023-08-29 11:50:50 +00:00
f7252ed40e fix(GODT-2693): Duplicate messages in sent folder
* Only call `RemoveOnFail` on error. It was also called on success.
* Add some logging to track send times.
* Fix non-deterministic hash from `rfc8222.GetMessageHash`.
    https://github.com/ProtonMail/gluon/pull/395
2023-08-29 11:50:50 +00:00
66c30716ea fix(GODT-2867): get attachment returns API error on network problem. 2023-08-29 13:20:45 +02:00
06d7fdf26f fix(GODT-2805): Ignore Contact Group Labels 2023-08-29 07:25:00 +00:00
8aee732fe8 fix(GODT-2866): Add 429/5xx Retry to Event Service 2023-08-29 07:25:00 +00:00
47233752f5 fix(GODT-2855): fix for text overlapping in settings view. 2023-08-28 13:00:33 +00:00
cb6e51531b test: Fix TestBridge_SyncWithOnGoingEvents
Checking for the initial status of the original check as this is not
required to verify the intended behavior. The original check is also
prone to issues related to event processing order.
2023-08-28 13:55:14 +02:00
0b9b886039 fix(GODT-2829): Fix new sync service bugs
* Fix wrong context use for message downloads
* Fix delete of sync data failing due ErrNotFound
* Pre-allocate attachment data buffer before download
* Fix calculation of progress if message count is higher than total
2023-08-28 13:37:55 +02:00
1fa0d77b10 chore: Add trace profiling option 2023-08-28 11:23:41 +02:00
efbe84964f feat(GODT-2829): Integrate new sync service
Update imap service to use the new sync service.

The new sync state is stored as simple file on disk to avoid contention
with concurrent vault writes.
2023-08-25 15:21:00 +02:00
aa77a67a1c fix(GODT-2829): Sync Service fixes
Fix tracking of child jobs. The build stage splits the incoming work
even further, but this was not reflected in the wait group counter. This
also fixes an issue where the cache was cleared to late.

Add more debug info for analysis.

Refactor sync state interface in order to have persistent sync rate.
2023-08-25 15:03:27 +02:00
78f7cbdc79 feat(GODT-2829): New Sync Service
Implementation of the new sync service that interleaves syncing jobs for
all active users.

It also includes improvements to the message downloader. The download
will now auto rate limit the parallel workers based on the server
responses.

Additionally each of the stages is now tested in isolation to ensure the
behavior matches the expectations.

Finally, this patch does not replace the existing IMAP sync. A follow up
patch is necessary to integrate the IMAP bits into the interfaces
required by these changes.
2023-08-23 16:12:19 +02:00
a731237701 test(GODT-2871): tests for new telemetry logic. 2023-08-22 11:15:47 +02:00
f557666b4d feat(GODT-2871): is telemetry enabled as service. 2023-08-21 14:08:47 +02:00
5f89e85139 chore: Under Bridge 3.5.0 changelog. 2023-08-17 12:39:31 +02:00
9fb922d18f chore: merge master to devel 2023-08-17 12:04:17 +02:00
884c6ed932 fix(GODT-2865): add error on failed unlock. 2023-08-17 08:19:25 +00:00
db77bd4983 test(GODT-2873): Wait for Gluon Watcher to finish
Avoids go-routine leak checker entry.

https://github.com/ProtonMail/gluon/pull/393
2023-08-17 06:31:25 +00:00
85bfcf7158 test(GODT-2744): Add integration tests for moving messages (with MOVE support) 2023-08-17 06:22:12 +00:00
09a5f8ac0f test(GODT-2872): Fix nightly job 2023-08-16 12:56:40 +00:00
81f81a63e8 test(GODT-2742): Add more integration tests regarding drafts 2023-08-16 10:56:23 +00:00
e724fafe2f chore: checkout features from trift to devel 2023-08-15 15:40:53 +02:00
cba73997cd fix(GODT-2802): Fix missing key pass input for keyring unlock 2023-08-14 13:09:22 +02:00
29f44fc312 fix(GODT-2814): Server Manager shutdown task cancel 2023-08-14 11:04:53 +02:00
41c125f65e feat(GODT-2803): Gluon IMAP State access
Update to latest Gluon to allow access to the database for bridge. The
cache is placed in a `SharedCache` type to ensure that we respect calls
to `Connector.Close`.

In theory, this should never trigger an error, but this way we can catch
it if it happens.

https://github.com/ProtonMail/gluon/pull/391
2023-08-14 09:51:49 +02:00
24331f9715 fix(GODT-2803): Separate conditions to pause event loop for IMAP
Add two separate toggles to control event loop pausing. This is required
to prevent cases where the bridge requests the event loop to be paused
but a sync process completes and resumes the event loop.

For the loop to resume now both states need to be set to false. This
will be removed once GODT-2848 is implemented.
2023-08-11 14:32:18 +02:00
a5500629e5 feat(GODT-2803): Add Event Poll Waiter
Wait for the current event poll to finish publishing events after a
request to pause the event loop. This is required to change the gluon
cache directory.
2023-08-11 14:32:18 +02:00
a46533dcf2 feat(GODT-2787): Force Scrollview to top when re-opening questions set. 2023-08-11 08:43:23 +02:00
12183fbf05 feat(GODT-2787): Tweaking Bug Report form with last Review. 2023-08-11 05:39:34 +00:00
c90248920a ci(GODT-2717): Create a job that will run on schedule 2023-08-10 12:19:59 +00:00
ccb9b7f81c feat(GODT-2787): Fix vertical alignement on CategoryItem. 2023-08-10 08:43:37 +00:00
78c0651661 feat(GODT-2842): Implement Bug Report Fallback notification. 2023-08-10 09:46:12 +02:00
4ee6da4baa chore(GODT-2848): Simplify User Event Service
Rather than having the services subscribe to each individual event type,
have only one subscriber for the whole event object. This spreads the
logic to the service through the `EventHandler` type.

This is important for the next stage where the IMAP service will be
required to apply events from the past.
2023-08-09 16:11:09 +02:00
e8ce8942be feat(GODT-2788): Add preview to bug report validation. 2023-08-09 11:39:08 +00:00
9d0d3708af feat(GODT-2808): Apply comment from Bug Report content review. 2023-08-09 11:07:15 +00:00
4c908aac7c fix(GODT-2857): Do not check changed values in clear recent flag
https://github.com/ProtonMail/gluon/pull/390
2023-08-09 10:18:30 +02:00
6eb1878f66 fix(GODT-2827): Restore ticker to event poller 2023-08-09 10:17:41 +02:00
826dc2e5c3 test(GODT-2743): Sync high number of messages 2023-08-08 14:37:00 +00:00
20b188368a feat(GODT-2799): SMTP service interacts directly with Server Manager
Bridge no longer needs to manually add and remove accounts from the
service.
2023-08-08 14:14:52 +02:00
ded4f370dc fix(GODT-2759): Use examine rather than select for fetching
When fetching messages in the debug mailbox state command, use read only
mode to avoid modifying the mailbox state.
2023-08-08 13:05:39 +02:00
519a6ecdb7 fix(GODT-2833): Migration of Message Flags
https://github.com/ProtonMail/gluon/pull/388
2023-08-08 12:38:54 +02:00
9a96588afb feat(GODT-2814): Standalone Server Manager
Convert ServerManger into a standalone service so that it can become a
self contained module.
2023-08-07 16:47:41 +02:00
1f2c573803 feat(GODT-2782): Apply content modification after review. 2023-08-03 18:05:33 +02:00
cc33423c1f feat(GODT-2782): Add category name in decribe issue view + apply review comments. 2023-08-03 17:51:37 +02:00
1b95c290f1 feat(GODT-2808): Initial list of categories and questions. 2023-08-01 14:49:53 +00:00
9b88778c43 feat(GODT-2788): Add JSON validator file. 2023-08-01 14:49:53 +00:00
ae4705ba70 feat(GODT-2787): Replace the PathTracker by a more visual NavigationIndicator. 2023-08-01 14:49:53 +00:00
243ddf47ab feat(GODT-2816): Handle maxChar even for non mandatory field + make it customisable from JSON. 2023-08-01 14:49:53 +00:00
80d729e3e5 feat(GODT-2816): Wait until mandatory fields are filled then fill body and title. 2023-08-01 14:49:53 +00:00
3d64c5f894 feat(GODT-2794): Clear cached answers when report is sent. 2023-08-01 14:49:53 +00:00
c4bcc38c53 feat(GODT-2793): Feed the bug report body with the answered questions. 2023-08-01 14:49:53 +00:00
2c2f816f3a feat(GODT-2791): Add Unitary test. 2023-08-01 14:49:53 +00:00
86e115b2f3 feat(GODT-2791): Parse the Bug Report Flow description file and ensure forward compatibility (GODT-2789). 2023-08-01 14:49:53 +00:00
1a2783a63b feat(GODT-2821): Display questions in one page. 2023-08-01 14:49:53 +00:00
cbbab71f5c feat(GODT-2786): Init bug report flow description file. 2023-08-01 14:49:53 +00:00
80add80be2 feat(GODT-2792): Implement display of question set for bug report. 2023-08-01 14:49:53 +00:00
f0e2688a8e fix(GODT-2800): Ensure 429 does not cause bad event 2023-07-31 13:15:26 +02:00
06639ff6cd fix(GODT-2800): Handle subscribe followed by unsubscribe properly 2023-07-31 13:12:29 +02:00
716de7f45a test: Fix event registration in TestBridge_SyncWithOngoingEvents
We need to register the event subscriber earlier to avoid missing the
event later.
2023-07-31 12:36:06 +02:00
750de0cd31 test: TestBridge_SendAddTextBodyPartIfNotExists eventually fix
It may take a couple of fetch requests until the pending state updates
get applied.
2023-07-31 12:36:06 +02:00
823ca4d207 feat(GODT-2822): Integrate and activate all service
The bridge now runs on the new architecture.
2023-07-31 12:36:03 +02:00
a187747c7c feat(GODT-2802): IMAP Serivce
Handles all IMAP related tasks. Unlike the previous iteration, this new
service automatically adds and removes users from Gluon by interfacing
with server manager.
2023-07-31 11:06:47 +02:00
11ebb16933 fix(GODT-2822): Update smtp and events service to use ordered cancel 2023-07-31 11:06:47 +02:00
0048767022 fix(GODT-28022): Missing user identity service methods
Add methods to access user identity state from the outside.
2023-07-31 11:06:47 +02:00
c4b75c6f34 fix(GODT-2802): Ensure cpc reply does not block if never read 2023-07-31 11:06:43 +02:00
32448063dc test: Verify leaks at end of WithEnv
Helps track down the individual test leaks.
2023-07-28 15:00:23 +02:00
86bde91958 feat(GODT-2802): Ordered task cancellation
Add `OrderedCancelGroup` type to enforce the cancellation of go routines
in a reverse order. Additionally this type waits for the go-routine to
finish before proceeding to the next one.

We need this to guarantee that all the user services shut down properly
without deadlocking.
2023-07-28 14:50:42 +02:00
90c34406ba test: Fix deadlock in chToType
Make sure the go routine is cancelled if there is a new event, but no
one is reading it after a call to done.
2023-07-28 13:32:28 +02:00
334a256638 fix(GODT-2802): Remove CPC request from Event Service
Prevents deadlocks if the service needs to be paused during an event
loop.
2023-07-26 10:03:48 +02:00
da528f2d9b feat(GODT-2801): Add Key Decryption helpers to identityservice.State 2023-07-25 09:59:26 +02:00
1b0f930471 fix(GODT-2802): Consolidate Address Mode 2023-07-25 09:15:52 +02:00
09c523e2d2 feat(GODT-2801): User Service Integration
Enable the User Event, User Identity and User SMTP service. The services
don't persist any data at the moment to avoid conflict with the existing
event loop.
2023-07-24 17:10:05 +02:00
0b35f41ffd feat(GODT-2799): Integrate SMTP service with User Identity Service 2023-07-24 17:10:05 +02:00
7be1a8ae8a feat(GODT-2801): Identity State Cloning & Auth Check
This patch moves the `user.User.CheckAuth` function into the `State`
type as it contains all the necessary information to perform this check.

A couple of more interfaces are added to abstract the retrieval of the
Bridge and the User's key passwords, as well as telemetry reports.

Finally, adds `State.OnAddressEvents` which other services should use to
update the state.
2023-07-24 17:10:05 +02:00
bdbf1bdd76 fix(GODT-2800): Pending Subscriptions Cleanup
Ensure pending subscriptions are cleaned up with `Service.Close()` or
we can leak go routines.
2023-07-24 17:10:05 +02:00
776976a8a2 feat(GODT-2801): Debug names for QueuedChannels
https://github.com/ProtonMail/gluon/pull/385
https://github.com/ProtonMail/go-proton-api/pull/90
2023-07-24 16:38:01 +02:00
040ddadb7a feat(GODT-2801): Identity Service
Identity Service contains all the information related to user state,
addresses and keys.

This patch also introduces the `State` type which can be used by other
services to maintain their own copy of this state to avoid lock
contention.

Finally, there are currently no external facing methods via a CPC
interface. Those will added as needed once the refactoring of the
architecture is complete.
2023-07-24 11:22:50 +02:00
11f6f84dd6 fix(GODT-2800): ChanneledSubcriber types 2023-07-21 15:18:49 +02:00
82efa16d65 feat(GODT-2800): User Event Service
This patch adds the User Event Service which is meant to replace the
current event polling flow.

Each user interested in receiving events should register a new
subscriber using the `Service.Subscribe` function and then react on
the incoming events.

The current patch does not hook this up Bridge user as there are no
existing consumers, but it does provide extensive testing for the
expected behavior.
2023-07-21 11:47:43 +02:00
110286b81c fix(GODT-2813): Write new vault to temporary file first
Do not immediately overwrite the old vault with new data. If something
goes wrong, we at least maintain on previously valid state.
2023-07-19 09:12:36 +02:00
bc66841cdc chore(GODT-2799): Move SMTP backend to SMTP service module 2023-07-19 09:12:33 +02:00
8d028966c7 chore(GODT-2799): Move Identifier interface to separate module
Required so we can move the smtp backend into the smtp service module.
2023-07-19 09:07:02 +02:00
d120bbeffc chore(GODT-2799): Separate account states for SMTP Backend
Rather than accessing the Bridge user list, each user register their
individual SMTP service with the server manager.

Note that some dependencies on the user data are hidden behind the
`UserInterface`. These will be removed in a future patch.
2023-07-19 09:07:02 +02:00
eda49483e2 chore: use qmlformat on qml files, and removed deprecated tests. 2023-07-18 18:12:29 +02:00
3ef526333a feat(GODT-2799): SMTP Service
Refactor code to isolate the SMTP functionality in a dedicated SMTP
service for each user as discussed in the Bridge Service Architecture
RFC.

Some shared types have been moved from `user` to `usertypes` so that
they can be shared with Service and User Code.

Finally due to lack of recursive imports, the user data SMTP needs
access to is hidden behind an interface until the User Identity service
is implemented.
2023-07-18 11:49:18 +02:00
f454f1a74f chore: Enable fuzzing in CI 2023-07-18 09:13:20 +02:00
e97a4d8847 chore: update Makefile to add fuzz
Signed-off-by: Arjun Singh <ajsinghyadav00@gmail.com>
2023-07-18 09:13:17 +02:00
cb8174dbfd test: oss-fuzz support for fuzzing
Signed-off-by: Arjun Singh <ajsinghyadav00@gmail.com>
2023-07-18 09:13:11 +02:00
cfca429067 fix(GODT-2807): fix issue where sessionID would not be removed from command-line on restart by bridge-gui. 2023-07-17 12:48:10 +02:00
1ffb9089ba fix(GODT-2687): Tabs after header field colon
https://github.com/ProtonMail/gluon/pull/384
2023-07-17 11:26:00 +02:00
c0fc23d7cd chore: merge "release/trift" to devel 2023-07-13 08:21:49 +02:00
42178806d1 Revert "feat(GODT-2749): manual windows-test."
This reverts commit 650158ea8a.

The issue has that was causing the problem has been addressed.
2023-07-11 16:07:11 +02:00
e34050282e fix(GODT-2764): Allow perma-delete for messages which still have labels
Rather than match the web client behavior (removing labels) when
messages are appended to trash, we simply omit the check to see if the
message in trash is present in other labels.

This will produce the same end result and will resolve the issue of
users not being able to fully delete their messages.
2023-07-11 16:04:36 +02:00
efb6ba0f1b fix(GODT-2782): Filter all labels when doing perma delete check
Previously we were not filtering out labels we ignored from the
perma-delete check. The introduction of new system label types could
break this check leading to user never being able to perma-delete
messages.
2023-07-11 13:41:21 +02:00
80194ad797 fix(GODT-2693): Fix message appearing twice after sent
Previous attempt at fixing a bug in the send recorder (GODT-2627)
introduced a new problem where if the same message is sent multiple
times with different recipients it is possible to trigger a case where
the incorrect wait channel is chosen. This in turn led to IMAP client
not recognizing that message has been successfully submitted. This
case is represented by
`TestSendHashed_SameMessageWIthDifferentToListShouldWaitSuccessfullyAfterSend`
but could potentially happen over time or due some other
concurrency/scheduling wake up order.

To prevent this from happening every send recorder request now requires
that the full list of addresses be presented. This is necessary for us
to locate the correct entry and its respective wait channel.

Finally each unique send recorder request is assigned an ID, in order
to ensure make sure that if we ever need to cancel a request,
we don't accidentally cancel a similar request if the original was
removed from scope due to expiration.
2023-07-11 07:33:32 +00:00
cda6b2a728 fix(GODT-2781): try to remove stale lock file before failing in checkSingleInstance. 2023-07-11 09:03:45 +02:00
44bde86fde fix(GODT-2780): fix 'QSystemTrayIcon::setVisible: No Icon set' warning in bridge-gui log on startup. 2023-07-10 10:47:10 +02:00
6cb52cacc9 fix(GODT-2778): fix login screen being disabled after an 'already logged in' error. 2023-07-10 09:44:18 +02:00
8248491833 chore: Merge branch 'release-notes' into devel 2023-07-07 16:00:56 +02:00
fbdede28f0 chore: fix typos found by codespell in Changelog.md 2023-07-07 15:31:34 +02:00
66bc911652 chore: fix typos found by codespell 2023-07-07 15:31:14 +02:00
c860741ffa doc: Update 3.3.1 release notes 2023-07-07 11:02:48 +02:00
300d243331 doc: Added 3.3.1 release notes 2023-07-04 20:35:54 +00:00
c4abb14ae6 chore: 3.3.0 stable release notes 2023-06-15 14:36:21 +02:00
a06fd31f49 doc: Add missing 3.3.0 changes 2023-06-07 22:52:37 +00:00
016319784e doc: Add 3.3.0 2023-06-07 22:48:17 +00:00
1d595b933a doc: Add 3.2.0 release 2023-05-26 06:45:49 +00:00
39cd165bd1 doc: Improve readability by separating sentences 2023-05-26 06:45:34 +00:00
afef730870 chore: notes 2023-05-15 17:16:44 +02:00
f691795ca1 doc: Add 3.2.0 release notes 2023-05-15 11:26:03 +00:00
9798cdec5c doc: Add 3.1.3 2023-05-10 08:03:14 +00:00
9ae899f420 doc: Add 3.1.3 2023-05-10 08:02:19 +00:00
f2a8990972 doc: typo 2023-04-26 17:14:15 +00:00
8db30a89c0 doc: typo 2023-04-26 17:13:12 +00:00
449b580056 doc: typo 2023-04-26 17:11:54 +00:00
38397accf5 doc: Add 3.1.2 release 2023-04-26 17:10:54 +00:00
5d49cf2c09 doc: Add 3.1.2 2023-04-26 17:08:34 +00:00
c1918e2b1b doc: release notes 3.1.1 2023-04-11 13:21:53 +02:00
60a779c653 doc: Added 3.1.0 2023-04-06 06:50:15 +00:00
4adc05d354 doc: Add 3.0.21 2023-03-23 09:43:10 +00:00
86fbf4415a doc: Add 3.0.21 2023-03-23 09:42:51 +00:00
b4ea667858 doc: Added 3.0.20 release notes 2023-03-09 15:47:32 +00:00
8f45fe823a doc: Add 3.0.20 release notes 2023-03-09 15:46:54 +00:00
3b1c3b9d44 doc: Add 3.0.19 release 2023-02-28 07:21:55 +00:00
60256fd076 doc: Add 3.0.19 release 2023-02-28 07:21:01 +00:00
9c1b9e8df2 doc: Add 3.0.18 and remove non-beta 3.0.17 2023-02-24 08:36:02 +00:00
fb44de4f18 doc: Add 3.0.18 release notes 2023-02-23 16:16:26 +00:00
c821d02f67 doc: Reduce the technical terms and do not show v3 development related changes for users who have only seen v2 2023-02-21 07:01:32 +00:00
ec2a4f9111 doc: release notes v3 live 2023-02-20 05:59:57 +01:00
968a01053f chore: release notes 2023-02-17 12:02:44 +01:00
6f914a4973 chore: early 3.0.15 release notes 2023-02-14 12:24:50 +01:00
76bdc21fef doc: Add 3.0.14 notes 2023-02-09 09:56:45 +00:00
dd29ff4731 doc: Remove details 2023-02-01 11:31:57 +00:00
8b94a28e00 doc: 3.0.12 notes 2023-02-01 12:28:08 +01:00
60100ad7f0 chore: update release notes wording. 2023-02-01 11:48:21 +01:00
62a50fd7fc chore: release notes 2023-02-01 11:21:12 +01:00
257 changed files with 24354 additions and 13147 deletions

View File

@ -47,6 +47,19 @@ stages:
allow_failure: true allow_failure: true
- when: never - when: never
.rules-branch-manual-scheduled-and-test-branch-always:
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
when: always
allow_failure: false
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME=~ /^test/
when: always
allow_failure: false
- if: $CI_COMMIT_BRANCH
when: manual
allow_failure: true
- when: never
# ENV # ENV
.env-windows: .env-windows:
before_script: before_script:
@ -118,6 +131,15 @@ lint:
tags: tags:
- medium - medium
bug-report-preview:
stage: test
extends:
- .rules-branch-and-MR-manual
script:
- make lint-bug-report-preview
tags:
- medium
.script-test: .script-test:
stage: test stage: test
extends: extends:
@ -134,6 +156,15 @@ test-linux:
tags: tags:
- large - large
fuzz-linux:
stage: test
extends:
- .rules-branch-manual-MR-and-devel-always
script:
- make fuzz
tags:
- large
test-linux-race: test-linux-race:
extends: extends:
- test-linux - test-linux
@ -154,11 +185,19 @@ test-integration-race:
script: script:
- make test-integration-race - make test-integration-race
test-integration-nightly:
extends:
- test-integration
- .rules-branch-manual-scheduled-and-test-branch-always
needs:
- test-integration
script:
- make test-integration-nightly
test-windows: test-windows:
extends: extends:
- .env-windows - .env-windows
- .script-test - .script-test
- .rules-branch-and-MR-manual
test-darwin: test-darwin:
extends: extends:
@ -168,15 +207,16 @@ test-darwin:
test-coverage: test-coverage:
stage: test stage: test
extends: extends:
- .rules-branch-manual-MR-and-devel-always - .rules-branch-manual-scheduled-and-test-branch-always
script: script:
- ./utils/coverage.sh - ./utils/coverage.sh
coverage: '/total:.*\(statements\).*\d+\.\d+%/' coverage: '/total:.*\(statements\).*\d+\.\d+%/'
needs: needs:
- test-linux - test-linux
#- test-windows - test-windows
- test-darwin - test-darwin
- test-integration - test-integration
- test-integration-nightly
tags: tags:
- small - small
artifacts: artifacts:

View File

@ -3,6 +3,91 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/) Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Umshiang Bridge 3.5.1
### Fixed
* GODT-2963: Use multi error to report file removal errors.
* GODT-2956: Restore old deletion rules.
* GODT-2951: Negative WaitGroup Counter.
* GODT-2590: Fix send on closed channel.
* GODT-2949: Fix close of close channel in event service.
## Umshiang Bridge 3.5.0
### Added
* GODT-2734: Add testing steps to modify account settings.
* GODT-2746: Integration tests for reporting a problem.
* GODT-2891: Allow message create & delete during sync.
* GODT-2848: Decouple IMAP service from Event Loop.
* Add trace profiling option.
* GODT-2829: New Sync Service.
* Test: oss-fuzz support for fuzzing.
* GODT-2799: SMTP Service.
* GODT-2800: User Event Service.
* GODT-2801: Identity Service.
* GODT-2802: IMAP Serivce.
* GODT-2788: Add preview to bug report validation and JSON file validator.
* GODT-2803: Bridge Database access.
### Changed
* GODT-2909: Remove Timeout on event publish.
* GODT-2913: Reduce the number of configuration failure detected.
* GODT-2828: Increase sync progress report frequency.
* Test: Fix TestBridge_SyncWithOnGoingEvents.
* GODT-2871: Is telemetry enabled as service.
* Test(GODT-2873): Wait for Gluon Watcher to finish.
* Test(GODT-2744): Add integration tests for moving messages (with MOVE support).
* Test(GODT-2872): Fix nightly job.
* Test(GODT-2742): Add more integration tests regarding drafts.
* GODT-2787: Force Scrollview to top when re-opening questions set.
* GODT-2787: Tweaking Bug Report form with last Review.
* Ci(GODT-2717): Create a job that will run on schedule.
* GODT-2787: Fix vertical alignement on CategoryItem.
* GODT-2842: Implement Bug Report Fallback notification.
* Chore(GODT-2848): Simplify User Event Service.
* GODT-2808: Apply comment from Bug Report content review.
* Test(GODT-2743): Sync high number of messages.
* GODT-2814: Standalone Server Manager.
* GODT-2808: Initial list of categories and questions.
* GODT-2787: Replace the PathTracker by a more visual NavigationIndicator.
* GODT-2816: Wait until mandatory fields are filled then fill body and title.
* GODT-2794: Clear cached answers when report is sent.
* GODT-2793: Feed the bug report body with the answered questions.
* GODT-2791: Parse the Bug Report Flow description file and ensure forward compatibility (GODT-2789).
* GODT-2821: Display questions in one page.
* GODT-2786: Init bug report flow description file.
* GODT-2792: Implement display of question set for bug report.
* Use qmlformat on qml files, and removed deprecated tests.
### Fixed
* GODT-2828: Fix negative report time.
* GODT-2828: Fix sync progress report after restart.
* GODT-2867: Do not crash on timeout or context cancel.
* GODT-2693: Duplicate messages in sent folder.
* GODT-2867: Get attachment returns API error on network problem.
* GODT-2805: Ignore Contact Group Labels.
* GODT-2866: Add 429/5xx Retry to Event Service.
* GODT-2855: Fix for text overlapping in settings view.
* Test: Verify leaks at end of WithEnv.
* Test: Fix event registration in TestBridge_SyncWithOngoingEvents.
* Test: Fix deadlock in chToType.
* GODT-2865: Add error on failed unlock.
* GODT-2857: Do not check changed values in clear recent flag.
* GODT-2827: Restore ticker to event poller.
* Test: TestBridge_SendAddTextBodyPartIfNotExists eventually fix.
* GODT-2813: Write new vault to temporary file first.
* GODT-2807: Fix issue where sessionID would not be removed from command-line on restart by bridge-gui.
* GODT-2687: Tabs after header field colon.
* GODT-2764: Allow perma-delete for messages which still have labels.
* GODT-2693: Fix message appearing twice after sent.
* GODT-2781: Try to remove stale lock file before failing in checkSingleInstance.
* GODT-2780: Fix 'QSystemTrayIcon::setVisible: No Icon set' warning in bridge-gui log on startup.
* GODT-2778: Fix login screen being disabled after an 'already logged in' error.
* Fix typos found by codespell.
* GODT-2577: Answered flag should only be applied to replied messages.
## Trift Bridge 3.4.1 ## Trift Bridge 3.4.1
### Fixed ### Fixed
@ -49,7 +134,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-2578: Refresh literals appended to Sent folder. * GODT-2578: Refresh literals appended to Sent folder.
* GODT-2753: Vault test now check that value auto-assigned is first available port. * GODT-2753: Vault test now check that value auto-assigned is first available port.
* GODT-2522: Handle migration with unreferenced db values. * GODT-2522: Handle migration with unreferenced db values.
* GODT-2693: Allow missing whitespace after header field colon. * GODT-2670: Allow missing whitespace after header field colon.
* GODT-2653: Only log when err is not nil. * GODT-2653: Only log when err is not nil.
* GODT-2680: Fix for C++ debugger not working on ARM64 because of OpenSSL 3.1. * GODT-2680: Fix for C++ debugger not working on ARM64 because of OpenSSL 3.1.
* GODT-2675: Update GPA to applye togin-gonic/gin patch + update COPYING_NOTES. * GODT-2675: Update GPA to applye togin-gonic/gin patch + update COPYING_NOTES.
@ -134,7 +219,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-2437: Silence harmless report to sentry. * GODT-2437: Silence harmless report to sentry.
* GODT-2649: Clean up cache files after failed connector create (Gluon). * GODT-2649: Clean up cache files after failed connector create (Gluon).
* GODT-2638: Validate messages before import. * GODT-2638: Validate messages before import.
* GODT-2646: Bump GPA and Gluon dependecy after CIRCL upgrade. * GODT-2646: Bump GPA and Gluon dependency after CIRCL upgrade.
* GODT-2454: Only Send status update if transaction succeeded. * GODT-2454: Only Send status update if transaction succeeded.
* Test: fix flaky tests. * Test: fix flaky tests.
* GODT-2628: Attempt to fix closed channel panic on logout. * GODT-2628: Attempt to fix closed channel panic on logout.
@ -194,7 +279,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-2574: Fix label/unlabel of large amounts of messages. * GODT-2574: Fix label/unlabel of large amounts of messages.
* GODT-2573: Handle invalid header fields in message. * GODT-2573: Handle invalid header fields in message.
* GODT-2573: Crash on null update. * GODT-2573: Crash on null update.
* GODT-2407: Replace invalid email addresses with emtpy for new Drafts. * GODT-2407: Replace invalid email addresses with empty for new Drafts.
## [Bridge 3.1.3] Quebec ## [Bridge 3.1.3] Quebec
@ -335,7 +420,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-2429: Do not report context cancel to sentry. * GODT-2429: Do not report context cancel to sentry.
### Fixed ### Fixed
* GODT-2467: elide long email adresses in 'bad event' QML notification dialog. * GODT-2467: elide long email addresses in 'bad event' QML notification dialog.
* GODT-2449: fix bug in Bridge-GUI's Exception::what(). * GODT-2449: fix bug in Bridge-GUI's Exception::what().
* GODT-2427: Parsing header issues. * GODT-2427: Parsing header issues.
* GODT-2426: Fix crash on user delete. * GODT-2426: Fix crash on user delete.
@ -352,7 +437,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-2404: Handle unexpected EOF. * GODT-2404: Handle unexpected EOF.
* GODT-2400: Allow state updates to be applied if command fails. * GODT-2400: Allow state updates to be applied if command fails.
* GODT-2399: Fix immediate message deletion during updates. * GODT-2399: Fix immediate message deletion during updates.
* GODT-2390: Missing changes from pervious commit. * GODT-2390: Missing changes from previous commit.
* GODT-2390: Add reports for uncaught json and net.opErr. * GODT-2390: Add reports for uncaught json and net.opErr.
* GODT-2414: Multiple deletion bug in WriteControlledStore. * GODT-2414: Multiple deletion bug in WriteControlledStore.
@ -417,7 +502,7 @@ GODT-1804: Preserve MIME parameters when uploading attachments.
* GODT-2223: Improve event handling. * GODT-2223: Improve event handling.
* GODT-2305: Detect missing gluon DB. * GODT-2305: Detect missing gluon DB.
* GODT-2291: Change gluon store default location from Cache to Data. * GODT-2291: Change gluon store default location from Cache to Data.
* Other: Disable dialer test until badssl cert is bumbed. * Other: Disable dialer test until badssl cert is bumped.
* GODT-2292: Updated BUILDS.md doc. * GODT-2292: Updated BUILDS.md doc.
* GODT-2258: suggest email as login when signing in via status window. * GODT-2258: suggest email as login when signing in via status window.
* Other: Report corrupt and/or insecure vaults to sentry. * Other: Report corrupt and/or insecure vaults to sentry.
@ -697,7 +782,7 @@ GODT-1804: Preserve MIME parameters when uploading attachments.
## [Bridge 2.4.6] Osney ## [Bridge 2.4.6] Osney
### Changed ### Changed
* GODT-2019: When signing out and a single user is connecte* we do not go back to the welcome screen. * GODT-2019: When signing out and a single user is connected we do not go back to the welcome screen.
* GODT-2071: Bridge-gui report error if an orphan bridge is detected. * GODT-2071: Bridge-gui report error if an orphan bridge is detected.
* GODT-2046: Bridge-gui log is included in optional archive sent with bug reports. * GODT-2046: Bridge-gui log is included in optional archive sent with bug reports.
* GODT-2039: Bridge monitors bridge-gui via its PID. * GODT-2039: Bridge monitors bridge-gui via its PID.
@ -851,7 +936,7 @@ GODT-1804: Preserve MIME parameters when uploading attachments.
* GODT-1260: Renaming. * GODT-1260: Renaming.
* GODT-1502: Rebranding: color and radius. * GODT-1502: Rebranding: color and radius.
* GODT-1549: Add notification when address list changes. * GODT-1549: Add notification when address list changes.
* GODT-1560: Dependecy licenses update and link. * GODT-1560: Dependency licenses update and link.
### Changed ### Changed
* GODT-1543: Using one buffered event for off and on connection. * GODT-1543: Using one buffered event for off and on connection.
@ -948,7 +1033,7 @@ GODT-1537: Manual in-app update mechanism.
* GODT-1338: GODT-1343 Help view buttons. * GODT-1338: GODT-1343 Help view buttons.
* GODT-1340: Not crashing, user list updating in main thread. * GODT-1340: Not crashing, user list updating in main thread.
* GODT-1345: Adding panic handlers. * GODT-1345: Adding panic handlers.
* GODT-1271: Fix Status margings. * GODT-1271: Fix Status margins.
* GODT-1320: Add loading property to each action within a notification. * GODT-1320: Add loading property to each action within a notification.
* GODT-1210: Add "free user" banner. * GODT-1210: Add "free user" banner.
* GODT-1314: Limit description field length within 150/800 bounds. * GODT-1314: Limit description field length within 150/800 bounds.
@ -990,7 +1075,7 @@ GODT-1537: Manual in-app update mechanism.
* GODT-1381 Treat readonly folder as failure for cache on disk. * GODT-1381 Treat readonly folder as failure for cache on disk.
* GODT-1431 Prevent watcher when not using disk on cache. * GODT-1431 Prevent watcher when not using disk on cache.
* GODT-1381: Use in-memory cache in case local cache is unavailable. * GODT-1381: Use in-memory cache in case local cache is unavailable.
* GODT-1356 GODT-1302: Cache on disk concurency and API retries. * GODT-1356 GODT-1302: Cache on disk concurrency and API retries.
* GODT-1332 Added tests for cache move functions. * GODT-1332 Added tests for cache move functions.
* GODT-1332: moved cache related functions to separate file. * GODT-1332: moved cache related functions to separate file.
* GODT-1332 moving cache does not work on Windows. * GODT-1332 moving cache does not work on Windows.
@ -1241,7 +1326,7 @@ GODT-1537: Manual in-app update mechanism.
### Fixed ### Fixed
* GODT-1029 Fix tray icon not updating under certain conditions. * GODT-1029 Fix tray icon not updating under certain conditions.
* GODT-1062 Fix lost notification bar when window is closed. * GODT-1062 Fix lost notification bar when window is closed.
* GODT-1058 Install version after chaning channel right away only in case of downgrade. * GODT-1058 Install version after changing channel right away only in case of downgrade.
* GODT-1073 Re-write autostart link on every start if turned on in preferences. * GODT-1073 Re-write autostart link on every start if turned on in preferences.
* GODT-1055 Fix flaky empty trash test. * GODT-1055 Fix flaky empty trash test.
@ -1331,7 +1416,7 @@ GODT-1537: Manual in-app update mechanism.
* GODT-820 Added GUI notification on impossibility of update installation (both silent and manual). * GODT-820 Added GUI notification on impossibility of update installation (both silent and manual).
* GODT-870 Added GUI notification on error during silent update. * GODT-870 Added GUI notification on error during silent update.
* GODT-805 Added GUI notification on update available. * GODT-805 Added GUI notification on update available.
* GODT-804 Added GUI notification on silent update installed (promt to restart). * GODT-804 Added GUI notification on silent update installed (prompt to restart).
* GODT-275 Added option to disable autoupdates in settings (default autoupdate is enabled). * GODT-275 Added option to disable autoupdates in settings (default autoupdate is enabled).
* GODT-874 Added manual triggers to Updater module. * GODT-874 Added manual triggers to Updater module.
* GODT-851 Added support of UID EXPUNGE. * GODT-851 Added support of UID EXPUNGE.
@ -1655,7 +1740,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
### Changed ### Changed
* GODT-360 Detect charset embedded in html/xml. * GODT-360 Detect charset embedded in html/xml.
* GODT-354 Do not label/unlabel messsages from `All Mail` folder. * GODT-354 Do not label/unlabel messages from `All Mail` folder.
* GODT-388 Support for both bridge and import/export credentials by package users. * GODT-388 Support for both bridge and import/export credentials by package users.
* GODT-387 Store factory to make store optional. * GODT-387 Store factory to make store optional.
* GODT-386 Renamed bridge to general users and keep bridge only for bridge stuff. * GODT-386 Renamed bridge to general users and keep bridge only for bridge stuff.
@ -1820,13 +1905,13 @@ CSB-331 Fix sending error due to mixed case in sender address.
* GODT-88 Run mbox sync in parallel when switch password mode (re-init not user). * GODT-88 Run mbox sync in parallel when switch password mode (re-init not user).
* GODT-95 Do not throw error when trying to create new mailbox in IMAP root. * GODT-95 Do not throw error when trying to create new mailbox in IMAP root.
* GODT-75 Do not fail on unlabel inside delete. * GODT-75 Do not fail on unlabel inside delete.
* #1095 always delete IMAP USER including wrong pasword. * #1095 always delete IMAP USER including wrong password.
* Unique pmapi client userID (including #1098). * Unique pmapi client userID (including #1098).
* Using go.enmime@v0.6.1 snapshot. * Using go.enmime@v0.6.1 snapshot.
* Better detection of non-auth-error. * Better detection of non-auth-error.
* Reset `hasAuthChannel` during logout for proper login functionality (set up auth channel and unlock keys). * Reset `hasAuthChannel` during logout for proper login functionality (set up auth channel and unlock keys).
* Allow `APPEND` messages without parsable email address in sender field. * Allow `APPEND` messages without parsable email address in sender field.
* #1060 avoid `Append` after internal message ID was found and message was copyed to mailbox using `MessageLabel`. * #1060 avoid `Append` after internal message ID was found and message was copied to mailbox using `MessageLabel`.
* #1049 Basic usage of store in SMTP package to poll event loop during sending message. * #1049 Basic usage of store in SMTP package to poll event loop during sending message.
* #1050 pollNow waits for events to be processed. * #1050 pollNow waits for events to be processed.
* #1047 Fix fetch of empty mailbox. * #1047 Fix fetch of empty mailbox.
@ -1952,7 +2037,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
* #903 added http.Client timeout to not hang out forever. * #903 added http.Client timeout to not hang out forever.
* Closing body after checking internet connection. * Closing body after checking internet connection.
* Pedantic lint for bridgeUtils. * Pedantic lint for bridgeUtils.
* Selected events are buffered and emited again when frontend loop is ready. * Selected events are buffered and emitted again when frontend loop is ready.
* #890 implemented 2FA endpoint (auth split). * #890 implemented 2FA endpoint (auth split).
* #888 TLS Cert. * #888 TLS Cert.
* Error bar and modal with explanation in GUI. * Error bar and modal with explanation in GUI.
@ -1960,7 +2045,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
* Add pinning to bridge (only for live API builds). * Add pinning to bridge (only for live API builds).
* #887 #883: * #887 #883:
* Wait before clearing data. * Wait before clearing data.
* Configer which provides pmapi.ClientConfig and app directories. * Configure which provides pmapi.ClientConfig and app directories.
* #861 restart after clear data. * #861 restart after clear data.
* Panic handler for all goroutines. * Panic handler for all goroutines.
* CD for linux. * CD for linux.
@ -2008,7 +2093,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
* #882 unassign PMAPI client after logout and force to run garbage collector. * #882 unassign PMAPI client after logout and force to run garbage collector.
* #880, #884, #885, #886 fix of informing user about outgoing non-encrypted e-mail. * #880, #884, #885, #886 fix of informing user about outgoing non-encrypted e-mail.
* #838 `Sirupsen` -> `sirupsen`. * #838 `Sirupsen` -> `sirupsen`.
* #893 save panic report file everytime. * #893 save panic report file every time.
* #880 fix of informing user about outgoing non-encrypted e-mail. * #880 fix of informing user about outgoing non-encrypted e-mail.
* Fix aliases in split mode. * Fix aliases in split mode.
* Fix decrypted data in log notification. * Fix decrypted data in log notification.
@ -2082,7 +2167,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
### Changed ### Changed
* Fix custom message format. * Fix custom message format.
* #802 acumulated long lines while parsing body structure. * #802 accumulated long lines while parsing body structure.
* Process `AddressEvent` before `MessageEvent`. * Process `AddressEvent` before `MessageEvent`.
* #791 updated crypto: fix wrong signature format. * #791 updated crypto: fix wrong signature format.
* #793 fix returning size. * #793 fix returning size.
@ -2104,7 +2189,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
### Changed ### Changed
* #748 when charset missing assume utf8 and check the validity. * #748 when charset missing assume utf8 and check the validity.
* #750 before sync check that events are uptodate, if not poll events instead of sync. * #750 before sync check that events are up-to-date, if not poll events instead of sync.
* Use pmapi with support of decrypted access token. * Use pmapi with support of decrypted access token.
* #750 Status is using DB status instead of API. * #750 Status is using DB status instead of API.
* Format panic error as string instead of struct dump. * Format panic error as string instead of struct dump.
@ -2121,7 +2206,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
* Full version of program visible on release notes. * Full version of program visible on release notes.
### Changed ### Changed
* #720 only one concurent DB sync. * #720 only one concurrent DB sync.
* #720 sync every 3 pages. * #720 sync every 3 pages.
* #512 extending list of charsets go-pm-mime!4. * #512 extending list of charsets go-pm-mime!4.
@ -2145,7 +2230,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
* Fix srp modulus issue with new `ProtonMail/crypto`. * Fix srp modulus issue with new `ProtonMail/crypto`.
* Generate version files from main file. * Generate version files from main file.
* Be able to set update set on build. * Be able to set update set on build.
* #597 check on start that certificat will be still valid after one month and generate new cert if not. * #597 check on start that certificate will be still valid after one month and generate new cert if not.
* #597 extended certificate validity to 2 years. * #597 extended certificate validity to 2 years.
* Copyright 2019. * Copyright 2019.
* Exclude `protontech` repos from credits. * Exclude `protontech` repos from credits.
@ -2164,7 +2249,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
* #592 internal references are added only when not present already. * #592 internal references are added only when not present already.
* #592 field `Date` changed to m.Time only when wrong format or missing `Date`. * #592 field `Date` changed to m.Time only when wrong format or missing `Date`.
* #645 pmapi#26 `Message.Flags` instead of `IsEncrypted`, `Type`, `IsReplied`, `IsRepliedAll`, `IsForwarded`. * #645 pmapi#26 `Message.Flags` instead of `IsEncrypted`, `Type`, `IsReplied`, `IsRepliedAll`, `IsForwarded`.
* DB: do not allow to put Body or Attachements to db. * DB: do not allow to put Body or Attachments to db.
* #574 SMTP: can now send more than one email. * #574 SMTP: can now send more than one email.
* #671 Verbosity levels: `debug` (only bridge), `debug-client` (bridge and client communication), `debug-server` (bridge, whole SMTP/IMAP communication). * #671 Verbosity levels: `debug` (only bridge), `debug-client` (bridge and client communication), `debug-server` (bridge, whole SMTP/IMAP communication).
* #644 Return rfc.size 0 or correct size of fetched body (stored in DB). * #644 Return rfc.size 0 or correct size of fetched body (stored in DB).
@ -2236,7 +2321,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
* Start with new versioning. * Start with new versioning.
1.1.0 1.1.0
| | `--- bug fix number (internal, irregular, beta relases) | | `--- bug fix number (internal, irregular, beta releases)
| `----- minor version (features, release once per month, live release, milestones) | `----- minor version (features, release once per month, live release, milestones)
`------- major version (big changes, once per year, breaking changes, api force upgrade) `------- major version (big changes, once per year, breaking changes, api force upgrade)
@ -2302,7 +2387,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
* All `client.Do` errors are interpreted as connection issue. * All `client.Do` errors are interpreted as connection issue.
* Moved to internal gitlab. * Moved to internal gitlab.
* Typo `frontend-qml`. * Typo `frontend-qml`.
* Better message for case when server is not reacheable. * Better message for case when server is not reachable.
* Setting 1min timeout to IMAP connection. * Setting 1min timeout to IMAP connection.
### Changed ### Changed
@ -2334,12 +2419,12 @@ CSB-331 Fix sending error due to mixed case in sender address.
* Keychain format and function refactor. * Keychain format and function refactor.
* Create crash file on panic with full trace. * Create crash file on panic with full trace.
* Clear old data only in main process (no double keychain typing). * Clear old data only in main process (no double keychain typing).
* Create label udpate API route. * Create label update API route.
* Selectable text in release notes. * Selectable text in release notes.
### Added ### Added
* Support sending to external PGP recipients. * Support sending to external PGP recipients.
* Return error codes: `0: Ok`, `2: Frontend crashed`, `3: Bridge already running`, `4: Uknown argument`, `42: Restart application`. * Return error codes: `0: Ok`, `2: Frontend crashed`, `3: Bridge already running`, `4: Unknown argument`, `42: Restart application`.
### Release notes ### Release notes
* Support of encryption to external PGP recipients using contacts created on beta.protonmail.com (see https://protonmail.com/blog/pgp-vulnerability-efail/ to understand the vulnerabilities that may be associated with sending to other PGP clients). * Support of encryption to external PGP recipients using contacts created on beta.protonmail.com (see https://protonmail.com/blog/pgp-vulnerability-efail/ to understand the vulnerabilities that may be associated with sending to other PGP clients).
@ -2364,7 +2449,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
* Bug report window. * Bug report window.
* Checkbox and with label (only I/E). * Checkbox and with label (only I/E).
* Error dialog and Info tooltip (only I/E). * Error dialog and Info tooltip (only I/E).
* Add user modal formating (colors, text). * Add user modal formatting (colors, text).
* Account view style. * Account view style.
* Input box style (used in bug report). * Input box style (used in bug report).
* Input field style (used in add account and change port). * Input field style (used in add account and change port).

View File

@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
.PHONY: build build-gui build-nogui build-launcher versioner hasher .PHONY: build build-gui build-nogui build-launcher versioner hasher
# Keep version hardcoded so app build works also without Git repository. # Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=3.4.1+git BRIDGE_APP_VERSION?=3.5.1+git
APP_VERSION:=${BRIDGE_APP_VERSION} APP_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG APP_VENDOR:=Proton AG
@ -246,7 +246,7 @@ test-race: gofiles
test-integration: gofiles test-integration: gofiles
mkdir -p coverage/integration mkdir -p coverage/integration
go test \ go test \
-v -timeout=60m -p=1 -count=1 \ -v -timeout=60m -p=1 -count=1 -tags=test_integration \
${GOCOVERAGE} \ ${GOCOVERAGE} \
github.com/ProtonMail/proton-bridge/v3/tests \ github.com/ProtonMail/proton-bridge/v3/tests \
${GOCOVERDIR}/integration ${GOCOVERDIR}/integration
@ -258,6 +258,22 @@ test-integration-debug: gofiles
test-integration-race: gofiles test-integration-race: gofiles
go test -v -timeout=60m -p=1 -count=1 -race -failfast github.com/ProtonMail/proton-bridge/v3/tests go test -v -timeout=60m -p=1 -count=1 -race -failfast github.com/ProtonMail/proton-bridge/v3/tests
test-integration-nightly: gofiles
mkdir -p coverage/integration
go test \
-v -timeout=90m -p=1 -count=1 -tags=test_integration \
${GOCOVERAGE} \
github.com/ProtonMail/proton-bridge/v3/tests \
${GOCOVERDIR}/integration \
nightly
fuzz: gofiles
go test -fuzz=FuzzUnmarshal -parallel=4 -fuzztime=60s $(PWD)/internal/legacy/credentials
go test -fuzz=FuzzNewParser -parallel=4 -fuzztime=60s $(PWD)/pkg/message/parser
go test -fuzz=FuzzReadHeaderBody -parallel=4 -fuzztime=60s $(PWD)/pkg/message
go test -fuzz=FuzzDecodeHeader -parallel=4 -fuzztime=60s $(PWD)/pkg/mime
go test -fuzz=FuzzDecodeCharset -parallel=4 -fuzztime=60s $(PWD)/pkg/mime
bench: bench:
go test -run '^$$' -bench=. -memprofile bench_mem.pprof -cpuprofile bench_cpu.pprof ./internal/store go test -run '^$$' -bench=. -memprofile bench_mem.pprof -cpuprofile bench_cpu.pprof ./internal/store
go tool pprof -png -output bench_mem.png bench_mem.pprof go tool pprof -png -output bench_mem.png bench_mem.pprof
@ -274,9 +290,22 @@ mocks:
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/telemetry HeartbeatManager > internal/telemetry/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/telemetry HeartbeatManager > internal/telemetry/mocks/mocks.go
cp internal/telemetry/mocks/mocks.go internal/bridge/mocks/telemetry_mocks.go cp internal/telemetry/mocks/mocks.go internal/bridge/mocks/telemetry_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/user MessageDownloader > internal/user/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/services/userevents \
EventSource,EventIDStore > internal/services/userevents/mocks/mocks.go
mockgen --package userevents github.com/ProtonMail/proton-bridge/v3/internal/services/userevents \
EventSubscriber,MessageEventHandler,LabelEventHandler,AddressEventHandler,RefreshEventHandler,UserEventHandler,UserUsedSpaceEventHandler > tmp
mv tmp internal/services/userevents/mocks_test.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/events EventPublisher \
> internal/events/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/services/useridentity IdentityProvider,Telemetry \
> internal/services/useridentity/mocks/mocks.go
mockgen --self_package "github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice" -package syncservice github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice \
ApplyStageInput,BuildStageInput,BuildStageOutput,DownloadStageInput,DownloadStageOutput,MetadataStageInput,MetadataStageOutput,\
StateProvider,Regulator,UpdateApplier,MessageBuilder,APIClient,Reporter,DownloadRateModifier \
> tmp
mv tmp internal/services/syncservice/mocks_test.go
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog lint: gofiles lint-golang lint-license lint-dependencies lint-changelog lint-bug-report
lint-license: lint-license:
./utils/missing_license.sh check ./utils/missing_license.sh check
@ -292,6 +321,12 @@ lint-golang:
$(info linting with GOMAXPROCS=${GOMAXPROCS}) $(info linting with GOMAXPROCS=${GOMAXPROCS})
golangci-lint run ./... golangci-lint run ./...
lint-bug-report:
python3 utils/validate_bug_report_file.py --file "internal/frontend/bridge-gui/bridge-gui/qml/Resources/bug_report_flow.json"
lint-bug-report-preview:
python3 utils/validate_bug_report_file.py --file "internal/frontend/bridge-gui/bridge-gui/qml/Resources/bug_report_flow.json" --preview
gobinsec: gobinsec-cache.yml build gobinsec: gobinsec-cache.yml build
gobinsec -wait -cache -config utils/gobinsec_conf.yml ${EXE_TARGET} ${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE} gobinsec -wait -cache -config utils/gobinsec_conf.yml ${EXE_TARGET} ${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE}

View File

@ -2,13 +2,13 @@
## First login and sync ## First login and sync
When user logs in to the bridge for the first time, immediatelly starts the first sync. When user logs in to the bridge for the first time, immediately starts the first sync.
First sync downloads all headers of all e-mails and creates database to have proper UIDs First sync downloads all headers of all e-mails and creates database to have proper UIDs
and indexes for IMAP. See [database](database.md) for more information. and indexes for IMAP. See [database](database.md) for more information.
By default, whenever it's possible, sync downloads only all e-mails maiblox which already By default, whenever it's possible, sync downloads only all e-mails maiblox which already
have list of labels so we can construct all mailboxes (inbox, sent, trash, custom folders have list of labels so we can construct all mailboxes (inbox, sent, trash, custom folders
and lables) without need to download each e-mail headers many times. and labels) without need to download each e-mail headers many times.
Note that we need to download also bodies to calculate size of the e-mail and set proper Note that we need to download also bodies to calculate size of the e-mail and set proper
content type (clients uses content type for guess if e-mail contains attachment)--but only content type (clients uses content type for guess if e-mail contains attachment)--but only
@ -22,7 +22,7 @@ client right after adding account.
When account is added to client, client start the sync. This sync will ask Bridge app When account is added to client, client start the sync. This sync will ask Bridge app
for all headers (done quickly) and then starts to download all bodies and attachment. for all headers (done quickly) and then starts to download all bodies and attachment.
Unfortunatelly for some e-mail more than once if the same e-mail is in more mailboxes Unfortunately for some e-mail more than once if the same e-mail is in more mailboxes
(e.g. inbox and all mail)--there is no way to tell over IMAP it's the same message. (e.g. inbox and all mail)--there is no way to tell over IMAP it's the same message.
After successful login of client to IMAP, Bridge starts event loop. That periodicly ask After successful login of client to IMAP, Bridge starts event loop. That periodicly ask
@ -37,7 +37,7 @@ sequenceDiagram
Note right of B: Set up PM account<br/>by user Note right of B: Set up PM account<br/>by user
loop First sync loop First sync
B ->> S: Fetch body and attachements B ->> S: Fetch body and attachments
Note right of B: Build local database<br/>(e-mail UIDs) Note right of B: Build local database<br/>(e-mail UIDs)
end end
@ -58,8 +58,8 @@ sequenceDiagram
C ->> B: IMAP SELECT directory C ->> B: IMAP SELECT directory
C ->> B: IMAP SEARCH e-mails UIDs C ->> B: IMAP SEARCH e-mails UIDs
C ->> B: IMAP FETCH of e-mail UID C ->> B: IMAP FETCH of e-mail UID
B ->> S: Fetch body and attachements B ->> S: Fetch body and attachments
Note right of B: Decrypt message<br/>and attachement Note right of B: Decrypt message<br/>and attachment
B ->> C: IMAP response B ->> C: IMAP response
end end
``` ```

View File

@ -1,12 +1,12 @@
# Update mechanism of Bridge # Update mechanism of Bridge
There are mulitple options how to change version of application: There are multiple options how to change version of application:
* Automatic in-app update * Automatic in-app update
* Manual in-app update * Manual in-app update
* Manual install * Manual install
In-app update ends with restarting bridge into new version. Automatic in-app In-app update ends with restarting bridge into new version. Automatic in-app
update is downloading, verifying and installing the new version immediatelly update is downloading, verifying and installing the new version immediately
without user confirmation. For manual in-app update user needs to confirm first. without user confirmation. For manual in-app update user needs to confirm first.
Update is done from special update file published on website. Update is done from special update file published on website.
@ -25,7 +25,7 @@ The bridge is installed and executed differently for given OS:
* macOS app does not use launcher * macOS app does not use launcher
* No launcher, only one executable * No launcher, only one executable
* In-App udpate replaces the bridge files in installation path directly * In-App update replaces the bridge files in installation path directly
```mermaid ```mermaid

4
go.mod
View File

@ -5,9 +5,9 @@ go 1.20
require ( require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.2.0 github.com/Masterminds/semver/v3 v3.2.0
github.com/ProtonMail/gluon v0.16.1-0.20230808094407-85a10f17ae92 github.com/ProtonMail/gluon v0.17.1-0.20230829112217-5d5c25c504b5
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.4.1-0.20230727082922-9115b4750ec7 github.com/ProtonMail/go-proton-api v0.4.1-0.20230831064234-0e3a549b3f36
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton
github.com/PuerkitoBio/goquery v1.8.1 github.com/PuerkitoBio/goquery v1.8.1
github.com/abiosoft/ishell v2.0.0+incompatible github.com/abiosoft/ishell v2.0.0+incompatible

8
go.sum
View File

@ -23,8 +23,8 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo= github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk= github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g= github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/gluon v0.16.1-0.20230808094407-85a10f17ae92 h1:yoaUatxdB6EXChiWdfIBpasJJxrQ6dHJknG0hwBAqmQ= github.com/ProtonMail/gluon v0.17.1-0.20230829112217-5d5c25c504b5 h1:C/8P5NHAKi2yCKez+OZ5rSR8SsL7k8si4pK4SE2QtV8=
github.com/ProtonMail/gluon v0.16.1-0.20230808094407-85a10f17ae92/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo= github.com/ProtonMail/gluon v0.17.1-0.20230829112217-5d5c25c504b5/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
@ -35,8 +35,8 @@ github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4= github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230727082922-9115b4750ec7 h1:Rmg3TPK6vFGNWR4hxmPoBhV75Sl716iB46wEi2U4Q+c= github.com/ProtonMail/go-proton-api v0.4.1-0.20230831064234-0e3a549b3f36 h1:JVMK2w90bCWayUCXJIb3wkQ5+j2P/NbnrX3BrDoLzsc=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230727082922-9115b4750ec7/go.mod h1:+aTJoYu8bqzGECXL2DOdiZTZ64bGn3w0NC8VcFpJrFM= github.com/ProtonMail/go-proton-api v0.4.1-0.20230831064234-0e3a549b3f36/go.mod h1:nS8hMGjJLgC0Iej0JMYbsI388LesEkM1Hj/jCCxQeaQ=
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI= github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk= github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc= github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc=

View File

@ -52,6 +52,9 @@ const (
flagCPUProfile = "cpu-prof" flagCPUProfile = "cpu-prof"
flagCPUProfileShort = "p" flagCPUProfileShort = "p"
flagTraceProfile = "trace-prof"
flagTraceProfileShort = "t"
flagMemProfile = "mem-prof" flagMemProfile = "mem-prof"
flagMemProfileShort = "m" flagMemProfileShort = "m"
@ -96,6 +99,11 @@ func New() *cli.App {
Aliases: []string{flagCPUProfileShort}, Aliases: []string{flagCPUProfileShort},
Usage: "Generate CPU profile", Usage: "Generate CPU profile",
}, },
&cli.BoolFlag{
Name: flagTraceProfile,
Aliases: []string{flagTraceProfileShort},
Usage: "Generate Trace profile",
},
&cli.BoolFlag{ &cli.BoolFlag{
Name: flagMemProfile, Name: flagMemProfile,
Aliases: []string{flagMemProfileShort}, Aliases: []string{flagMemProfileShort},
@ -379,6 +387,11 @@ func withProfiler(c *cli.Context, fn func() error) error {
defer profile.Start(profile.CPUProfile, profile.ProfilePath(".")).Stop() defer profile.Start(profile.CPUProfile, profile.ProfilePath(".")).Stop()
} }
if c.Bool(flagTraceProfile) {
logrus.Debug("Running with Trace profiling")
defer profile.Start(profile.TraceProfile, profile.ProfilePath(".")).Stop()
}
if c.Bool(flagMemProfile) { if c.Bool(flagMemProfile) {
logrus.Debug("Running with memory profiling") logrus.Debug("Running with memory profiling")
defer profile.Start(profile.MemProfile, profile.MemProfileAllocs, profile.ProfilePath(".")).Stop() defer profile.Start(profile.MemProfile, profile.MemProfileAllocs, profile.ProfilePath(".")).Stop()

View File

@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
//go:build !build_qa //go:build !build_qa && !test_integration
package bridge package bridge

View File

@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
//go:build build_qa //go:build build_qa || test_integration
package bridge package bridge

View File

@ -23,7 +23,6 @@ import (
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
"net"
"net/http" "net/http"
"sync" "sync"
"time" "time"
@ -38,8 +37,11 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/focus" "github.com/ProtonMail/proton-bridge/v3/internal/focus"
"github.com/ProtonMail/proton-bridge/v3/internal/identifier"
"github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/sentry" "github.com/ProtonMail/proton-bridge/v3/internal/sentry"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry" "github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
"github.com/ProtonMail/proton-bridge/v3/internal/user" "github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
@ -59,7 +61,7 @@ type Bridge struct {
// api manages user API clients. // api manages user API clients.
api *proton.Manager api *proton.Manager
proxyCtl ProxyController proxyCtl ProxyController
identifier Identifier identifier identifier.Identifier
// tlsConfig holds the bridge TLS config used by the IMAP and SMTP servers. // tlsConfig holds the bridge TLS config used by the IMAP and SMTP servers.
tlsConfig *tls.Config tlsConfig *tls.Config
@ -125,9 +127,8 @@ type Bridge struct {
// goHeartbeat triggers a check/sending if heartbeat is needed. // goHeartbeat triggers a check/sending if heartbeat is needed.
goHeartbeat func() goHeartbeat func()
uidValidityGenerator imap.UIDValidityGenerator serverManager *imapsmtpserver.Service
syncService *syncservice.Service
serverManager *ServerManager
} }
// New creates a new bridge. // New creates a new bridge.
@ -140,7 +141,7 @@ func New(
apiURL string, // the URL of the API to use apiURL string, // the URL of the API to use
cookieJar http.CookieJar, // the cookie jar to use cookieJar http.CookieJar, // the cookie jar to use
identifier Identifier, // the identifier to keep track of the user agent identifier identifier.Identifier, // the identifier to keep track of the user agent
tlsReporter TLSReporter, // the TLS reporter to report TLS errors tlsReporter TLSReporter, // the TLS reporter to report TLS errors
roundTripper http.RoundTripper, // the round tripper to use for API requests roundTripper http.RoundTripper, // the round tripper to use for API requests
proxyCtl ProxyController, // the DoH controller proxyCtl ProxyController, // the DoH controller
@ -207,7 +208,7 @@ func newBridge(
reporter reporter.Reporter, reporter reporter.Reporter,
api *proton.Manager, api *proton.Manager,
identifier Identifier, identifier identifier.Identifier,
proxyCtl ProxyController, proxyCtl ProxyController,
uidValidityGenerator imap.UIDValidityGenerator, uidValidityGenerator imap.UIDValidityGenerator,
@ -269,17 +270,26 @@ func newBridge(
firstStart: firstStart, firstStart: firstStart,
lastVersion: lastVersion, lastVersion: lastVersion,
tasks: tasks, tasks: tasks,
syncService: syncservice.NewService(reporter, panicHandler),
uidValidityGenerator: uidValidityGenerator,
serverManager: newServerManager(),
} }
if err := bridge.serverManager.Init(bridge); err != nil { bridge.serverManager = imapsmtpserver.NewService(context.Background(),
&bridgeSMTPSettings{b: bridge},
&bridgeIMAPSettings{b: bridge},
&bridgeEventPublisher{b: bridge},
panicHandler,
reporter,
uidValidityGenerator,
&bridgeIMAPSMTPTelemetry{b: bridge},
)
if err := bridge.serverManager.Init(context.Background(), bridge.tasks, &bridgeEventSubscription{b: bridge}); err != nil {
return nil, err return nil, err
} }
bridge.syncService.Run(bridge.tasks)
return bridge, nil return bridge, nil
} }
@ -407,11 +417,6 @@ func (bridge *Bridge) GetErrors() []error {
func (bridge *Bridge) Close(ctx context.Context) { func (bridge *Bridge) Close(ctx context.Context) {
logrus.Info("Closing bridge") logrus.Info("Closing bridge")
// Close the servers
if err := bridge.serverManager.CloseServers(ctx); err != nil {
logrus.WithError(err).Error("Failed to close servers")
}
// Close all users. // Close all users.
safe.Lock(func() { safe.Lock(func() {
for _, user := range bridge.users { for _, user := range bridge.users {
@ -419,6 +424,11 @@ func (bridge *Bridge) Close(ctx context.Context) {
} }
}, bridge.usersLock) }, bridge.usersLock)
// Close the servers
if err := bridge.serverManager.CloseServers(ctx); err != nil {
logrus.WithError(err).Error("Failed to close servers")
}
// Stop all ongoing tasks. // Stop all ongoing tasks.
bridge.tasks.CancelAndWait() bridge.tasks.CancelAndWait()
@ -527,24 +537,6 @@ func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
}, nil }, nil
} }
func newListener(port int, useTLS bool, tlsConfig *tls.Config) (net.Listener, error) {
if useTLS {
tlsListener, err := tls.Listen("tcp", fmt.Sprintf("%v:%v", constants.Host, port), tlsConfig)
if err != nil {
return nil, err
}
return tlsListener, nil
}
netListener, err := net.Listen("tcp", fmt.Sprintf("%v:%v", constants.Host, port))
if err != nil {
return nil, err
}
return netListener, nil
}
func min(a, b time.Duration) time.Duration { func min(a, b time.Duration) time.Duration {
if a < b { if a < b {
return a return a

View File

@ -44,6 +44,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/focus" "github.com/ProtonMail/proton-bridge/v3/internal/focus"
"github.com/ProtonMail/proton-bridge/v3/internal/locations" "github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
"github.com/ProtonMail/proton-bridge/v3/internal/updater" "github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/internal/user" "github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent" "github.com/ProtonMail/proton-bridge/v3/internal/useragent"
@ -54,6 +55,7 @@ import (
"github.com/emersion/go-sasl" "github.com/emersion/go-sasl"
"github.com/emersion/go-smtp" "github.com/emersion/go-smtp"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/goleak"
) )
var ( var (
@ -621,6 +623,10 @@ func TestBridge_AddressWithoutKeys(t *testing.T) {
defer m.Close() defer m.Close()
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// Watch for sync finished event.
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done()
// Create a user which will have an address without keys. // Create a user which will have an address without keys.
userID, _, err := s.CreateUser("nokeys", []byte("password")) userID, _, err := s.CreateUser("nokeys", []byte("password"))
require.NoError(t, err) require.NoError(t, err)
@ -641,10 +647,6 @@ func TestBridge_AddressWithoutKeys(t *testing.T) {
// Remove the address keys. // Remove the address keys.
require.NoError(t, s.RemoveAddressKey(userID, aliasAddrID, aliasAddr.Keys[0].ID)) require.NoError(t, s.RemoveAddressKey(userID, aliasAddrID, aliasAddr.Keys[0].ID))
// Watch for sync finished event.
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done()
// We should be able to log the user in. // We should be able to log the user in.
require.NoError(t, getErr(bridge.LoginFull(context.Background(), "nokeys", []byte("password"), nil, nil))) require.NoError(t, getErr(bridge.LoginFull(context.Background(), "nokeys", []byte("password"), nil, nil)))
require.NoError(t, err) require.NoError(t, err)
@ -700,10 +702,10 @@ func TestBridge_InitGluonDirectory(t *testing.T) {
configDir, err := b.GetGluonDataDir() configDir, err := b.GetGluonDataDir()
require.NoError(t, err) require.NoError(t, err)
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(b.GetGluonCacheDir())) _, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
require.False(t, os.IsNotExist(err)) require.False(t, os.IsNotExist(err))
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir)) _, err = os.ReadDir(imapsmtpserver.ApplyGluonConfigPathSuffix(configDir))
require.False(t, os.IsNotExist(err)) require.False(t, os.IsNotExist(err))
}) })
}) })
@ -776,16 +778,16 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Old store should no more exists. // Old store should no more exists.
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(currentCacheDir)) _, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(currentCacheDir))
require.True(t, os.IsNotExist(err)) require.True(t, os.IsNotExist(err))
// Database should not have changed. // Database should not have changed.
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir)) _, err = os.ReadDir(imapsmtpserver.ApplyGluonConfigPathSuffix(configDir))
require.False(t, os.IsNotExist(err)) require.False(t, os.IsNotExist(err))
// New path should have Gluon sub-folder. // New path should have Gluon sub-folder.
require.Equal(t, filepath.Join(newCacheDir, "gluon"), b.GetGluonCacheDir()) require.Equal(t, filepath.Join(newCacheDir, "gluon"), b.GetGluonCacheDir())
// And store should be inside it. // And store should be inside it.
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(b.GetGluonCacheDir())) _, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
require.False(t, os.IsNotExist(err)) require.False(t, os.IsNotExist(err))
// We should be able to fetch. // We should be able to fetch.
@ -873,6 +875,9 @@ func TestBridge_ChangeAddressOrder(t *testing.T) {
// withEnv creates the full test environment and runs the tests. // withEnv creates the full test environment and runs the tests.
func withEnv(t *testing.T, tests func(context.Context, *server.Server, *proton.NetCtl, bridge.Locator, []byte), opts ...server.Option) { func withEnv(t *testing.T, tests func(context.Context, *server.Server, *proton.NetCtl, bridge.Locator, []byte), opts ...server.Option) {
opt := goleak.IgnoreCurrent()
defer goleak.VerifyNone(t, opt)
server := server.New(opts...) server := server.New(opts...)
defer server.Close() defer server.Close()
@ -1057,6 +1062,7 @@ func getConnectedUserIDs(t *testing.T, b *bridge.Bridge) []string {
func chToType[In, Out any](inCh <-chan In, done func()) (<-chan Out, func()) { func chToType[In, Out any](inCh <-chan In, done func()) (<-chan Out, func()) {
outCh := make(chan Out) outCh := make(chan Out)
ctx, cancel := context.WithCancel(context.Background())
go func() { go func() {
defer close(outCh) defer close(outCh)
@ -1066,11 +1072,19 @@ func chToType[In, Out any](inCh <-chan In, done func()) (<-chan Out, func()) {
panic(fmt.Sprintf("unexpected type %T", in)) panic(fmt.Sprintf("unexpected type %T", in))
} }
outCh <- out select {
case <-ctx.Done():
return
case outCh <- out:
}
} }
}() }()
return outCh, done return outCh, func() {
cancel()
done()
}
} }
type eventWaiter struct { type eventWaiter struct {

View File

@ -33,7 +33,7 @@ const (
DefaultMaxSessionCountForBugReport = 10 DefaultMaxSessionCountForBugReport = 10
) )
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error { func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, title, description, username, email, client string, attachLogs bool) error {
var account string var account string
if info, err := bridge.QueryUserInfo(username); err == nil { if info, err := bridge.QueryUserInfo(username); err == nil {
@ -82,7 +82,7 @@ func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, descript
OS: osType, OS: osType,
OSVersion: osVersion, OSVersion: osVersion,
Title: "[Bridge] Bug", Title: "[Bridge] Bug - " + title,
Description: description, Description: description,
Client: client, Client: client,

View File

@ -79,7 +79,7 @@ func (bridge *Bridge) CheckClientState(ctx context.Context, checkFlags bool, pro
} }
log.Debug("Building state") log.Debug("Building state")
state, err := meta.BuildMailboxToMessageMap(usr) state, err := meta.BuildMailboxToMessageMap(ctx, usr)
if err != nil { if err != nil {
log.WithError(err).Error("Failed to build state") log.WithError(err).Error("Failed to build state")
return result, err return result, err

45
internal/bridge/events.go Normal file
View File

@ -0,0 +1,45 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge 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 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge 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.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"context"
"github.com/ProtonMail/gluon/watcher"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
)
type bridgeEventSubscription struct {
b *Bridge
}
func (b bridgeEventSubscription) Add(ofType ...events.Event) *watcher.Watcher[events.Event] {
return b.b.addWatcher(ofType...)
}
func (b bridgeEventSubscription) Remove(watcher *watcher.Watcher[events.Event]) {
b.b.remWatcher(watcher)
}
type bridgeEventPublisher struct {
b *Bridge
}
func (b bridgeEventPublisher) PublishEvent(_ context.Context, event events.Event) {
b.b.publish(event)
}

View File

@ -40,3 +40,35 @@ func (bridge *Bridge) setUserAgent(name, version string) {
} }
} }
} }
type bridgeUserAgentUpdater struct {
*Bridge
}
func (b *bridgeUserAgentUpdater) GetUserAgent() string {
return b.identifier.GetUserAgent()
}
func (b *bridgeUserAgentUpdater) HasClient() bool {
return b.identifier.HasClient()
}
func (b *bridgeUserAgentUpdater) SetClient(name, version string) {
b.identifier.SetClient(name, version)
}
func (b *bridgeUserAgentUpdater) SetPlatform(platform string) {
b.identifier.SetPlatform(platform)
}
func (b *bridgeUserAgentUpdater) SetClientString(client string) {
b.identifier.SetClientString(client)
}
func (b *bridgeUserAgentUpdater) GetClientString() string {
return b.identifier.GetClientString()
}
func (b *bridgeUserAgentUpdater) SetUserAgent(name, version string) {
b.setUserAgent(name, version)
}

View File

@ -20,23 +20,12 @@ package bridge
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"io"
"os"
"path/filepath"
"strings" "strings"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon"
"github.com/ProtonMail/gluon/async"
imapEvents "github.com/ProtonMail/gluon/events" imapEvents "github.com/ProtonMail/gluon/events"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/gluon/store"
"github.com/ProtonMail/gluon/store/fallback_v0"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/logging" "github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent" "github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -45,16 +34,6 @@ func (bridge *Bridge) restartIMAP(ctx context.Context) error {
return bridge.serverManager.RestartIMAP(ctx) return bridge.serverManager.RestartIMAP(ctx)
} }
// addIMAPUser connects the given user to gluon.
func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
return bridge.serverManager.AddIMAPUser(ctx, user)
}
// removeIMAPUser disconnects the given user from gluon, optionally also removing its files.
func (bridge *Bridge) removeIMAPUser(ctx context.Context, user *user.User, withData bool) error {
return bridge.serverManager.RemoveIMAPUser(ctx, user, withData)
}
func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) { func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
switch event := event.(type) { switch event := event.(type) {
case imapEvents.UserAdded: case imapEvents.UserAdded:
@ -92,108 +71,59 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
} }
} }
func ApplyGluonCachePathSuffix(basePath string) string { type bridgeIMAPSettings struct {
return filepath.Join(basePath, "backend", "store") b *Bridge
} }
func ApplyGluonConfigPathSuffix(basePath string) string { func (b *bridgeIMAPSettings) EventPublisher() imapsmtpserver.IMAPEventPublisher {
return filepath.Join(basePath, "backend", "db") return b
} }
func newIMAPServer( func (b *bridgeIMAPSettings) TLSConfig() *tls.Config {
gluonCacheDir, gluonConfigDir string, return b.b.tlsConfig
version *semver.Version, }
tlsConfig *tls.Config,
reporter reporter.Reporter,
logClient, logServer bool,
eventCh chan<- imapEvents.Event,
tasks *async.Group,
uidValidityGenerator imap.UIDValidityGenerator,
panicHandler async.PanicHandler,
) (*gluon.Server, error) {
gluonCacheDir = ApplyGluonCachePathSuffix(gluonCacheDir)
gluonConfigDir = ApplyGluonConfigPathSuffix(gluonConfigDir)
logrus.WithFields(logrus.Fields{ func (b *bridgeIMAPSettings) LogClient() bool {
"gluonStore": gluonCacheDir, return b.b.logIMAPClient
"gluonDB": gluonConfigDir, }
"version": version,
"logClient": logClient,
"logServer": logServer,
}).Info("Creating IMAP server")
if logClient || logServer { func (b *bridgeIMAPSettings) LogServer() bool {
log := logrus.WithField("protocol", "IMAP") return b.b.logIMAPServer
log.Warning("================================================") }
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================") func (b *bridgeIMAPSettings) Port() int {
return b.b.vault.GetIMAPPort()
}
func (b *bridgeIMAPSettings) SetPort(i int) error {
return b.b.vault.SetIMAPPort(i)
}
func (b *bridgeIMAPSettings) UseSSL() bool {
return b.b.vault.GetIMAPSSL()
}
func (b *bridgeIMAPSettings) CacheDirectory() string {
return b.b.GetGluonCacheDir()
}
func (b *bridgeIMAPSettings) DataDirectory() (string, error) {
return b.b.GetGluonDataDir()
}
func (b *bridgeIMAPSettings) SetCacheDirectory(s string) error {
return b.b.vault.SetGluonDir(s)
}
func (b *bridgeIMAPSettings) Version() *semver.Version {
return b.b.curVersion
}
func (b *bridgeIMAPSettings) PublishIMAPEvent(ctx context.Context, event imapEvents.Event) {
select {
case <-ctx.Done():
return
case b.b.imapEventCh <- event:
// do nothing
} }
var imapClientLog io.Writer
if logClient {
imapClientLog = logging.NewIMAPLogger()
} else {
imapClientLog = io.Discard
}
var imapServerLog io.Writer
if logServer {
imapServerLog = logging.NewIMAPLogger()
} else {
imapServerLog = io.Discard
}
imapServer, err := gluon.New(
gluon.WithTLS(tlsConfig),
gluon.WithDataDir(gluonCacheDir),
gluon.WithDatabaseDir(gluonConfigDir),
gluon.WithStoreBuilder(new(storeBuilder)),
gluon.WithLogger(imapClientLog, imapServerLog),
getGluonVersionInfo(version),
gluon.WithReporter(reporter),
gluon.WithUIDValidityGenerator(uidValidityGenerator),
gluon.WithPanicHandler(panicHandler),
)
if err != nil {
return nil, err
}
tasks.Once(func(ctx context.Context) {
async.ForwardContext(ctx, eventCh, imapServer.AddWatcher())
})
tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, imapServer.GetErrorCh(), func(err error) {
logrus.WithError(err).Error("IMAP server error")
})
})
return imapServer, nil
}
func getGluonVersionInfo(version *semver.Version) gluon.Option {
return gluon.WithVersionInfo(
int(version.Major()),
int(version.Minor()),
int(version.Patch()),
constants.FullAppName,
"TODO",
"TODO",
)
}
type storeBuilder struct{}
func (*storeBuilder) New(path, userID string, passphrase []byte) (store.Store, error) {
return store.NewOnDiskStore(
filepath.Join(path, userID),
passphrase,
store.WithFallback(fallback_v0.NewOnDiskStoreV0WithCompressor(&fallback_v0.GZipCompressor{})),
)
}
func (*storeBuilder) Delete(path, userID string) error {
return os.RemoveAll(filepath.Join(path, userID))
} }

View File

@ -0,0 +1,26 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge 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 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge 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.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
type bridgeIMAPSMTPTelemetry struct {
b *Bridge
}
func (b bridgeIMAPSMTPTelemetry) SetCacheLocation(s string) {
b.b.heartbeat.SetCacheLocation(s)
}

View File

@ -1,55 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge 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 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge 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.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"strings"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/sirupsen/logrus"
)
func (bridge *Bridge) databaseResyncNeeded() bool {
if strings.HasPrefix(bridge.lastVersion.String(), "3.4.0") &&
strings.HasPrefix(bridge.curVersion.String(), "3.4.1") {
logrus.WithFields(logrus.Fields{
"lastVersion": bridge.lastVersion.String(),
"currVersion": bridge.curVersion.String(),
}).Warning("Database re-synchronisation needed")
return true
}
return false
}
func (bridge *Bridge) migrateUser(vault *vault.User) {
if bridge.databaseResyncNeeded() {
if err := bridge.reporter.ReportMessage("Database need to be re-sync for migration."); err != nil {
logrus.WithError(err).Error("Failed to report database re-sync for migration.")
}
if err := vault.ClearSyncStatus(); err != nil {
logrus.WithError(err).Error("Failed reset to SyncStatus.")
if err2 := bridge.reporter.ReportMessageWithContext("Failed to reset SyncStatus for Database migration.",
reporter.Context{
"error": err,
}); err2 != nil {
logrus.WithError(err2).Error("Failed to report reset SyncStatus error.")
}
}
}
}

View File

@ -84,6 +84,11 @@ func TestBridge_Refresh(t *testing.T) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes() mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
// Wait for refresh event first
refreshCh, refreshChDone := chToType[events.Event, events.UserRefreshed](b.GetEvents(events.UserRefreshed{}))
defer refreshChDone()
require.Equal(t, userID, (<-refreshCh).UserID)
// Then sync event
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{})) syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done() defer done()

View File

@ -467,7 +467,9 @@ SGVsbG8gd29ybGQK
require.Eventually(t, func() bool { require.Eventually(t, func() bool {
messages, err := clientFetch(senderIMAPClient, `Sent`, imap.FetchBodyStructure) messages, err := clientFetch(senderIMAPClient, `Sent`, imap.FetchBodyStructure)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 4, len(messages)) if len(messages) != 4 {
return false
}
// messages may not be in order // messages may not be in order
for _, message := range messages { for _, message := range messages {

View File

@ -1,696 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge 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 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge 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.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"context"
"fmt"
"net"
"path/filepath"
"github.com/ProtonMail/gluon"
"github.com/ProtonMail/gluon/connector"
"github.com/ProtonMail/gluon/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/pkg/cpc"
"github.com/emersion/go-smtp"
"github.com/sirupsen/logrus"
)
// ServerManager manages the IMAP & SMTP servers and their listeners.
type ServerManager struct {
requests *cpc.CPC
imapServer *gluon.Server
imapListener net.Listener
smtpServer *smtp.Server
smtpListener net.Listener
loadedUserCount int
}
func newServerManager() *ServerManager {
return &ServerManager{
requests: cpc.NewCPC(),
}
}
func (sm *ServerManager) Init(bridge *Bridge) error {
imapServer, err := createIMAPServer(bridge)
if err != nil {
return err
}
smtpServer := createSMTPServer(bridge)
sm.imapServer = imapServer
sm.smtpServer = smtpServer
bridge.tasks.Once(func(ctx context.Context) {
logging.DoAnnotated(ctx, func(ctx context.Context) {
sm.run(ctx, bridge)
}, logging.Labels{
"service": "server-manager",
})
})
return nil
}
func (sm *ServerManager) CloseServers(ctx context.Context) error {
defer sm.requests.Close()
_, err := sm.requests.Send(ctx, &smRequestClose{})
return err
}
func (sm *ServerManager) RestartIMAP(ctx context.Context) error {
_, err := sm.requests.Send(ctx, &smRequestRestartIMAP{})
return err
}
func (sm *ServerManager) RestartSMTP(ctx context.Context) error {
_, err := sm.requests.Send(ctx, &smRequestRestartSMTP{})
return err
}
func (sm *ServerManager) AddIMAPUser(ctx context.Context, user *user.User) error {
_, err := sm.requests.Send(ctx, &smRequestAddIMAPUser{user: user})
return err
}
func (sm *ServerManager) RemoveIMAPUser(ctx context.Context, user *user.User, withData bool) error {
_, err := sm.requests.Send(ctx, &smRequestRemoveIMAPUser{
user: user,
withData: withData,
})
return err
}
func (sm *ServerManager) SetGluonDir(ctx context.Context, gluonDir string) error {
_, err := sm.requests.Send(ctx, &smRequestSetGluonDir{
dir: gluonDir,
})
return err
}
func (sm *ServerManager) AddGluonUser(ctx context.Context, conn connector.Connector, passphrase []byte) (string, error) {
reply, err := cpc.SendTyped[string](ctx, sm.requests, &smRequestAddGluonUser{
conn: conn,
passphrase: passphrase,
})
return reply, err
}
func (sm *ServerManager) RemoveGluonUser(ctx context.Context, gluonID string) error {
_, err := sm.requests.Send(ctx, &smRequestRemoveGluonUser{
userID: gluonID,
})
return err
}
func (sm *ServerManager) run(ctx context.Context, bridge *Bridge) {
eventCh, cancel := bridge.GetEvents()
defer cancel()
for {
select {
case <-ctx.Done():
sm.handleClose(ctx, bridge)
return
case evt := <-eventCh:
switch evt.(type) {
case events.ConnStatusDown:
logrus.Info("Server Manager, network down stopping listeners")
if err := sm.closeSMTPServer(bridge); err != nil {
logrus.WithError(err).Error("Failed to close SMTP server")
}
if err := sm.stopIMAPListener(bridge); err != nil {
logrus.WithError(err)
}
case events.ConnStatusUp:
logrus.Info("Server Manager, network up starting listeners")
sm.handleLoadedUserCountChange(ctx, bridge)
}
case request, ok := <-sm.requests.ReceiveCh():
if !ok {
return
}
switch r := request.Value().(type) {
case *smRequestClose:
sm.handleClose(ctx, bridge)
request.Reply(ctx, nil, nil)
return
case *smRequestRestartSMTP:
err := sm.restartSMTP(bridge)
request.Reply(ctx, nil, err)
case *smRequestRestartIMAP:
err := sm.restartIMAP(ctx, bridge)
request.Reply(ctx, nil, err)
case *smRequestAddIMAPUser:
err := sm.handleAddIMAPUser(ctx, r.user)
request.Reply(ctx, nil, err)
if err == nil {
sm.loadedUserCount++
sm.handleLoadedUserCountChange(ctx, bridge)
}
case *smRequestRemoveIMAPUser:
err := sm.handleRemoveIMAPUser(ctx, r.user, r.withData)
request.Reply(ctx, nil, err)
if err == nil {
sm.loadedUserCount--
sm.handleLoadedUserCountChange(ctx, bridge)
}
case *smRequestSetGluonDir:
err := sm.handleSetGluonDir(ctx, bridge, r.dir)
request.Reply(ctx, nil, err)
case *smRequestAddGluonUser:
id, err := sm.handleAddGluonUser(ctx, r.conn, r.passphrase)
request.Reply(ctx, id, err)
case *smRequestRemoveGluonUser:
err := sm.handleRemoveGluonUser(ctx, r.userID)
request.Reply(ctx, nil, err)
}
}
}
}
func (sm *ServerManager) handleLoadedUserCountChange(ctx context.Context, bridge *Bridge) {
logrus.Infof("Validating Listener State %v", sm.loadedUserCount)
if sm.shouldStartServers() {
if sm.imapListener == nil {
if err := sm.serveIMAP(ctx, bridge); err != nil {
logrus.WithError(err).Error("Failed to start IMAP server")
}
}
if sm.smtpListener == nil {
if err := sm.restartSMTP(bridge); err != nil {
logrus.WithError(err).Error("Failed to start SMTP server")
}
}
} else {
if sm.imapListener != nil {
if err := sm.stopIMAPListener(bridge); err != nil {
logrus.WithError(err).Error("Failed to stop IMAP server")
}
}
if sm.smtpListener != nil {
if err := sm.closeSMTPServer(bridge); err != nil {
logrus.WithError(err).Error("Failed to stop SMTP server")
}
}
}
}
func (sm *ServerManager) handleClose(ctx context.Context, bridge *Bridge) {
// Close the IMAP server.
if err := sm.closeIMAPServer(ctx, bridge); err != nil {
logrus.WithError(err).Error("Failed to close IMAP server")
}
// Close the SMTP server.
if err := sm.closeSMTPServer(bridge); err != nil {
logrus.WithError(err).Error("Failed to close SMTP server")
}
}
func (sm *ServerManager) handleAddIMAPUser(ctx context.Context, user *user.User) error {
if sm.imapServer == nil {
return fmt.Errorf("no imap server instance running")
}
imapConn, err := user.NewIMAPConnectors()
if err != nil {
return fmt.Errorf("failed to create IMAP connectors: %w", err)
}
for addrID, imapConn := range imapConn {
log := logrus.WithFields(logrus.Fields{
"userID": user.ID(),
"addrID": addrID,
})
if gluonID, ok := user.GetGluonID(addrID); ok {
log.WithField("gluonID", gluonID).Info("Loading existing IMAP user")
// Load the user, checking whether the DB was newly created.
isNew, err := sm.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey())
if err != nil {
return fmt.Errorf("failed to load IMAP user: %w", err)
}
if isNew {
// If the DB was newly created, clear the sync status; gluon's DB was not found.
logrus.Warn("IMAP user DB was newly created, clearing sync status")
// Remove the user from IMAP so we can clear the sync status.
if err := sm.imapServer.RemoveUser(ctx, gluonID, false); err != nil {
return fmt.Errorf("failed to remove IMAP user: %w", err)
}
// Clear the sync status -- we need to resync all messages.
if err := user.ClearSyncStatus(); err != nil {
return fmt.Errorf("failed to clear sync status: %w", err)
}
// Add the user back to the IMAP server.
if isNew, err := sm.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey()); err != nil {
return fmt.Errorf("failed to add IMAP user: %w", err)
} else if isNew {
panic("IMAP user should already have a database")
}
} else if status := user.GetSyncStatus(); !status.HasLabels {
// Otherwise, the DB already exists -- if the labels are not yet synced, we need to re-create the DB.
if err := sm.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
return fmt.Errorf("failed to remove old IMAP user: %w", err)
}
if err := user.RemoveGluonID(addrID, gluonID); err != nil {
return fmt.Errorf("failed to remove old IMAP user ID: %w", err)
}
gluonID, err := sm.imapServer.AddUser(ctx, imapConn, user.GluonKey())
if err != nil {
return fmt.Errorf("failed to add IMAP user: %w", err)
}
if err := user.SetGluonID(addrID, gluonID); err != nil {
return fmt.Errorf("failed to set IMAP user ID: %w", err)
}
log.WithField("gluonID", gluonID).Info("Re-created IMAP user")
}
} else {
log.Info("Creating new IMAP user")
gluonID, err := sm.imapServer.AddUser(ctx, imapConn, user.GluonKey())
if err != nil {
return fmt.Errorf("failed to add IMAP user: %w", err)
}
if err := user.SetGluonID(addrID, gluonID); err != nil {
return fmt.Errorf("failed to set IMAP user ID: %w", err)
}
log.WithField("gluonID", gluonID).Info("Created new IMAP user")
}
}
// Trigger a sync for the user, if needed.
user.TriggerSync()
return nil
}
func (sm *ServerManager) handleRemoveIMAPUser(ctx context.Context, user *user.User, withData bool) error {
if sm.imapServer == nil {
return fmt.Errorf("no imap server instance running")
}
logrus.WithFields(logrus.Fields{
"userID": user.ID(),
"withData": withData,
}).Debug("Removing IMAP user")
for addrID, gluonID := range user.GetGluonIDs() {
if err := sm.imapServer.RemoveUser(ctx, gluonID, withData); err != nil {
return fmt.Errorf("failed to remove IMAP user: %w", err)
}
if withData {
if err := user.RemoveGluonID(addrID, gluonID); err != nil {
return fmt.Errorf("failed to remove IMAP user ID: %w", err)
}
}
}
return nil
}
func createIMAPServer(bridge *Bridge) (*gluon.Server, error) {
gluonDataDir, err := bridge.GetGluonDataDir()
if err != nil {
return nil, fmt.Errorf("failed to get Gluon Database directory: %w", err)
}
return newIMAPServer(
bridge.vault.GetGluonCacheDir(),
gluonDataDir,
bridge.curVersion,
bridge.tlsConfig,
bridge.reporter,
bridge.logIMAPClient,
bridge.logIMAPServer,
bridge.imapEventCh,
bridge.tasks,
bridge.uidValidityGenerator,
bridge.panicHandler,
)
}
func createSMTPServer(bridge *Bridge) *smtp.Server {
return newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP)
}
func (sm *ServerManager) closeSMTPServer(bridge *Bridge) error {
// We close the listener ourselves even though it's also closed by smtpServer.Close().
// This is because smtpServer.Serve() is called in a separate goroutine and might be executed
// after we've already closed the server. However, go-smtp has a bug; it blocks on the listener
// even after the server has been closed. So we close the listener ourselves to unblock it.
if sm.smtpListener != nil {
logrus.Info("Closing SMTP Listener")
if err := sm.smtpListener.Close(); err != nil {
return fmt.Errorf("failed to close SMTP listener: %w", err)
}
sm.smtpListener = nil
}
if sm.smtpServer != nil {
logrus.Info("Closing SMTP server")
if err := sm.smtpServer.Close(); err != nil {
logrus.WithError(err).Debug("Failed to close SMTP server (expected -- we close the listener ourselves)")
}
sm.smtpServer = nil
bridge.publish(events.SMTPServerStopped{})
}
return nil
}
func (sm *ServerManager) closeIMAPServer(ctx context.Context, bridge *Bridge) error {
if sm.imapListener != nil {
logrus.Info("Closing IMAP Listener")
if err := sm.imapListener.Close(); err != nil {
return fmt.Errorf("failed to close IMAP listener: %w", err)
}
sm.imapListener = nil
bridge.publish(events.IMAPServerStopped{})
}
if sm.imapServer != nil {
logrus.Info("Closing IMAP server")
if err := sm.imapServer.Close(ctx); err != nil {
return fmt.Errorf("failed to close IMAP server: %w", err)
}
sm.imapServer = nil
}
return nil
}
func (sm *ServerManager) restartIMAP(ctx context.Context, bridge *Bridge) error {
logrus.Info("Restarting IMAP server")
if sm.imapListener != nil {
if err := sm.imapListener.Close(); err != nil {
return fmt.Errorf("failed to close IMAP listener: %w", err)
}
sm.imapListener = nil
bridge.publish(events.IMAPServerStopped{})
}
if sm.shouldStartServers() {
return sm.serveIMAP(ctx, bridge)
}
return nil
}
func (sm *ServerManager) restartSMTP(bridge *Bridge) error {
logrus.Info("Restarting SMTP server")
if err := sm.closeSMTPServer(bridge); err != nil {
return fmt.Errorf("failed to close SMTP: %w", err)
}
bridge.publish(events.SMTPServerStopped{})
sm.smtpServer = newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP)
if sm.shouldStartServers() {
return sm.serveSMTP(bridge)
}
return nil
}
func (sm *ServerManager) serveSMTP(bridge *Bridge) error {
port, err := func() (int, error) {
logrus.WithFields(logrus.Fields{
"port": bridge.vault.GetSMTPPort(),
"ssl": bridge.vault.GetSMTPSSL(),
}).Info("Starting SMTP server")
smtpListener, err := newListener(bridge.vault.GetSMTPPort(), bridge.vault.GetSMTPSSL(), bridge.tlsConfig)
if err != nil {
return 0, fmt.Errorf("failed to create SMTP listener: %w", err)
}
sm.smtpListener = smtpListener
bridge.tasks.Once(func(context.Context) {
if err := sm.smtpServer.Serve(smtpListener); err != nil {
logrus.WithError(err).Info("SMTP server stopped")
}
})
if err := bridge.vault.SetSMTPPort(getPort(smtpListener.Addr())); err != nil {
return 0, fmt.Errorf("failed to store SMTP port in vault: %w", err)
}
return getPort(smtpListener.Addr()), nil
}()
if err != nil {
bridge.publish(events.SMTPServerError{
Error: err,
})
return err
}
bridge.publish(events.SMTPServerReady{
Port: port,
})
return nil
}
func (sm *ServerManager) serveIMAP(ctx context.Context, bridge *Bridge) error {
port, err := func() (int, error) {
if sm.imapServer == nil {
return 0, fmt.Errorf("no IMAP server instance running")
}
logrus.WithFields(logrus.Fields{
"port": bridge.vault.GetIMAPPort(),
"ssl": bridge.vault.GetIMAPSSL(),
}).Info("Starting IMAP server")
imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig)
if err != nil {
return 0, fmt.Errorf("failed to create IMAP listener: %w", err)
}
sm.imapListener = imapListener
if err := sm.imapServer.Serve(ctx, sm.imapListener); err != nil {
return 0, fmt.Errorf("failed to serve IMAP: %w", err)
}
if err := bridge.vault.SetIMAPPort(getPort(imapListener.Addr())); err != nil {
return 0, fmt.Errorf("failed to store IMAP port in vault: %w", err)
}
return getPort(imapListener.Addr()), nil
}()
if err != nil {
bridge.publish(events.IMAPServerError{
Error: err,
})
return err
}
bridge.publish(events.IMAPServerReady{
Port: port,
})
return nil
}
func (sm *ServerManager) stopIMAPListener(bridge *Bridge) error {
logrus.Info("Stopping IMAP listener")
if sm.imapListener != nil {
if err := sm.imapListener.Close(); err != nil {
return err
}
sm.imapListener = nil
bridge.publish(events.IMAPServerStopped{})
}
return nil
}
func (sm *ServerManager) handleSetGluonDir(ctx context.Context, bridge *Bridge, newGluonDir string) error {
return safe.RLockRet(func() error {
currentGluonDir := bridge.GetGluonCacheDir()
newGluonDir = filepath.Join(newGluonDir, "gluon")
if newGluonDir == currentGluonDir {
return fmt.Errorf("new gluon dir is the same as the old one")
}
if err := sm.closeIMAPServer(context.Background(), bridge); err != nil {
return fmt.Errorf("failed to close IMAP: %w", err)
}
sm.loadedUserCount = 0
if err := bridge.moveGluonCacheDir(currentGluonDir, newGluonDir); err != nil {
logrus.WithError(err).Error("failed to move GluonCacheDir")
if err := bridge.vault.SetGluonDir(currentGluonDir); err != nil {
return fmt.Errorf("failed to revert GluonCacheDir: %w", err)
}
}
bridge.heartbeat.SetCacheLocation(newGluonDir)
gluonDataDir, err := bridge.GetGluonDataDir()
if err != nil {
return fmt.Errorf("failed to get Gluon Database directory: %w", err)
}
imapServer, err := newIMAPServer(
bridge.vault.GetGluonCacheDir(),
gluonDataDir,
bridge.curVersion,
bridge.tlsConfig,
bridge.reporter,
bridge.logIMAPClient,
bridge.logIMAPServer,
bridge.imapEventCh,
bridge.tasks,
bridge.uidValidityGenerator,
bridge.panicHandler,
)
if err != nil {
return fmt.Errorf("failed to create new IMAP server: %w", err)
}
sm.imapServer = imapServer
for _, bridgeUser := range bridge.users {
if err := sm.handleAddIMAPUser(ctx, bridgeUser); err != nil {
return fmt.Errorf("failed to add users to new IMAP server: %w", err)
}
sm.loadedUserCount++
}
if sm.shouldStartServers() {
if err := sm.serveIMAP(ctx, bridge); err != nil {
return fmt.Errorf("failed to serve IMAP: %w", err)
}
}
return nil
}, bridge.usersLock)
}
func (sm *ServerManager) handleAddGluonUser(ctx context.Context, conn connector.Connector, passphrase []byte) (string, error) {
if sm.imapServer == nil {
return "", fmt.Errorf("no imap server instance running")
}
return sm.imapServer.AddUser(ctx, conn, passphrase)
}
func (sm *ServerManager) handleRemoveGluonUser(ctx context.Context, userID string) error {
if sm.imapServer == nil {
return fmt.Errorf("no imap server instance running")
}
return sm.imapServer.RemoveUser(ctx, userID, true)
}
func (sm *ServerManager) shouldStartServers() bool {
return sm.loadedUserCount >= 1
}
type smRequestClose struct{}
type smRequestRestartIMAP struct{}
type smRequestRestartSMTP struct{}
type smRequestAddIMAPUser struct {
user *user.User
}
type smRequestRemoveIMAPUser struct {
user *user.User
withData bool
}
type smRequestSetGluonDir struct {
dir string
}
type smRequestAddGluonUser struct {
conn connector.Connector
passphrase []byte
}
type smRequestRemoveGluonUser struct {
userID string
}

View File

@ -20,11 +20,10 @@ package bridge
import ( import (
"context" "context"
"fmt" "fmt"
"net"
"os"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
"github.com/ProtonMail/proton-bridge/v3/internal/updater" "github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -131,26 +130,41 @@ func (bridge *Bridge) GetGluonDataDir() (string, error) {
} }
func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error { func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error {
bridge.usersLock.RLock()
defer func() {
logrus.Info("Restarting user event loops")
for _, u := range bridge.users {
u.ResumeEventLoop()
}
bridge.usersLock.RUnlock()
}()
type waiter struct {
w *userevents.EventPollWaiter
id string
}
waiters := make([]waiter, 0, len(bridge.users))
logrus.Info("Pausing user event loops for gluon dir change")
for id, u := range bridge.users {
waiters = append(waiters, waiter{w: u.PauseEventLoopWithWaiter(), id: id})
}
logrus.Info("Waiting on user event loop completion")
for _, waiter := range waiters {
if err := waiter.w.WaitPollFinished(ctx); err != nil {
logrus.WithError(err).Errorf("Failed to wait on event loop pause for user %v", waiter.id)
return fmt.Errorf("failed on event loop pause: %w", err)
}
}
logrus.Info("Changing gluon directory")
return bridge.serverManager.SetGluonDir(ctx, newGluonDir) return bridge.serverManager.SetGluonDir(ctx, newGluonDir)
} }
func (bridge *Bridge) moveGluonCacheDir(oldGluonDir, newGluonDir string) error {
logrus.Infof("gluon cache moving from %s to %s", oldGluonDir, newGluonDir)
oldCacheDir := ApplyGluonCachePathSuffix(oldGluonDir)
if err := copyDir(oldCacheDir, ApplyGluonCachePathSuffix(newGluonDir)); err != nil {
return fmt.Errorf("failed to copy gluon dir: %w", err)
}
if err := bridge.vault.SetGluonDir(newGluonDir); err != nil {
return fmt.Errorf("failed to set new gluon cache dir: %w", err)
}
if err := os.RemoveAll(oldCacheDir); err != nil {
logrus.WithError(err).Error("failed to remove old gluon cache dir")
}
return nil
}
func (bridge *Bridge) GetProxyAllowed() bool { func (bridge *Bridge) GetProxyAllowed() bool {
return bridge.vault.GetProxyAllowed() return bridge.vault.GetProxyAllowed()
} }
@ -318,16 +332,3 @@ func (bridge *Bridge) FactoryReset(ctx context.Context) {
logrus.WithError(err).Error("Failed to clear data paths") logrus.WithError(err).Error("Failed to clear data paths")
} }
} }
func getPort(addr net.Addr) int {
switch addr := addr.(type) {
case *net.TCPAddr:
return addr.Port
case *net.UDPAddr:
return addr.Port
default:
return 0
}
}

View File

@ -25,6 +25,7 @@ import (
"github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server" "github.com/ProtonMail/go-proton-api/server"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge" "github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -51,6 +52,45 @@ func TestBridge_Settings_GluonDir(t *testing.T) {
}) })
} }
func TestBridge_Settings_GluonDirWithOnGoingEvents(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
userID, addrID, err := s.CreateUser("imap", password)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done()
_, err := bridge.LoginFull(context.Background(), "imap", password, nil, nil)
require.NoError(t, err)
<-syncCh
})
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
require.NoError(t, err)
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, labelID, 200)
})
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// Create a new location for the Gluon data.
newGluonDir := t.TempDir()
// Move the gluon dir; it should also move the user's data.
require.NoError(t, bridge.SetGluonDir(context.Background(), newGluonDir))
// Check that the new directory is not empty.
entries, err := os.ReadDir(newGluonDir)
require.NoError(t, err)
// There should be at least one entry.
require.NotEmpty(t, entries)
})
})
}
func TestBridge_Settings_IMAPPort(t *testing.T) { func TestBridge_Settings_IMAPPort(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) { withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {

View File

@ -21,43 +21,37 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/ProtonMail/proton-bridge/v3/internal/identifier"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/sirupsen/logrus"
) )
func (bridge *Bridge) restartSMTP(ctx context.Context) error { func (bridge *Bridge) restartSMTP(ctx context.Context) error {
return bridge.serverManager.RestartSMTP(ctx) return bridge.serverManager.RestartSMTP(ctx)
} }
func newSMTPServer(bridge *Bridge, tlsConfig *tls.Config, logSMTP bool) *smtp.Server { type bridgeSMTPSettings struct {
logrus.WithField("logSMTP", logSMTP).Info("Creating SMTP server") b *Bridge
}
smtpServer := smtp.NewServer(&smtpBackend{Bridge: bridge})
func (b *bridgeSMTPSettings) TLSConfig() *tls.Config {
smtpServer.TLSConfig = tlsConfig return b.b.tlsConfig
smtpServer.Domain = constants.Host }
smtpServer.AllowInsecureAuth = true
smtpServer.MaxLineLength = 1 << 16 func (b *bridgeSMTPSettings) Log() bool {
smtpServer.ErrorLog = logging.NewSMTPLogger() return b.b.logSMTP
}
// go-smtp suppors SASL PLAIN but not LOGIN. We need to add LOGIN support ourselves.
smtpServer.EnableAuth(sasl.Login, func(conn *smtp.Conn) sasl.Server { func (b *bridgeSMTPSettings) Port() int {
return sasl.NewLoginServer(func(username, password string) error { return b.b.vault.GetSMTPPort()
return conn.Session().AuthPlain(username, password) }
})
}) func (b *bridgeSMTPSettings) SetPort(i int) error {
return b.b.vault.SetSMTPPort(i)
if logSMTP { }
log := logrus.WithField("protocol", "SMTP")
log.Warning("================================================") func (b *bridgeSMTPSettings) UseSSL() bool {
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") return b.b.vault.GetSMTPSSL()
log.Warning("================================================") }
smtpServer.Debug = logging.NewSMTPDebugLogger() func (b *bridgeSMTPSettings) Identifier() identifier.UserAgentUpdater {
} return &bridgeUserAgentUpdater{Bridge: b.b}
return smtpServer
} }

View File

@ -21,9 +21,11 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings"
"sync/atomic" "sync/atomic"
"testing" "testing"
"time" "time"
@ -252,14 +254,17 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
// Login the user; its sync should fail. // Login the user; its sync should fail.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done()
{ {
syncCh, done := chToType[events.Event, events.SyncFailed](b.GetEvents(events.SyncFailed{})) syncFailedCh, syncFailedDone := chToType[events.Event, events.SyncFailed](b.GetEvents(events.SyncFailed{}))
defer done() defer syncFailedDone()
userID, err := b.LoginFull(ctx, "imap", password, nil, nil) userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, userID, (<-syncCh).UserID) require.Equal(t, userID, (<-syncFailedCh).UserID)
info, err := b.GetUserInfo(userID) info, err := b.GetUserInfo(userID)
require.NoError(t, err) require.NoError(t, err)
@ -282,11 +287,7 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
// Remove the network limit, allowing the sync to finish. // Remove the network limit, allowing the sync to finish.
netCtl.SetReadLimit(0) netCtl.SetReadLimit(0)
{ {
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done()
require.Equal(t, userID, (<-syncCh).UserID) require.Equal(t, userID, (<-syncCh).UserID)
info, err := b.GetUserInfo(userID) info, err := b.GetUserInfo(userID)
@ -298,12 +299,6 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }() defer func() { _ = client.Logout() }()
status, err := client.Select(`Folders/folder`, false)
require.NoError(t, err)
// Original folder should have more than 0 messages and less than the total.
require.Greater(t, status.Messages, uint32(0))
require.Less(t, status.Messages, uint32(numMsg))
// Check that the new messages arrive in the right location. // Check that the new messages arrive in the right location.
require.Eventually(t, func() bool { require.Eventually(t, func() bool {
status, err := client.Select(`Folders/folder2`, true) status, err := client.Select(`Folders/folder2`, true)
@ -321,6 +316,269 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
}, server.WithTLS(false)) }, server.WithTLS(false))
} }
func TestBridge_CanProcessEventsDuringSync(t *testing.T) {
numMsg := 1 << 8
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
userID, addrID, err := s.CreateUser("imap", password)
require.NoError(t, err)
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
require.NoError(t, err)
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
})
// Simulate 429 to prevent sync from progressing.
s.AddStatusHook(func(request *http.Request) (int, bool) {
if strings.Contains(request.URL.Path, "/mail/v4/messages/") {
return http.StatusTooManyRequests, true
}
return 0, false
})
// The initial user should be fully synced.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
syncStartedCh, syncStartedDone := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
defer syncStartedDone()
addressCreatedCh, addressCreatedDone := chToType[events.Event, events.UserAddressCreated](bridge.GetEvents(events.UserAddressCreated{}))
defer addressCreatedDone()
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
require.NoError(t, err)
require.Equal(t, userID, (<-syncStartedCh).UserID)
// Create a new address
newAddress := "foo@proton.ch"
addrID, err := s.CreateAddress(userID, newAddress, password)
require.NoError(t, err)
event := <-addressCreatedCh
require.Equal(t, userID, event.UserID)
require.Equal(t, newAddress, event.Email)
require.Equal(t, addrID, event.AddressID)
})
}, server.WithTLS(false))
}
func TestBridge_RefreshDuringSyncRestartSync(t *testing.T) {
numMsg := 1 << 8
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
userID, addrID, err := s.CreateUser("imap", password)
require.NoError(t, err)
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
require.NoError(t, err)
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
})
var refreshPerformed atomic.Bool
refreshPerformed.Store(false)
// Simulate 429 to prevent sync from progressing.
s.AddStatusHook(func(request *http.Request) (int, bool) {
if strings.Contains(request.URL.Path, "/mail/v4/messages/") {
if !refreshPerformed.Load() {
return http.StatusTooManyRequests, true
}
}
return 0, false
})
// The initial user should be fully synced.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done()
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
require.NoError(t, err)
syncStartedCh, syncStartedDone := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
defer syncStartedDone()
require.Equal(t, userID, (<-syncStartedCh).UserID)
require.NoError(t, err, s.RefreshUser(userID, proton.RefreshMail))
require.Equal(t, userID, (<-syncStartedCh).UserID)
refreshPerformed.Store(true)
require.Equal(t, userID, (<-syncCh).UserID)
})
}, server.WithTLS(false))
}
func TestBridge_EventReplayAfterSyncHasFinished(t *testing.T) {
numMsg := 1 << 8
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
userID, addrID, err := s.CreateUser("imap", password)
require.NoError(t, err)
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
require.NoError(t, err)
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
})
addrID1, err := s.CreateAddress(userID, "foo@proton.ch", password)
require.NoError(t, err)
var allowSyncToProgress atomic.Bool
allowSyncToProgress.Store(false)
// Simulate 429 to prevent sync from progressing.
s.AddStatusHook(func(request *http.Request) (int, bool) {
if request.Method == "GET" && strings.Contains(request.URL.Path, "/mail/v4/messages/") {
if !allowSyncToProgress.Load() {
return http.StatusTooManyRequests, true
}
}
return 0, false
})
// The initial user should be fully synced.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done()
syncStartedCh, syncStartedDone := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
defer syncStartedDone()
addressCreatedCh, addressCreatedDone := chToType[events.Event, events.UserAddressCreated](bridge.GetEvents(events.UserAddressCreated{}))
defer addressCreatedDone()
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
require.NoError(t, err)
require.Equal(t, userID, (<-syncStartedCh).UserID)
// create 20 more messages and move them to inbox
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 20)
})
// User AddrID2 event as a check point to see when the new address was created.
addrID2, err := s.CreateAddress(userID, "bar@proton.ch", password)
require.NoError(t, err)
allowSyncToProgress.Store(true)
require.Equal(t, userID, (<-syncCh).UserID)
// At most two events can be published, one for the first address, then for the second.
// if the second event is not `addrID2` then something went wrong.
event := <-addressCreatedCh
if event.AddressID == addrID1 {
event = <-addressCreatedCh
}
require.Equal(t, addrID2, event.AddressID)
info, err := bridge.GetUserInfo(userID)
require.NoError(t, err)
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
// Finally check if the 20 messages are in INBOX.
status, err := client.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
require.NoError(t, err)
require.Equal(t, uint32(20), status.Messages)
// Finally check if the numMsg are in the folder.
status, err = client.Status("Folders/folder", []imap.StatusItem{imap.StatusMessages})
require.NoError(t, err)
require.Equal(t, uint32(numMsg), status.Messages)
})
}, server.WithTLS(false))
}
func TestBridge_MessageCreateDuringSync(t *testing.T) {
numMsg := 1 << 8
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
userID, addrID, err := s.CreateUser("imap", password)
require.NoError(t, err)
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
require.NoError(t, err)
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
})
var allowSyncToProgress atomic.Bool
allowSyncToProgress.Store(false)
// Simulate 429 to prevent sync from progressing.
s.AddStatusHook(func(request *http.Request) (int, bool) {
if request.Method == "GET" && strings.Contains(request.URL.Path, "/mail/v4/messages/") {
if !allowSyncToProgress.Load() {
return http.StatusTooManyRequests, true
}
}
return 0, false
})
// The initial user should be fully synced.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
syncStartedCh, syncStartedDone := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
defer syncStartedDone()
addressCreatedCh, addressCreatedDone := chToType[events.Event, events.UserAddressCreated](bridge.GetEvents(events.UserAddressCreated{}))
defer addressCreatedDone()
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
require.NoError(t, err)
require.Equal(t, userID, (<-syncStartedCh).UserID)
// create 20 more messages and move them to inbox
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 20)
})
// User AddrID2 event as a check point to see when the new address was created.
addrID, err := s.CreateAddress(userID, "bar@proton.ch", password)
require.NoError(t, err)
// At most two events can be published, one for the first address, then for the second.
// if the second event is not `addrID` then something went wrong.
event := <-addressCreatedCh
require.Equal(t, addrID, event.AddressID)
allowSyncToProgress.Store(true)
info, err := bridge.GetUserInfo(userID)
require.NoError(t, err)
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
require.Eventually(t, func() bool {
// Finally check if the 20 messages are in INBOX.
status, err := client.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
require.NoError(t, err)
return uint32(20) == status.Messages
}, 10*time.Second, time.Second)
})
}, server.WithTLS(false))
}
func withClient(ctx context.Context, t *testing.T, s *server.Server, username string, password []byte, fn func(context.Context, *proton.Client)) { //nolint:unparam func withClient(ctx context.Context, t *testing.T, s *server.Server, username string, password []byte, fn func(context.Context, *proton.Client)) { //nolint:unparam
m := proton.New( m := proton.New(
proton.WithHostURL(s.GetHostURL()), proton.WithHostURL(s.GetHostURL()),

View File

@ -31,7 +31,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
// Disabled due to flakyness. // Disabled due to flakiness.
func _TestBridge_SyncExistsWithErrorWhenTooManyFilesAreOpen(t *testing.T) { //nolint:unused func _TestBridge_SyncExistsWithErrorWhenTooManyFilesAreOpen(t *testing.T) { //nolint:unused
var rlimitCurrent syscall.Rlimit var rlimitCurrent syscall.Rlimit

View File

@ -32,15 +32,7 @@ type Locator interface {
GetLicenseFilePath() string GetLicenseFilePath() string
GetDependencyLicensesLink() string GetDependencyLicensesLink() string
Clear(...string) error Clear(...string) error
} ProvideIMAPSyncConfigPath() (string, error)
type Identifier interface {
GetUserAgent() string
HasClient() bool
SetClient(name, version string)
SetPlatform(platform string)
SetClientString(client string)
GetClientString() string
} }
type ProxyController interface { type ProxyController interface {

View File

@ -30,6 +30,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/logging" "github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
"github.com/ProtonMail/proton-bridge/v3/internal/try" "github.com/ProtonMail/proton-bridge/v3/internal/try"
"github.com/ProtonMail/proton-bridge/v3/internal/user" "github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
@ -243,6 +244,11 @@ func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error { func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
logrus.WithField("userID", userID).Info("Deleting user") logrus.WithField("userID", userID).Info("Deleting user")
syncConfigDir, err := bridge.locator.ProvideIMAPSyncConfigPath()
if err != nil {
return fmt.Errorf("failed to get sync config path")
}
return safe.LockRet(func() error { return safe.LockRet(func() error {
if !bridge.vault.HasUser(userID) { if !bridge.vault.HasUser(userID) {
return ErrNoSuchUser return ErrNoSuchUser
@ -252,6 +258,10 @@ func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
bridge.logoutUser(ctx, user, true, true, !bridge.GetTelemetryDisabled()) bridge.logoutUser(ctx, user, true, true, !bridge.GetTelemetryDisabled())
} }
if err := imapservice.DeleteSyncState(syncConfigDir, userID); err != nil {
return fmt.Errorf("failed to delete use sync config")
}
if err := bridge.vault.DeleteUser(userID); err != nil { if err := bridge.vault.DeleteUser(userID); err != nil {
logrus.WithError(err).Error("Failed to delete vault user") logrus.WithError(err).Error("Failed to delete vault user")
} }
@ -278,18 +288,10 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va
return fmt.Errorf("address mode is already %q", mode) return fmt.Errorf("address mode is already %q", mode)
} }
if err := bridge.removeIMAPUser(ctx, user, true); err != nil {
return fmt.Errorf("failed to remove IMAP user: %w", err)
}
if err := user.SetAddressMode(ctx, mode); err != nil { if err := user.SetAddressMode(ctx, mode); err != nil {
return fmt.Errorf("failed to set address mode: %w", err) return fmt.Errorf("failed to set address mode: %w", err)
} }
if err := bridge.addIMAPUser(ctx, user); err != nil {
return fmt.Errorf("failed to add IMAP user: %w", err)
}
bridge.publish(events.AddressModeChanged{ bridge.publish(events.AddressModeChanged{
UserID: userID, UserID: userID,
AddressMode: mode, AddressMode: mode,
@ -335,13 +337,7 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
logrus.WithError(rerr).Error("Failed to report feedback failure") logrus.WithError(rerr).Error("Failed to report feedback failure")
} }
if err := bridge.addIMAPUser(ctx, user); err != nil { return user.BadEventFeedbackResync(ctx)
return fmt.Errorf("failed to add IMAP user: %w", err)
}
user.BadEventFeedbackResync(ctx)
return nil
} }
if rerr := bridge.reporter.ReportMessageWithContext( if rerr := bridge.reporter.ReportMessageWithContext(
@ -524,8 +520,10 @@ func (bridge *Bridge) addUserWithVault(
return fmt.Errorf("failed to get Statistics directory: %w", err) return fmt.Errorf("failed to get Statistics directory: %w", err)
} }
// re-set SyncStatus if database need to be re-synced for migration. syncSettingsPath, err := bridge.locator.ProvideIMAPSyncConfigPath()
bridge.migrateUser(vault) if err != nil {
return fmt.Errorf("failed to get IMAP sync config path: %w", err)
}
user, err := user.New( user, err := user.New(
ctx, ctx,
@ -538,16 +536,16 @@ func (bridge *Bridge) addUserWithVault(
bridge.vault.GetMaxSyncMemory(), bridge.vault.GetMaxSyncMemory(),
statsPath, statsPath,
bridge, bridge,
bridge.serverManager,
bridge.serverManager,
&bridgeEventSubscription{b: bridge},
bridge.syncService,
syncSettingsPath,
) )
if err != nil { if err != nil {
return fmt.Errorf("failed to create user: %w", err) return fmt.Errorf("failed to create user: %w", err)
} }
// Connect the user's address(es) to gluon.
if err := bridge.addIMAPUser(ctx, user); err != nil {
return fmt.Errorf("failed to add IMAP user: %w", err)
}
// Handle events coming from the user before forwarding them to the bridge. // Handle events coming from the user before forwarding them to the bridge.
// For example, if the user's addresses change, we need to update them in gluon. // For example, if the user's addresses change, we need to update them in gluon.
bridge.tasks.Once(func(ctx context.Context) { bridge.tasks.Once(func(ctx context.Context) {
@ -557,11 +555,8 @@ func (bridge *Bridge) addUserWithVault(
"event": event, "event": event,
}).Debug("Received user event") }).Debug("Received user event")
if err := bridge.handleUserEvent(ctx, user, event); err != nil { bridge.handleUserEvent(ctx, user, event)
logrus.WithError(err).Error("Failed to handle user event") bridge.publish(event)
} else {
bridge.publish(event)
}
}) })
}) })
@ -612,10 +607,6 @@ func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI,
"withData": withData, "withData": withData,
}).Debug("Logging out user") }).Debug("Logging out user")
if err := bridge.removeIMAPUser(ctx, user, withData); err != nil {
logrus.WithError(err).Error("Failed to remove IMAP user")
}
if err := user.Logout(ctx, withAPI); err != nil { if err := user.Logout(ctx, withAPI); err != nil {
logrus.WithError(err).Error("Failed to logout user") logrus.WithError(err).Error("Failed to logout user")
} }

View File

@ -19,44 +19,17 @@ package bridge
import ( import (
"context" "context"
"fmt"
"github.com/ProtonMail/gluon/reporter" "github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v3/internal" "github.com/ProtonMail/proton-bridge/v3/internal"
"github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/user" "github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, event events.Event) error { func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, event events.Event) {
switch event := event.(type) { switch event := event.(type) {
case events.UserAddressCreated:
if err := bridge.handleUserAddressCreated(ctx, user, event); err != nil {
return fmt.Errorf("failed to handle user address created event: %w", err)
}
case events.UserAddressEnabled:
if err := bridge.handleUserAddressEnabled(ctx, user, event); err != nil {
return fmt.Errorf("failed to handle user address enabled event: %w", err)
}
case events.UserAddressDisabled:
if err := bridge.handleUserAddressDisabled(ctx, user, event); err != nil {
return fmt.Errorf("failed to handle user address disabled event: %w", err)
}
case events.UserAddressDeleted:
if err := bridge.handleUserAddressDeleted(ctx, user, event); err != nil {
return fmt.Errorf("failed to handle user address deleted event: %w", err)
}
case events.UserRefreshed:
if err := bridge.handleUserRefreshed(ctx, user, event); err != nil {
return fmt.Errorf("failed to handle user refreshed event: %w", err)
}
case events.UserDeauth: case events.UserDeauth:
bridge.handleUserDeauth(ctx, user) bridge.handleUserDeauth(ctx, user)
@ -66,102 +39,6 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
case events.UncategorizedEventError: case events.UncategorizedEventError:
bridge.handleUncategorizedErrorEvent(event) bridge.handleUncategorizedErrorEvent(event)
} }
return nil
}
func (bridge *Bridge) handleUserAddressCreated(ctx context.Context, user *user.User, event events.UserAddressCreated) error {
if user.GetAddressMode() == vault.CombinedMode {
return nil
}
gluonID, err := bridge.serverManager.AddGluonUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
if err != nil {
return fmt.Errorf("failed to add user to IMAP server: %w", err)
}
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
return fmt.Errorf("failed to set gluon ID: %w", err)
}
return nil
}
func (bridge *Bridge) handleUserAddressEnabled(ctx context.Context, user *user.User, event events.UserAddressEnabled) error {
if user.GetAddressMode() == vault.CombinedMode {
return nil
}
gluonID, err := bridge.serverManager.AddGluonUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
if err != nil {
return fmt.Errorf("failed to add user to IMAP server: %w", err)
}
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
return fmt.Errorf("failed to set gluon ID: %w", err)
}
return nil
}
func (bridge *Bridge) handleUserAddressDisabled(ctx context.Context, user *user.User, event events.UserAddressDisabled) error {
if user.GetAddressMode() == vault.CombinedMode {
return nil
}
gluonID, ok := user.GetGluonID(event.AddressID)
if !ok {
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
}
if err := bridge.serverManager.RemoveGluonUser(ctx, gluonID); err != nil {
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
}
if err := user.RemoveGluonID(event.AddressID, gluonID); err != nil {
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
}
return nil
}
func (bridge *Bridge) handleUserAddressDeleted(ctx context.Context, user *user.User, event events.UserAddressDeleted) error {
if user.GetAddressMode() == vault.CombinedMode {
return nil
}
gluonID, ok := user.GetGluonID(event.AddressID)
if !ok {
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
}
if err := bridge.serverManager.handleRemoveGluonUser(ctx, gluonID); err != nil {
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
}
if err := user.RemoveGluonID(event.AddressID, gluonID); err != nil {
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
}
return nil
}
func (bridge *Bridge) handleUserRefreshed(ctx context.Context, user *user.User, event events.UserRefreshed) error {
return safe.RLockRet(func() error {
if event.CancelEventPool {
user.CancelSyncAndEventPoll()
}
if err := bridge.removeIMAPUser(ctx, user, true); err != nil {
return fmt.Errorf("failed to remove IMAP user: %w", err)
}
if err := bridge.addIMAPUser(ctx, user); err != nil {
return fmt.Errorf("failed to add IMAP user: %w", err)
}
return nil
}, bridge.usersLock)
} }
func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) { func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
@ -171,7 +48,7 @@ func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
}, bridge.usersLock) }, bridge.usersLock)
} }
func (bridge *Bridge) handleUserBadEvent(_ context.Context, user *user.User, event events.UserBadEvent) { func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, event events.UserBadEvent) {
safe.Lock(func() { safe.Lock(func() {
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{ if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
"user_id": user.ID(), "user_id": user.ID(),
@ -184,12 +61,7 @@ func (bridge *Bridge) handleUserBadEvent(_ context.Context, user *user.User, eve
logrus.WithError(rerr).Error("Failed to report failed event handling") logrus.WithError(rerr).Error("Failed to report failed event handling")
} }
user.CancelSyncAndEventPoll() user.OnBadEvent(ctx)
// Disable IMAP user
if err := bridge.removeIMAPUser(context.Background(), user, false); err != nil {
logrus.WithError(err).Error("Failed to remove IMAP user")
}
}, bridge.usersLock) }, bridge.usersLock)
} }

View File

@ -17,7 +17,13 @@
package events package events
import "fmt" import (
"context"
"fmt"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/watcher"
)
type Event interface { type Event interface {
fmt.Stringer fmt.Stringer
@ -28,3 +34,30 @@ type Event interface {
type eventBase struct{} type eventBase struct{}
func (eventBase) _isEvent() {} func (eventBase) _isEvent() {}
type EventPublisher interface {
PublishEvent(ctx context.Context, event Event)
}
type NullEventPublisher struct{}
func (NullEventPublisher) PublishEvent(_ context.Context, _ Event) {}
type Subscription interface {
Add(ofType ...Event) *watcher.Watcher[Event]
Remove(watcher *watcher.Watcher[Event])
}
type NullSubscription struct{}
func (n NullSubscription) Add(ofType ...Event) *watcher.Watcher[Event] {
return watcher.New[Event](&async.NoopPanicHandler{}, ofType...)
}
func (n NullSubscription) Remove(watcher *watcher.Watcher[Event]) {
watcher.Close()
}
func NewNullSubscription() *NullSubscription {
return &NullSubscription{}
}

View File

@ -0,0 +1,48 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/proton-bridge/v3/internal/events (interfaces: EventPublisher)
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
events "github.com/ProtonMail/proton-bridge/v3/internal/events"
gomock "github.com/golang/mock/gomock"
)
// MockEventPublisher is a mock of EventPublisher interface.
type MockEventPublisher struct {
ctrl *gomock.Controller
recorder *MockEventPublisherMockRecorder
}
// MockEventPublisherMockRecorder is the mock recorder for MockEventPublisher.
type MockEventPublisherMockRecorder struct {
mock *MockEventPublisher
}
// NewMockEventPublisher creates a new mock instance.
func NewMockEventPublisher(ctrl *gomock.Controller) *MockEventPublisher {
mock := &MockEventPublisher{ctrl: ctrl}
mock.recorder = &MockEventPublisherMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockEventPublisher) EXPECT() *MockEventPublisherMockRecorder {
return m.recorder
}
// PublishEvent mocks base method.
func (m *MockEventPublisher) PublishEvent(arg0 context.Context, arg1 events.Event) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "PublishEvent", arg0, arg1)
}
// PublishEvent indicates an expected call of PublishEvent.
func (mr *MockEventPublisherMockRecorder) PublishEvent(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishEvent", reflect.TypeOf((*MockEventPublisher)(nil).PublishEvent), arg0, arg1)
}

View File

@ -37,6 +37,22 @@ func (event IMAPServerStopped) String() string {
return "IMAPServerStopped" return "IMAPServerStopped"
} }
type IMAPServerClosed struct {
eventBase
}
func (event IMAPServerClosed) String() string {
return "IMAPServerClosed"
}
type IMAPServerCreated struct {
eventBase
}
func (event IMAPServerCreated) String() string {
return "IMAPServerCreated"
}
type IMAPServerError struct { type IMAPServerError struct {
eventBase eventBase

View File

@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge package files
import ( import (
"fmt" "fmt"
@ -24,7 +24,7 @@ import (
"path/filepath" "path/filepath"
) )
func moveDir(from, to string) error { func MoveDir(from, to string) error {
entries, err := os.ReadDir(from) entries, err := os.ReadDir(from)
if err != nil { if err != nil {
return err return err
@ -36,7 +36,7 @@ func moveDir(from, to string) error {
return err return err
} }
if err := moveDir(filepath.Join(from, entry.Name()), filepath.Join(to, entry.Name())); err != nil { if err := MoveDir(filepath.Join(from, entry.Name()), filepath.Join(to, entry.Name())); err != nil {
return err return err
} }
@ -61,12 +61,12 @@ func moveFile(from, to string) error {
return os.Rename(from, to) return os.Rename(from, to)
} }
func copyDir(from, to string) error { func CopyDir(from, to string) error {
entries, err := os.ReadDir(from) entries, err := os.ReadDir(from)
if err != nil { if err != nil {
return err return err
} }
if err := createIfNotExists(to, 0o700); err != nil { if err := CreateIfNotExists(to, 0o700); err != nil {
return err return err
} }
for _, entry := range entries { for _, entry := range entries {
@ -74,11 +74,11 @@ func copyDir(from, to string) error {
destPath := filepath.Join(to, entry.Name()) destPath := filepath.Join(to, entry.Name())
if entry.IsDir() { if entry.IsDir() {
if err := copyDir(sourcePath, destPath); err != nil { if err := CopyDir(sourcePath, destPath); err != nil {
return err return err
} }
} else { } else {
if err := copyFile(sourcePath, destPath); err != nil { if err := CopyFile(sourcePath, destPath); err != nil {
return err return err
} }
} }
@ -86,7 +86,7 @@ func copyDir(from, to string) error {
return nil return nil
} }
func copyFile(srcFile, dstFile string) error { func CopyFile(srcFile, dstFile string) error {
out, err := os.Create(filepath.Clean(dstFile)) out, err := os.Create(filepath.Clean(dstFile))
defer func(out *os.File) { defer func(out *os.File) {
_ = out.Close() _ = out.Close()
@ -113,7 +113,7 @@ func copyFile(srcFile, dstFile string) error {
return nil return nil
} }
func exists(filePath string) bool { func Exists(filePath string) bool {
if _, err := os.Stat(filePath); os.IsNotExist(err) { if _, err := os.Stat(filePath); os.IsNotExist(err) {
return false return false
} }
@ -121,8 +121,8 @@ func exists(filePath string) bool {
return true return true
} }
func createIfNotExists(dir string, perm os.FileMode) error { func CreateIfNotExists(dir string, perm os.FileMode) error {
if exists(dir) { if Exists(dir) {
return nil return nil
} }

View File

@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge package files
import ( import (
"os" "os"
@ -41,7 +41,7 @@ func TestMoveDir(t *testing.T) {
} }
// Move the files. // Move the files.
if err := moveDir(from, to); err != nil { if err := MoveDir(from, to); err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -385,6 +385,14 @@ Status GRPCService::Login(ServerContext *, LoginRequest const *request, Empty *)
app().log().debug(__FUNCTION__); app().log().debug(__FUNCTION__);
UsersTab &usersTab = app().mainWindow().usersTab(); UsersTab &usersTab = app().mainWindow().usersTab();
loginUsername_ = QString::fromStdString(request->username()); loginUsername_ = QString::fromStdString(request->username());
SPUser const& user = usersTab.userTable().userWithUsernameOrEmail(QString::fromStdString(request->username()));
if (user) {
qtProxy_.sendDelayedEvent(newLoginAlreadyLoggedInEvent(user->id()));
return Status::OK;
}
if (usersTab.nextUserUsernamePasswordError()) { if (usersTab.nextUserUsernamePasswordError()) {
qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::USERNAME_PASSWORD_ERROR, usersTab.usernamePasswordErrorMessage())); qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::USERNAME_PASSWORD_ERROR, usersTab.usernamePasswordErrorMessage()));
return Status::OK; return Status::OK;
@ -826,7 +834,7 @@ bool GRPCService::sendEvent(SPStreamEvent const &event) {
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
void GRPCService::finishLogin() { void GRPCService::finishLogin() {
UsersTab &usersTab = app().mainWindow().usersTab(); UsersTab &usersTab = app().mainWindow().usersTab();
SPUser user = usersTab.userWithUsername(loginUsername_); SPUser user = usersTab.userWithUsernameOrEmail(loginUsername_);
bool const alreadyExist = user.get(); bool const alreadyExist = user.get();
if (!user) { if (!user) {
user = randomUser(); user = randomUser();

View File

@ -272,8 +272,8 @@ bridgepp::SPUser UsersTab::userWithID(QString const &userID) {
/// \return The user with the given username. /// \return The user with the given username.
/// \return A null pointer if the user is not in the list. /// \return A null pointer if the user is not in the list.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
bridgepp::SPUser UsersTab::userWithUsername(QString const &username) { bridgepp::SPUser UsersTab::userWithUsernameOrEmail(QString const &username) {
return users_.userWithUsername(username); return users_.userWithUsernameOrEmail(username);
} }

View File

@ -38,7 +38,7 @@ public: // member functions.
UsersTab &operator=(UsersTab &&) = delete; ///< Disabled move assignment operator. UsersTab &operator=(UsersTab &&) = delete; ///< Disabled move assignment operator.
UserTable &userTable(); ///< Returns a reference to the user table. UserTable &userTable(); ///< Returns a reference to the user table.
bridgepp::SPUser userWithID(QString const &userID); ///< Get the user with the given ID. bridgepp::SPUser userWithID(QString const &userID); ///< Get the user with the given ID.
bridgepp::SPUser userWithUsername(QString const &username); ///< Get the user with the given username. bridgepp::SPUser userWithUsernameOrEmail(QString const &username); ///< Get the user with the given username.
bool nextUserUsernamePasswordError() const; ///< Check if next user login should trigger a username/password error. bool nextUserUsernamePasswordError() const; ///< Check if next user login should trigger a username/password error.
bool nextUserFreeUserError() const; ///< Check if next user login should trigger a Free user error. bool nextUserFreeUserError() const; ///< Check if next user login should trigger a Free user error.
bool nextUserTFARequired() const; ///< Check if next user login should requires 2FA. bool nextUserTFARequired() const; ///< Check if next user login should requires 2FA.

View File

@ -150,13 +150,16 @@ bridgepp::SPUser UserTable::userWithID(QString const &userID) {
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] username The username. /// \param[in] username The username, or any email address attached to the account.
/// \return The user with the given username. /// \return The user with the given username.
/// \return A null pointer if the user is not in the list. /// \return A null pointer if the user is not in the list.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
bridgepp::SPUser UserTable::userWithUsername(QString const &username) { bridgepp::SPUser UserTable::userWithUsernameOrEmail(QString const &username) {
QList<SPUser>::const_iterator it = std::find_if(users_.constBegin(), users_.constEnd(), [&username](SPUser const &user) -> bool { QList<SPUser>::const_iterator it = std::find_if(users_.constBegin(), users_.constEnd(), [&username](SPUser const &user) -> bool {
return user->username() == username; if (user->username().compare(username, Qt::CaseInsensitive) == 0) {
return true;
}
return user->addresses().contains(username, Qt::CaseInsensitive);
}); });
return it == users_.end() ? nullptr : *it; return it == users_.end() ? nullptr : *it;

View File

@ -40,7 +40,7 @@ public: // member functions.
void append(bridgepp::SPUser const &user); ///< Append a user. void append(bridgepp::SPUser const &user); ///< Append a user.
bridgepp::SPUser userAtIndex(qint32 index); ///< Return the user at the given index. bridgepp::SPUser userAtIndex(qint32 index); ///< Return the user at the given index.
bridgepp::SPUser userWithID(QString const &userID); ///< Return the user with a given id. bridgepp::SPUser userWithID(QString const &userID); ///< Return the user with a given id.
bridgepp::SPUser userWithUsername(QString const &username); ///< Return the user with a given username. bridgepp::SPUser userWithUsernameOrEmail(QString const &username); ///< Return the user with a given username.
qint32 indexOfUser(QString const &userID); ///< Return the index of a given User. qint32 indexOfUser(QString const &userID); ///< Return the index of a given User.
void touch(qint32 index); ///< touch the user at a given index (indicates it has been modified). void touch(qint32 index); ///< touch the user at a given index (indicates it has been modified).
void touch(QString const& userID); ///< touch the user with the given userID (indicates it has been modified). void touch(QString const& userID); ///< touch the user with the given userID (indicates it has been modified).

View File

@ -20,6 +20,7 @@
#include "QMLBackend.h" #include "QMLBackend.h"
#include "SentryUtils.h" #include "SentryUtils.h"
#include "Settings.h" #include "Settings.h"
#include <bridgepp/CLI/CLIUtils.h>
#include <bridgepp/GRPC/GRPCClient.h> #include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/Exception/Exception.h> #include <bridgepp/Exception/Exception.h>
#include <bridgepp/ProcessMonitor.h> #include <bridgepp/ProcessMonitor.h>
@ -101,19 +102,23 @@ void AppController::onFatalError(Exception const &exception) {
qApp->exit(EXIT_FAILURE); qApp->exit(EXIT_FAILURE);
} }
//****************************************************************************************************************************************************
/// \param[in] isCrashing Is the restart triggered by a crash.
//****************************************************************************************************************************************************
void AppController::restart(bool isCrashing) { void AppController::restart(bool isCrashing) {
if (!launcher_.isEmpty()) { if (launcher_.isEmpty()) {
QProcess p; return;
log_->info(QString("Restarting - App : %1 - Args : %2").arg(launcher_, launcherArgs_.join(" ")));
QStringList args = launcherArgs_;
if (isCrashing) {
args.append(noWindowFlag);
}
p.startDetached(launcher_, args);
p.waitForStarted();
} }
QProcess p;
QStringList args = stripStringParameterFromCommandLine("--session-id", launcherArgs_);
if (isCrashing) {
args.append(noWindowFlag);
}
log_->info(QString("Restarting - App : %1 - Args : %2").arg(launcher_, args.join(" ")));
p.startDetached(launcher_, args);
p.waitForStarted();
} }

View File

@ -37,6 +37,15 @@
using namespace bridgepp; using namespace bridgepp;
namespace {
QString const bugReportFile = ":qml/Resources/bug_report_flow.json";
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
// //
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -89,6 +98,8 @@ void QMLBackend::init(GRPCConfig const &serviceConfig) {
this->setUseSSLForIMAP(sslForIMAP); this->setUseSSLForIMAP(sslForIMAP);
this->setUseSSLForSMTP(sslForSMTP); this->setUseSSLForSMTP(sslForSMTP);
this->retrieveUserList(); this->retrieveUserList();
if (!reportFlow_.parse(bugReportFile))
app().log().error(QString("Cannot parse BugReportFlow description file: %1").arg(bugReportFile));
} }
@ -210,6 +221,63 @@ bool QMLBackend::areSameFileOrFolder(QUrl const &lhs, QUrl const &rhs) const {
} }
//****************************************************************************************************************************************************
/// \param[in] categoryId The id of the bug category.
/// \return Set of question for this category.
//****************************************************************************************************************************************************
QString QMLBackend::getBugCategory(quint8 categoryId) const {
return reportFlow_.getCategory(categoryId);
}
//****************************************************************************************************************************************************
/// \param[in] categoryId The id of the bug category.
/// \return Set of question for this category.
//****************************************************************************************************************************************************
QVariantList QMLBackend::getQuestionSet(quint8 categoryId) const {
QVariantList list = reportFlow_.questionSet(categoryId);
if (list.count() == 0)
app().log().error(QString("Bug category not found (id: %1)").arg(categoryId));
return list;
};
//****************************************************************************************************************************************************
/// \param[in] questionId The id of the question.
/// \param[in] answer The answer to that question.
//****************************************************************************************************************************************************
void QMLBackend::setQuestionAnswer(quint8 questionId, QString const &answer) {
if (!reportFlow_.setAnswer(questionId, answer))
app().log().error(QString("Bug Report Question not found (id: %1)").arg(questionId));
}
//****************************************************************************************************************************************************
/// \param[in] questionId The id of the question.
/// \return answer for the given question.
//****************************************************************************************************************************************************
QString QMLBackend::getQuestionAnswer(quint8 questionId) const {
return reportFlow_.getAnswer(questionId);
}
//****************************************************************************************************************************************************
/// \param[in] categoryId The id of the question set.
/// \return concatenate answers for set of questions.
//****************************************************************************************************************************************************
QString QMLBackend::collectAnswers(quint8 categoryId) const {
return reportFlow_.collectAnswers(categoryId);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void QMLBackend::clearAnswers() {
reportFlow_.clearAnswers();
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \return The value for the 'showOnStartup' property. /// \return The value for the 'showOnStartup' property.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -581,6 +649,21 @@ QStringList QMLBackend::availableKeychain() const {
} }
//****************************************************************************************************************************************************
/// \return The value for the 'bugCategories' property.
//****************************************************************************************************************************************************
QVariantList QMLBackend::bugCategories() const {
return reportFlow_.categories();
}
//****************************************************************************************************************************************************
/// \return The value for the 'bugQuestions' property.
//****************************************************************************************************************************************************
QVariantList QMLBackend::bugQuestions() const {
return reportFlow_.questions();
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \return The value for the 'currentKeychain' property. /// \return The value for the 'currentKeychain' property.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -845,14 +928,15 @@ void QMLBackend::triggerReset() const {
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] category The category of the bug.
/// \param[in] description The description of the bug. /// \param[in] description The description of the bug.
/// \param[in] address The email address. /// \param[in] address The email address.
/// \param[in] emailClient The email client. /// \param[in] emailClient The email client.
/// \param[in] includeLogs Should the logs be included in the report. /// \param[in] includeLogs Should the logs be included in the report.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
void QMLBackend::reportBug(QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const { void QMLBackend::reportBug(QString const &category, QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const {
HANDLE_EXCEPTION( HANDLE_EXCEPTION(
app().grpc().reportBug(description, address, emailClient, includeLogs); app().grpc().reportBug(category, description, address, emailClient, includeLogs);
) )
} }
@ -1181,6 +1265,7 @@ void QMLBackend::connectGrpcEvents() {
connect(client, &GRPCClient::resetFinished, this, &QMLBackend::onResetFinished); connect(client, &GRPCClient::resetFinished, this, &QMLBackend::onResetFinished);
connect(client, &GRPCClient::reportBugFinished, this, &QMLBackend::reportBugFinished); connect(client, &GRPCClient::reportBugFinished, this, &QMLBackend::reportBugFinished);
connect(client, &GRPCClient::reportBugSuccess, this, &QMLBackend::bugReportSendSuccess); connect(client, &GRPCClient::reportBugSuccess, this, &QMLBackend::bugReportSendSuccess);
connect(client, &GRPCClient::reportBugFallback, this, &QMLBackend::bugReportSendFallback);
connect(client, &GRPCClient::reportBugError, this, &QMLBackend::bugReportSendError); connect(client, &GRPCClient::reportBugError, this, &QMLBackend::bugReportSendError);
connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); }); connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); });

View File

@ -24,6 +24,7 @@
#include "BuildConfig.h" #include "BuildConfig.h"
#include "TrayIcon.h" #include "TrayIcon.h"
#include "UserList.h" #include "UserList.h"
#include <bridgepp/BugReportFlow/BugReportFlow.h>
#include <bridgepp/GRPC/GRPCClient.h> #include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/GRPC/GRPCUtils.h> #include <bridgepp/GRPC/GRPCUtils.h>
#include <bridgepp/Worker/Overseer.h> #include <bridgepp/Worker/Overseer.h>
@ -51,12 +52,18 @@ public: // member functions.
void showSettings(QString const &reason); ///< Show the settings page. void showSettings(QString const &reason); ///< Show the settings page.
void selectUser(QString const &userID, bool forceShowWindow, QString const &reason); ///< Select the user and display its account details (or login screen). void selectUser(QString const &userID, bool forceShowWindow, QString const &reason); ///< Select the user and display its account details (or login screen).
// invokable methods can be called from QML. They generally return a value, which slots cannot do. // invocable methods can be called from QML. They generally return a value, which slots cannot do.
Q_INVOKABLE static QString buildYear(); ///< Return the application build year. Q_INVOKABLE static QString buildYear(); ///< Return the application build year.
Q_INVOKABLE QPoint getCursorPos() const; ///< Retrieve the cursor position. Q_INVOKABLE QPoint getCursorPos() const; ///< Retrieve the cursor position.
Q_INVOKABLE bool isPortFree(int port) const; ///< Check if a given network port is available. Q_INVOKABLE bool isPortFree(int port) const; ///< Check if a given network port is available.
Q_INVOKABLE QString nativePath(QUrl const &url) const; ///< Retrieve the native path of a local URL. Q_INVOKABLE QString nativePath(QUrl const &url) const; ///< Retrieve the native path of a local URL.
Q_INVOKABLE bool areSameFileOrFolder(QUrl const &lhs, QUrl const &rhs) const; ///< Check if two local URL point to the same file. Q_INVOKABLE bool areSameFileOrFolder(QUrl const &lhs, QUrl const &rhs) const; ///< Check if two local URL point to the same file.
Q_INVOKABLE QString getBugCategory(quint8 categoryId) const; ///< Get a Category name.
Q_INVOKABLE QVariantList getQuestionSet(quint8 categoryId) const; ///< Retrieve the set of question for a given bug category.
Q_INVOKABLE void setQuestionAnswer(quint8 questionId, QString const &answer); ///< Feed an answer for a given question.
Q_INVOKABLE QString getQuestionAnswer(quint8 questionId) const; ///< Get the answer for a given question.
Q_INVOKABLE QString collectAnswers(quint8 categoryId) const; ///< Collect answer for a given set of questions.
Q_INVOKABLE void clearAnswers(); ///< Clear all collected answers.
public: // Qt/QML properties. Note that the NOTIFY-er signal is required even for read-only properties (QML warning otherwise) public: // Qt/QML properties. Note that the NOTIFY-er signal is required even for read-only properties (QML warning otherwise)
Q_PROPERTY(bool showOnStartup READ showOnStartup NOTIFY showOnStartupChanged) Q_PROPERTY(bool showOnStartup READ showOnStartup NOTIFY showOnStartupChanged)
@ -87,6 +94,8 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
Q_PROPERTY(QString currentEmailClient READ currentEmailClient NOTIFY currentEmailClientChanged) Q_PROPERTY(QString currentEmailClient READ currentEmailClient NOTIFY currentEmailClientChanged)
Q_PROPERTY(QStringList availableKeychain READ availableKeychain NOTIFY availableKeychainChanged) Q_PROPERTY(QStringList availableKeychain READ availableKeychain NOTIFY availableKeychainChanged)
Q_PROPERTY(QString currentKeychain READ currentKeychain NOTIFY currentKeychainChanged) Q_PROPERTY(QString currentKeychain READ currentKeychain NOTIFY currentKeychainChanged)
Q_PROPERTY(QVariantList bugCategories READ bugCategories NOTIFY bugCategoriesChanged)
Q_PROPERTY(QVariantList bugQuestions READ bugQuestions NOTIFY bugQuestionsChanged)
Q_PROPERTY(UserList *users MEMBER users_ NOTIFY usersChanged) Q_PROPERTY(UserList *users MEMBER users_ NOTIFY usersChanged)
Q_PROPERTY(bool dockIconVisible READ dockIconVisible WRITE setDockIconVisible NOTIFY dockIconVisibleChanged) Q_PROPERTY(bool dockIconVisible READ dockIconVisible WRITE setDockIconVisible NOTIFY dockIconVisibleChanged)
@ -124,6 +133,8 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
QString currentEmailClient() const; ///< Getter for the 'currentEmail' property. QString currentEmailClient() const; ///< Getter for the 'currentEmail' property.
QStringList availableKeychain() const; ///< Getter for the 'availableKeychain' property. QStringList availableKeychain() const; ///< Getter for the 'availableKeychain' property.
QString currentKeychain() const; ///< Getter for the 'currentKeychain' property. QString currentKeychain() const; ///< Getter for the 'currentKeychain' property.
QVariantList bugCategories() const; ///< Getter for the 'bugCategories' property.
QVariantList bugQuestions() const; ///< Getter for the 'bugQuestions' property.
void setDockIconVisible(bool visible); ///< Setter for the 'dockIconVisible' property. void setDockIconVisible(bool visible); ///< Setter for the 'dockIconVisible' property.
bool dockIconVisible() const;; ///< Getter for the 'dockIconVisible' property. bool dockIconVisible() const;; ///< Getter for the 'dockIconVisible' property.
@ -153,6 +164,8 @@ signals: // Signal used by the Qt property system. Many of them are unused but r
void tagChanged(QString const &tag); ///<Signal for the change of the 'tag' property. void tagChanged(QString const &tag); ///<Signal for the change of the 'tag' property.
void currentEmailClientChanged(QString const &email); ///<Signal for the change of the 'currentEmailClient' property. void currentEmailClientChanged(QString const &email); ///<Signal for the change of the 'currentEmailClient' property.
void currentKeychainChanged(QString const &keychain); ///<Signal for the change of the 'currentKeychain' property. void currentKeychainChanged(QString const &keychain); ///<Signal for the change of the 'currentKeychain' property.
void bugCategoriesChanged(QVariantList const &bugCategories); ///<Signal for the change of the 'bugCategories' property.
void bugQuestionsChanged(QVariantList const &bugQuestions); ///<Signal for the change of the 'bugQuestions' property.
void availableKeychainChanged(QStringList const &keychains); ///<Signal for the change of the 'availableKeychain' property. void availableKeychainChanged(QStringList const &keychains); ///<Signal for the change of the 'availableKeychain' property.
void hostnameChanged(QString const &hostname); ///<Signal for the change of the 'hostname' property. void hostnameChanged(QString const &hostname); ///<Signal for the change of the 'hostname' property.
void isAutostartOnChanged(bool value); ///<Signal for the change of the 'isAutostartOn' property. void isAutostartOnChanged(bool value); ///<Signal for the change of the 'isAutostartOn' property.
@ -181,7 +194,7 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
void checkUpdates() const; ///< Slot for the update check. void checkUpdates() const; ///< Slot for the update check.
void installUpdate() const; ///< Slot for the update install. void installUpdate() const; ///< Slot for the update install.
void triggerReset() const; ///< Slot for the triggering of reset. void triggerReset() const; ///< Slot for the triggering of reset.
void reportBug(QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const; ///< Slot for the bug report. void reportBug(QString const &category, QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const; ///< Slot for the bug report.
void exportTLSCertificates() const; ///< Slot for the export of the TLS certificates. void exportTLSCertificates() const; ///< Slot for the export of the TLS certificates.
void onResetFinished(); ///< Slot for the reset finish signal. void onResetFinished(); ///< Slot for the reset finish signal.
void onVersionChanged(); ///< Slot for the version change signal. void onVersionChanged(); ///< Slot for the version change signal.
@ -253,6 +266,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML
void resetFinished(); ///< Signal for the 'resetFinished' gRPC stream event. void resetFinished(); ///< Signal for the 'resetFinished' gRPC stream event.
void reportBugFinished(); ///< Signal for the 'reportBugFinished' gRPC stream event. void reportBugFinished(); ///< Signal for the 'reportBugFinished' gRPC stream event.
void bugReportSendSuccess(); ///< Signal for the 'bugReportSendSuccess' gRPC stream event. void bugReportSendSuccess(); ///< Signal for the 'bugReportSendSuccess' gRPC stream event.
void bugReportSendFallback(); ///< Signal for the 'bugReportSendFallback' gRPC stream event.
void bugReportSendError(); ///< Signal for the 'bugReportSendError' gRPC stream event. void bugReportSendError(); ///< Signal for the 'bugReportSendError' gRPC stream event.
void showMainWindow(); ///< Signal for the 'showMainWindow' gRPC stream event. void showMainWindow(); ///< Signal for the 'showMainWindow' gRPC stream event.
void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event. void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event.
@ -284,6 +298,7 @@ private: // data members
bool isInternetOn_ { true }; ///< Does bridge consider internet as on? bool isInternetOn_ { true }; ///< Does bridge consider internet as on?
QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'. QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'.
std::unique_ptr<TrayIcon> trayIcon_; ///< The tray icon for the application. std::unique_ptr<TrayIcon> trayIcon_; ///< The tray icon for the application.
bridgepp::BugReportFlow reportFlow_; ///< The bug report flow.
friend class AppController; friend class AppController;
}; };

View File

@ -6,7 +6,11 @@
<file>qml/Banner.qml</file> <file>qml/Banner.qml</file>
<file>qml/Bridge.qml</file> <file>qml/Bridge.qml</file>
<file>qml/bridgeqml.qmlproject</file> <file>qml/bridgeqml.qmlproject</file>
<file>qml/BugCategoryView.qml</file>
<file>qml/BugQuestionView.qml</file>
<file>qml/BugReportFlow.qml</file>
<file>qml/BugReportView.qml</file> <file>qml/BugReportView.qml</file>
<file>qml/CategoryItem.qml</file>
<file>qml/Configuration.qml</file> <file>qml/Configuration.qml</file>
<file>qml/ConfigurationItem.qml</file> <file>qml/ConfigurationItem.qml</file>
<file>qml/ContentWrapper.qml</file> <file>qml/ContentWrapper.qml</file>
@ -32,6 +36,7 @@
<file>qml/icons/ic-eye-slash.svg</file> <file>qml/icons/ic-eye-slash.svg</file>
<file>qml/icons/ic-eye.svg</file> <file>qml/icons/ic-eye.svg</file>
<file>qml/icons/ic-illustrative-view-html-code.svg</file> <file>qml/icons/ic-illustrative-view-html-code.svg</file>
<file>qml/icons/ic-info-circle.svg</file>
<file>qml/icons/ic-info-circle-filled.svg</file> <file>qml/icons/ic-info-circle-filled.svg</file>
<file>qml/icons/ic-info.svg</file> <file>qml/icons/ic-info.svg</file>
<file>qml/icons/ic-microsoft-outlook.svg</file> <file>qml/icons/ic-microsoft-outlook.svg</file>
@ -95,6 +100,8 @@
<file>qml/Proton/TextArea.qml</file> <file>qml/Proton/TextArea.qml</file>
<file>qml/Proton/TextField.qml</file> <file>qml/Proton/TextField.qml</file>
<file>qml/Proton/Toggle.qml</file> <file>qml/Proton/Toggle.qml</file>
<file>qml/QuestionItem.qml</file>
<file>qml/Resources/bug_report_flow.json</file>
<file>qml/SettingsItem.qml</file> <file>qml/SettingsItem.qml</file>
<file>qml/SettingsView.qml</file> <file>qml/SettingsView.qml</file>
<file>qml/SetupGuide.qml</file> <file>qml/SetupGuide.qml</file>

View File

@ -192,6 +192,8 @@ TrayIcon::TrayIcon()
if (!onLinux()) { // we disable this on linux because of a Qt bug that causes the signal to be emitted for other apps (GODT-2750) if (!onLinux()) { // we disable this on linux because of a Qt bug that causes the signal to be emitted for other apps (GODT-2750)
connect(this, &TrayIcon::messageClicked, []() { app().backend().showMainWindow("tray icon popup notification clicked"); }); connect(this, &TrayIcon::messageClicked, []() { app().backend().showMainWindow("tray icon popup notification clicked"); });
} }
this->setIcon();
this->show(); this->show();
// TrayIcon does not expose its screen, so we connect relevant screen events to our DPI change handler. // TrayIcon does not expose its screen, so we connect relevant screen events to our DPI change handler.

View File

@ -63,7 +63,7 @@ BRIDGE_BUILD_ENV= ${BRIDGE_BUILD_ENV:-"dev"}
git submodule update --init --recursive ${VCPKG_ROOT} git submodule update --init --recursive ${VCPKG_ROOT}
check_exit "Failed to initialize vcpkg as a submodule." check_exit "Failed to initialize vcpkg as a submodule."
echo submodule udpated echo submodule updated
VCPKG_EXE="${VCPKG_ROOT}/vcpkg" VCPKG_EXE="${VCPKG_ROOT}/vcpkg"
VCPKG_BOOTSTRAP="${VCPKG_ROOT}/bootstrap-vcpkg.sh" VCPKG_BOOTSTRAP="${VCPKG_ROOT}/bootstrap-vcpkg.sh"

View File

@ -137,12 +137,21 @@ bool checkSingleInstance(QLockFile &lock) {
if (lock.getLockInfo(&pid, &hostname, &appName)) { if (lock.getLockInfo(&pid, &hostname, &appName)) {
details = QString("(PID : %1 - Host : %2 - App : %3)").arg(pid).arg(hostname, appName); details = QString("(PID : %1 - Host : %2 - App : %3)").arg(pid).arg(hostname, appName);
} }
if (lock.error() == QLockFile::LockFailedError) {
// This happens if a stale lock file exists and another process uses that PID.
// Try removing the stale file, which will fail if a real process is holding a
// file-level lock. A false error is more problematic than not locking properly
// on corner-case systems.
if (lock.removeStaleLockFile() && lock.tryLock()) {
app().log().info("Removed stale lock file");
app().log().info(QString("lock file created %1").arg(lock.fileName()));
return true;
}
}
app().log().error(QString("Instance already exists %1 %2").arg(lock.fileName(), details)); app().log().error(QString("Instance already exists %1 %2").arg(lock.fileName(), details));
return false; return false;
} else {
app().log().info(QString("lock file created %1").arg(lock.fileName()));
} }
app().log().info(QString("lock file created %1").arg(lock.fileName()));
return true; return true;
} }

View File

@ -1,251 +1,252 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import Proton import Proton
Item { Item {
id: root id: root
enum ViewType {
property ColorScheme colorScheme SmallView,
property var user LargeView
}
property var _spacing: 12 * ProtonStyle.px property var _spacing: 12 * ProtonStyle.px
property ColorScheme colorScheme
property color progressColor : { property color progressColor: {
if (!root.enabled) return root.colorScheme.text_weak if (!root.enabled)
if (root.type == AccountDelegate.SmallView) return root.colorScheme.text_weak return root.colorScheme.text_weak;
if (root.user && root.user.isSyncing) return root.colorScheme.text_weak if (root.type === AccountDelegate.SmallView)
if (root.progressRatio < .50) return root.colorScheme.signal_success return root.colorScheme.text_weak;
if (root.progressRatio < .75) return root.colorScheme.signal_warning if (root.user && root.user.isSyncing)
return root.colorScheme.signal_danger return root.colorScheme.text_weak;
if (root.progressRatio < .50)
return root.colorScheme.signal_success;
if (root.progressRatio < .75)
return root.colorScheme.signal_warning;
return root.colorScheme.signal_danger;
} }
property real progressRatio: { property real progressRatio: {
if (!root.user) if (!root.user)
return 0 return 0;
return root.user.isSyncing ? root.user.syncProgress : reasonableFraction(root.user.usedBytes, root.user.totalBytes) return root.user.isSyncing ? root.user.syncProgress : reasonableFraction(root.user.usedBytes, root.user.totalBytes);
} }
property string totalSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.totalBytes) : 0) property string totalSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.totalBytes) : 0)
property var type: AccountDelegate.SmallView
property string usedSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.usedBytes) : 0) property string usedSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.usedBytes) : 0)
property var user
function reasonableFraction(used, total){
var usedSafe = root.reasonableBytes(used)
var totalSafe = root.reasonableBytes(total)
if (totalSafe == 0 || usedSafe == 0) return 0
if (totalSafe <= usedSafe) return 1
return usedSafe / totalSafe
}
function reasonableBytes(bytes){
var safeBytes = bytes+0
if (safeBytes != bytes) return 0
if (safeBytes < 0) return 0
return Math.ceil(safeBytes)
}
function spaceWithUnits(bytes){
if (bytes*1 !== bytes || bytes == 0 ) return "0 kB"
var units = ['B',"kB", "MB", "GB", "TB"];
var i = parseInt(Math.floor(Math.log(bytes)/Math.log(1024)));
return Math.round(bytes*10 / Math.pow(1024, i))/10 + " " + units[i]
}
function primaryEmail() { function primaryEmail() {
return root.user ? root.user.primaryEmailOrUsername() : "" return root.user ? root.user.primaryEmailOrUsername() : "";
}
function reasonableBytes(bytes) {
const safeBytes = bytes + 0;
if (safeBytes !== bytes)
return 0;
if (safeBytes < 0)
return 0;
return Math.ceil(safeBytes);
}
function reasonableFraction(used, total) {
const usedSafe = root.reasonableBytes(used);
const totalSafe = root.reasonableBytes(total);
if (totalSafe === 0 || usedSafe === 0)
return 0;
if (totalSafe <= usedSafe)
return 1;
return usedSafe / totalSafe;
}
function spaceWithUnits(bytes) {
if (bytes * 1 !== bytes || bytes === 0)
return "0 kB";
const units = ['B', "kB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes * 10 / Math.pow(1024, i)) / 10 + " " + units[i];
} }
// width expected to be set by parent object // width expected to be set by parent object
implicitHeight : children[0].implicitHeight implicitHeight: children[0].implicitHeight
enum ViewType{
SmallView, LargeView
}
property var type : AccountDelegate.SmallView
RowLayout { RowLayout {
spacing: root._spacing spacing: root._spacing
anchors { anchors {
top: root.top
left: root.left left: root.left
right: root.right right: root.right
top: root.top
} }
Rectangle { Rectangle {
id: avatar id: avatar
Layout.fillHeight: true Layout.fillHeight: true
Layout.preferredWidth: height Layout.preferredWidth: height
color: root.colorScheme.background_avatar
radius: ProtonStyle.avatar_radius radius: ProtonStyle.avatar_radius
color: root.colorScheme.background_avatar
Label { Label {
colorScheme: root.colorScheme
anchors.fill: parent anchors.fill: parent
text: root.user ? root.user.avatarText.toUpperCase(): "" color: "#FFFFFF"
colorScheme: root.colorScheme
font.weight: Font.Normal
horizontalAlignment: Qt.AlignHCenter
text: root.user ? root.user.avatarText.toUpperCase() : ""
type: { type: {
switch (root.type) { switch (root.type) {
case AccountDelegate.SmallView: return Label.Body case AccountDelegate.SmallView:
case AccountDelegate.LargeView: return Label.Title return Label.Body;
case AccountDelegate.LargeView:
return Label.Title;
} }
} }
font.weight: Font.Normal
color: "#FFFFFF"
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter verticalAlignment: Qt.AlignVCenter
} }
} }
ColumnLayout { ColumnLayout {
id: account id: account
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
spacing: 0 spacing: 0
Label { Label {
id: labelEmail id: labelEmail
Layout.maximumWidth: root.width - (root._spacing + avatar.width) Layout.maximumWidth: root.width - (root._spacing + avatar.width)
colorScheme: root.colorScheme colorScheme: root.colorScheme
elide: Text.ElideMiddle
text: primaryEmail() text: primaryEmail()
type: { type: {
switch (root.type) { switch (root.type) {
case AccountDelegate.SmallView: return Label.Body case AccountDelegate.SmallView:
case AccountDelegate.LargeView: return Label.Title return Label.Body;
case AccountDelegate.LargeView:
return Label.Title;
} }
} }
elide: Text.ElideMiddle
MouseArea { MouseArea {
id: labelArea id: labelArea
anchors.fill:parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
} }
ToolTip { ToolTip {
id: toolTipEmail id: toolTipEmail
visible: labelArea.containsMouse && labelEmail.truncated
text: primaryEmail()
delay: 1000 delay: 1000
text: primaryEmail()
visible: labelArea.containsMouse && labelEmail.truncated
background: Rectangle { background: Rectangle {
border.color: root.colorScheme.background_strong border.color: root.colorScheme.background_strong
color: root.colorScheme.background_norm color: root.colorScheme.background_norm
} }
contentItem: Text { contentItem: Text {
color: root.colorScheme.text_norm color: root.colorScheme.text_norm
text: toolTipEmail.text text: toolTipEmail.text
} }
} }
} }
Item {
Item { implicitHeight: root.type == AccountDelegate.LargeView ? 6 * ProtonStyle.px : 0 } implicitHeight: root.type === AccountDelegate.LargeView ? 6 * ProtonStyle.px : 0
}
RowLayout { RowLayout {
spacing: 0 spacing: 0
Label { Label {
color: root.progressColor
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: { text: {
if (!root.user) if (!root.user)
return qsTr("Signed out") return qsTr("Signed out");
switch (root.user.state) { switch (root.user.state) {
case EUserState.SignedOut: case EUserState.SignedOut:
default: default:
return qsTr("Signed out") return qsTr("Signed out");
case EUserState.Locked: case EUserState.Locked:
return qsTr("Connecting") + dotsTimer.dots return qsTr("Connecting") + dotsTimer.dots;
case EUserState.Connected: case EUserState.Connected:
if (root.user.isSyncing) if (root.user.isSyncing)
return qsTr("Synchronizing (%1%)").arg(Math.floor(root.user.syncProgress * 100)) + dotsTimer.dots return qsTr("Synchronizing (%1%)").arg(Math.floor(root.user.syncProgress * 100)) + dotsTimer.dots;
else else
return root.usedSpace return root.usedSpace;
} }
} }
Timer { // dots animation while connecting & syncing.
id:dotsTimer
property string dots: ""
interval: 500;
repeat: true;
running: (root.user != null) && ((root.user.state === EUserState.Locked) || (root.user.isSyncing))
onTriggered: {
dots += "."
if (dots.length > 3)
dots = ""
}
onRunningChanged: {
dots = ""
}
}
color: root.progressColor
type: { type: {
switch (root.type) { switch (root.type) {
case AccountDelegate.SmallView: return Label.Caption case AccountDelegate.SmallView:
case AccountDelegate.LargeView: return Label.Body return Label.Caption;
case AccountDelegate.LargeView:
return Label.Body;
}
}
Timer {
// dots animation while connecting & syncing.
id: dotsTimer
property string dots: ""
interval: 500
repeat: true
running: (root.user != null) && ((root.user.state === EUserState.Locked) || (root.user.isSyncing))
onRunningChanged: {
dots = "";
}
onTriggered: {
dots += ".";
if (dots.length > 3)
dots = "";
} }
} }
} }
Label { Label {
colorScheme: root.colorScheme
text: root.user && root.user.state == EUserState.Connected && !root.user.isSyncing ? " / " + root.totalSpace : ""
color: root.colorScheme.text_weak color: root.colorScheme.text_weak
colorScheme: root.colorScheme
text: root.user && root.user.state === EUserState.Connected && !root.user.isSyncing ? " / " + root.totalSpace : ""
type: { type: {
switch (root.type) { switch (root.type) {
case AccountDelegate.SmallView: return Label.Caption case AccountDelegate.SmallView:
case AccountDelegate.LargeView: return Label.Body return Label.Caption;
case AccountDelegate.LargeView:
return Label.Body;
} }
} }
} }
} }
Item {
Item { implicitHeight: root.type == AccountDelegate.LargeView ? 3 * ProtonStyle.px : 0 } implicitHeight: root.type === AccountDelegate.LargeView ? 3 * ProtonStyle.px : 0
}
Rectangle { Rectangle {
id: progress_bar id: progress_bar
visible: root.user ? root.type == AccountDelegate.LargeView : false color: root.colorScheme.border_weak
width: 140 * ProtonStyle.px
height: 4 * ProtonStyle.px height: 4 * ProtonStyle.px
radius: ProtonStyle.progress_bar_radius radius: ProtonStyle.progress_bar_radius
color: root.colorScheme.border_weak visible: root.user ? root.type === AccountDelegate.LargeView : false
width: 140 * ProtonStyle.px
Rectangle { Rectangle {
id: progress_bar_filled id: progress_bar_filled
radius: ProtonStyle.progress_bar_radius
color: root.progressColor color: root.progressColor
visible: root.user ? parent.visible && (root.user.state == EUserState.Connected): false radius: ProtonStyle.progress_bar_radius
visible: root.user ? parent.visible && (root.user.state === EUserState.Connected) : false
width: Math.min(1, Math.max(0.02, root.progressRatio)) * parent.width
anchors { anchors {
top : parent.top bottom: parent.bottom
bottom : parent.bottom left: parent.left
left : parent.left top: parent.top
} }
width: Math.min(1,Math.max(0.02,root.progressRatio)) * parent.width
} }
} }
} }
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
} }

View File

@ -1,42 +1,35 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import Proton import Proton
Item { Item {
id: root id: root
property bool _connected: root.user ? root.user.state === EUserState.Connected : false
property int _contentWidth: 640
property int _detailsMargin: 25
property int _lineThickness: 1
property int _spacing: 20
property int _topMargin: 32
property ColorScheme colorScheme property ColorScheme colorScheme
property var notifications property var notifications
property var user property var user
signal showSignIn
signal showSetupGuide(var user, string address) signal showSetupGuide(var user, string address)
signal showSignIn
property int _contentWidth: 640
property int _topMargin: 32
property int _detailsMargin: 25
property int _spacing: 20
property int _lineThickness: 1
property bool _connected: root.user ? root.user.state === EUserState.Connected : false
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
@ -45,6 +38,7 @@ Item {
ScrollView { ScrollView {
id: scrollView id: scrollView
anchors.fill: parent anchors.fill: parent
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds
ColumnLayout { ColumnLayout {
@ -54,16 +48,16 @@ Item {
Rectangle { Rectangle {
id: topArea id: topArea
color: root.colorScheme.background_norm
clip: true
Layout.fillWidth: true Layout.fillWidth: true
clip: true
color: root.colorScheme.background_norm
implicitHeight: childrenRect.height implicitHeight: childrenRect.height
ColumnLayout { ColumnLayout {
id: topLayout id: topLayout
width: _contentWidth
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: _spacing spacing: _spacing
width: _contentWidth
RowLayout { RowLayout {
// account delegate with action buttons // account delegate with action buttons
@ -73,83 +67,82 @@ Item {
AccountDelegate { AccountDelegate {
Layout.fillWidth: true Layout.fillWidth: true
colorScheme: root.colorScheme colorScheme: root.colorScheme
user: root.user
type: AccountDelegate.LargeView
enabled: _connected enabled: _connected
type: AccountDelegate.LargeView
user: root.user
} }
Button { Button {
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Sign out")
secondary: true secondary: true
text: qsTr("Sign out")
visible: _connected visible: _connected
onClicked: { onClicked: {
if (!root.user) if (!root.user)
return; return;
root.user.logout(); root.user.logout();
} }
} }
Button { Button {
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Sign in")
secondary: true secondary: true
text: qsTr("Sign in")
visible: root.user ? (root.user.state === EUserState.SignedOut) : false visible: root.user ? (root.user.state === EUserState.SignedOut) : false
onClicked: { onClicked: {
if (!root.user) if (!root.user)
return; return;
root.showSignIn(); root.showSignIn();
} }
} }
Button { Button {
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme colorScheme: root.colorScheme
icon.source: "/qml/icons/ic-trash.svg" icon.source: "/qml/icons/ic-trash.svg"
secondary: true secondary: true
visible: root.user ? root.user.state !== EUserState.Locked : false
onClicked: { onClicked: {
if (!root.user) if (!root.user)
return; return;
root.notifications.askDeleteAccount(root.user); root.notifications.askDeleteAccount(root.user);
} }
visible: root.user ? root.user.state !== EUserState.Locked : false
} }
} }
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
height: root._lineThickness
color: root.colorScheme.border_weak color: root.colorScheme.border_weak
height: root._lineThickness
} }
SettingsItem { SettingsItem {
colorScheme: root.colorScheme Layout.fillWidth: true
text: qsTr("Email clients")
actionText: qsTr("Configure") actionText: qsTr("Configure")
colorScheme: root.colorScheme
description: qsTr("Using the mailbox details below (re)configure your client.") description: qsTr("Using the mailbox details below (re)configure your client.")
showSeparator: splitMode.visible
text: qsTr("Email clients")
type: SettingsItem.Button type: SettingsItem.Button
visible: _connected && (!root.user.splitMode) || (root.user.addresses.length === 1) visible: _connected && (!root.user.splitMode) || (root.user.addresses.length === 1)
showSeparator: splitMode.visible
onClicked: { onClicked: {
if (!root.user) if (!root.user)
return; return;
root.showSetupGuide(root.user, user.addresses[0]); root.showSetupGuide(root.user, user.addresses[0]);
} }
Layout.fillWidth: true
} }
SettingsItem { SettingsItem {
id: splitMode id: splitMode
colorScheme: root.colorScheme Layout.fillWidth: true
text: qsTr("Split addresses")
description: qsTr("Setup multiple email addresses individually.")
type: SettingsItem.Toggle
checked: root.user ? root.user.splitMode : false checked: root.user ? root.user.splitMode : false
visible: _connected && root.user.addresses.length > 1 colorScheme: root.colorScheme
description: qsTr("Setup multiple email addresses individually.")
showSeparator: addressSelector.visible showSeparator: addressSelector.visible
text: qsTr("Split addresses")
type: SettingsItem.Toggle
visible: _connected && root.user.addresses.length > 1
onClicked: { onClicked: {
if (!splitMode.checked) { if (!splitMode.checked) {
root.notifications.askEnableSplitMode(user); root.notifications.askEnableSplitMode(user);
@ -158,26 +151,23 @@ Item {
root.user.toggleSplitMode(!splitMode.checked); root.user.toggleSplitMode(!splitMode.checked);
} }
} }
Layout.fillWidth: true
} }
RowLayout { RowLayout {
Layout.fillWidth: true
Layout.bottomMargin: _spacing Layout.bottomMargin: _spacing
Layout.fillWidth: true
visible: _connected && root.user.splitMode visible: _connected && root.user.splitMode
ComboBox { ComboBox {
id: addressSelector id: addressSelector
colorScheme: root.colorScheme
Layout.fillWidth: true Layout.fillWidth: true
colorScheme: root.colorScheme
model: root.user ? root.user.addresses : null model: root.user ? root.user.addresses : null
} }
Button { Button {
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Configure")
secondary: true secondary: true
text: qsTr("Configure")
onClicked: { onClicked: {
if (!root.user) if (!root.user)
return; return;
@ -185,25 +175,23 @@ Item {
} }
} }
} }
Rectangle { Rectangle {
height: 0 height: 0
} // just for some extra space before separator } // just for some extra space before separator
} }
} }
Rectangle { Rectangle {
id: bottomArea id: bottomArea
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: bottomLayout.implicitHeight
color: root.colorScheme.background_weak color: root.colorScheme.background_weak
implicitHeight: bottomLayout.implicitHeight
ColumnLayout { ColumnLayout {
id: bottomLayout id: bottomLayout
width: _contentWidth
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: _spacing spacing: _spacing
visible: _connected visible: _connected
width: _contentWidth
Label { Label {
Layout.topMargin: _detailsMargin Layout.topMargin: _detailsMargin
@ -211,35 +199,34 @@ Item {
text: qsTr("Mailbox details") text: qsTr("Mailbox details")
type: Label.Body_semibold type: Label.Body_semibold
} }
RowLayout { RowLayout {
id: configuration id: configuration
spacing: _spacing
Layout.fillWidth: true
Layout.fillHeight: true
property string currentAddress: addressSelector.displayText property string currentAddress: addressSelector.displayText
Configuration { Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
colorScheme: root.colorScheme spacing: _spacing
title: qsTr("IMAP")
hostname: Backend.hostname
port: Backend.imapPort.toString()
username: configuration.currentAddress
password: root.user ? root.user.password : ""
security: Backend.useSSLForIMAP ? "SSL" : "STARTTLS"
}
Configuration { Configuration {
Layout.fillWidth: true Layout.fillWidth: true
colorScheme: root.colorScheme colorScheme: root.colorScheme
title: qsTr("SMTP")
hostname: Backend.hostname hostname: Backend.hostname
port: Backend.smtpPort.toString()
username: configuration.currentAddress
password: root.user ? root.user.password : "" password: root.user ? root.user.password : ""
port: Backend.imapPort.toString()
security: Backend.useSSLForIMAP ? "SSL" : "STARTTLS"
title: qsTr("IMAP")
username: configuration.currentAddress
}
Configuration {
Layout.fillWidth: true
colorScheme: root.colorScheme
hostname: Backend.hostname
password: root.user ? root.user.password : ""
port: Backend.smtpPort.toString()
security: Backend.useSSLForSMTP ? "SSL" : "STARTTLS" security: Backend.useSSLForSMTP ? "SSL" : "STARTTLS"
title: qsTr("SMTP")
username: configuration.currentAddress
} }
} }
} }

View File

@ -1,25 +1,19 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
import Proton import Proton
import Notifications import Notifications
@ -27,34 +21,28 @@ Popup {
id: root id: root
property ColorScheme colorScheme property ColorScheme colorScheme
property Notification notification
property var mainWindow property var mainWindow
property Notification notification
topMargin: 37
leftMargin: (mainWindow.width - root.implicitWidth)/2
implicitHeight: contentLayout.implicitHeight + contentLayout.anchors.topMargin + contentLayout.anchors.bottomMargin implicitHeight: contentLayout.implicitHeight + contentLayout.anchors.topMargin + contentLayout.anchors.bottomMargin
implicitWidth: 600 // contentLayout.implicitWidth + contentLayout.anchors.leftMargin + contentLayout.anchors.rightMargin implicitWidth: 600 // contentLayout.implicitWidth + contentLayout.anchors.leftMargin + contentLayout.anchors.rightMargin
leftMargin: (mainWindow.width - root.implicitWidth) / 2
popupType: ApplicationWindow.PopupType.Banner
shouldShow: notification ? (notification.active && !notification.dismissed) : false
modal: false modal: false
popupType: ApplicationWindow.PopupType.Banner
shouldShow: notification ? (notification.active && !notification.dismissed) : false
topMargin: 37
Action { Action {
id: defaultDismissAction id: defaultDismissAction
text: qsTr("OK") text: qsTr("OK")
onTriggered: { onTriggered: {
if (!root.notification) { if (!root.notification) {
return return;
} }
root.notification.dismissed = true;
root.notification.dismissed = true
} }
} }
RowLayout { RowLayout {
id: contentLayout id: contentLayout
anchors.fill: parent anchors.fill: parent
@ -63,170 +51,148 @@ Popup {
Item { Item {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
clip: true clip: true
implicitHeight: children[1].implicitHeight + children[1].anchors.topMargin + children[1].anchors.bottomMargin implicitHeight: children[1].implicitHeight + children[1].anchors.topMargin + children[1].anchors.bottomMargin
implicitWidth: children[1].implicitWidth + children[1].anchors.leftMargin + children[1].anchors.rightMargin implicitWidth: children[1].implicitWidth + children[1].anchors.leftMargin + children[1].anchors.rightMargin
Rectangle { Rectangle {
anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.left: parent.left anchors.left: parent.left
width: parent.width + 10 anchors.top: parent.top
radius: ProtonStyle.banner_radius
color: { color: {
if (!root.notification) { if (!root.notification) {
return "transparent" return "transparent";
} }
switch (root.notification.type) { switch (root.notification.type) {
case Notification.NotificationType.Info: case Notification.NotificationType.Info:
return root.colorScheme.signal_info return root.colorScheme.signal_info;
case Notification.NotificationType.Success: case Notification.NotificationType.Success:
return root.colorScheme.signal_success return root.colorScheme.signal_success;
case Notification.NotificationType.Warning: case Notification.NotificationType.Warning:
return root.colorScheme.signal_warning return root.colorScheme.signal_warning;
case Notification.NotificationType.Danger: case Notification.NotificationType.Danger:
return root.colorScheme.signal_danger return root.colorScheme.signal_danger;
} }
} }
radius: ProtonStyle.banner_radius
width: parent.width + 10
} }
RowLayout { RowLayout {
anchors.fill: parent
anchors.topMargin: 14
anchors.bottomMargin: 14 anchors.bottomMargin: 14
anchors.fill: parent
anchors.leftMargin: 16 anchors.leftMargin: 16
anchors.topMargin: 14
spacing: 8 spacing: 8
ColorImage { ColorImage {
color: root.colorScheme.text_invert
width: 24
height: 24
sourceSize.width: 24
sourceSize.height: 24
Layout.preferredHeight: 24 Layout.preferredHeight: 24
Layout.preferredWidth: 24 Layout.preferredWidth: 24
color: root.colorScheme.text_invert
height: 24
source: { source: {
if (!root.notification) { if (!root.notification) {
return "" return "";
} }
switch (root.notification.type) { switch (root.notification.type) {
case Notification.NotificationType.Info: case Notification.NotificationType.Info:
return "/qml/icons/ic-info-circle-filled.svg" return "/qml/icons/ic-info-circle-filled.svg";
case Notification.NotificationType.Success: case Notification.NotificationType.Success:
return "/qml/icons/ic-info-circle-filled.svg" return "/qml/icons/ic-info-circle-filled.svg";
case Notification.NotificationType.Warning: case Notification.NotificationType.Warning:
return "/qml/icons/ic-exclamation-circle-filled.svg" return "/qml/icons/ic-exclamation-circle-filled.svg";
case Notification.NotificationType.Danger: case Notification.NotificationType.Danger:
return "/qml/icons/ic-exclamation-circle-filled.svg" return "/qml/icons/ic-exclamation-circle-filled.svg";
} }
} }
sourceSize.height: 24
sourceSize.width: 24
width: 24
} }
Label { Label {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
Layout.leftMargin: 16 Layout.leftMargin: 16
color: root.colorScheme.text_invert color: root.colorScheme.text_invert
colorScheme: root.colorScheme
text: root.notification ? root.notification.description : "" text: root.notification ? root.notification.description : ""
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
} }
} }
Rectangle { Rectangle {
Layout.fillHeight: true Layout.fillHeight: true
width: 1
color: { color: {
if (!root.notification) { if (!root.notification) {
return "transparent" return "transparent";
} }
switch (root.notification.type) { switch (root.notification.type) {
case Notification.NotificationType.Info: case Notification.NotificationType.Info:
return root.colorScheme.signal_info_active return root.colorScheme.signal_info_active;
case Notification.NotificationType.Success: case Notification.NotificationType.Success:
return root.colorScheme.signal_success_active return root.colorScheme.signal_success_active;
case Notification.NotificationType.Warning: case Notification.NotificationType.Warning:
return root.colorScheme.signal_warning_active return root.colorScheme.signal_warning_active;
case Notification.NotificationType.Danger: case Notification.NotificationType.Danger:
return root.colorScheme.signal_danger_active return root.colorScheme.signal_danger_active;
} }
} }
width: 1
} }
Button { Button {
colorScheme: root.colorScheme
Layout.fillHeight: true
id: actionButton id: actionButton
Layout.fillHeight: true
action: (root.notification && root.notification.action.length > 0) ? root.notification.action[0] : defaultDismissAction action: (root.notification && root.notification.action.length > 0) ? root.notification.action[0] : defaultDismissAction
colorScheme: root.colorScheme
background: Item { background: Item {
clip: true clip: true
Rectangle { Rectangle {
anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.right: parent.right anchors.right: parent.right
width: parent.width + 10 anchors.top: parent.top
radius: ProtonStyle.banner_radius
color: { color: {
if (!root.notification) { if (!root.notification) {
return "transparent" return "transparent";
} }
let norm;
var norm let hover;
var hover let active;
var active
switch (root.notification.type) { switch (root.notification.type) {
case Notification.NotificationType.Info: case Notification.NotificationType.Info:
norm = root.colorScheme.signal_info norm = root.colorScheme.signal_info;
hover = root.colorScheme.signal_info_hover hover = root.colorScheme.signal_info_hover;
active = root.colorScheme.signal_info_active active = root.colorScheme.signal_info_active;
break; break;
case Notification.NotificationType.Success: case Notification.NotificationType.Success:
norm = root.colorScheme.signal_success norm = root.colorScheme.signal_success;
hover = root.colorScheme.signal_success_hover hover = root.colorScheme.signal_success_hover;
active = root.colorScheme.signal_success_active active = root.colorScheme.signal_success_active;
break; break;
case Notification.NotificationType.Warning: case Notification.NotificationType.Warning:
norm = root.colorScheme.signal_warning norm = root.colorScheme.signal_warning;
hover = root.colorScheme.signal_warning_hover hover = root.colorScheme.signal_warning_hover;
active = root.colorScheme.signal_warning_active active = root.colorScheme.signal_warning_active;
break; break;
case Notification.NotificationType.Danger: case Notification.NotificationType.Danger:
norm = root.colorScheme.signal_danger norm = root.colorScheme.signal_danger;
hover = root.colorScheme.signal_danger_hover hover = root.colorScheme.signal_danger_hover;
active = root.colorScheme.signal_danger_active active = root.colorScheme.signal_danger_active;
break; break;
} }
if (actionButton.down) { if (actionButton.down) {
return active return active;
} }
if (actionButton.enabled && (actionButton.highlighted || actionButton.hovered || actionButton.checked)) { if (actionButton.enabled && (actionButton.highlighted || actionButton.hovered || actionButton.checked)) {
return hover return hover;
} }
if (actionButton.loading) { if (actionButton.loading) {
return hover return hover;
} }
return norm;
return norm
} }
radius: ProtonStyle.banner_radius
width: parent.width + 10
} }
} }
} }

View File

@ -1,129 +1,112 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml import QtQml
import QtQuick import QtQuick
import QtQuick.Window import QtQuick.Window
import Qt.labs.platform import Qt.labs.platform
import Proton import Proton
import Notifications import Notifications
QtObject { QtObject {
id: root id: root
function bound(num, lowerLimit, upperLimit) { property MainWindow _mainWindow: MainWindow {
return Math.max(lowerLimit, Math.min(upperLimit, num)) id: mainWindow
notifications: root._notifications
title: root.title
visible: false
onVisibleChanged: {
Backend.dockIconVisible = visible;
}
Connections {
function onColorSchemeNameChanged(scheme) {
root.setColorScheme();
}
function onDiskCacheUnavailable() {
mainWindow.showAndRise();
}
function onHideMainWindow() {
mainWindow.hide();
}
target: Backend
}
} }
property var title: Backend.appname
property Notifications _notifications: Notifications { property Notifications _notifications: Notifications {
id: notifications id: notifications
frontendMain: mainWindow frontendMain: mainWindow
} }
property NotificationFilter _trayNotificationFilter: NotificationFilter { property NotificationFilter _trayNotificationFilter: NotificationFilter {
id: trayNotificationFilter id: trayNotificationFilter
source: root._notifications ? root._notifications.all : undefined source: root._notifications ? root._notifications.all : undefined
onTopmostChanged: {
if (topmost) { onTopmostChanged: {
switch (topmost.type) { if (topmost) {
case Notification.NotificationType.Danger: switch (topmost.type) {
Backend.setErrorTrayIcon(topmost.brief, topmost.icon) case Notification.NotificationType.Danger:
return Backend.setErrorTrayIcon(topmost.brief, topmost.icon);
case Notification.NotificationType.Warning: return;
Backend.setWarnTrayIcon(topmost.brief, topmost.icon) case Notification.NotificationType.Warning:
return Backend.setWarnTrayIcon(topmost.brief, topmost.icon);
case Notification.NotificationType.Info: return;
Backend.setUpdateTrayIcon(topmost.brief, topmost.icon) case Notification.NotificationType.Info:
return Backend.setUpdateTrayIcon(topmost.brief, topmost.icon);
return;
} }
} }
Backend.setNormalTrayIcon() Backend.setNormalTrayIcon();
} }
} }
property var title: Backend.appname
function bound(num, lowerLimit, upperLimit) {
property MainWindow _mainWindow: MainWindow { return Math.max(lowerLimit, Math.min(upperLimit, num));
id: mainWindow }
visible: false function setColorScheme() {
if (Backend.colorSchemeName === "light")
title: root.title ProtonStyle.currentStyle = ProtonStyle.lightStyle;
notifications: root._notifications if (Backend.colorSchemeName === "dark")
ProtonStyle.currentStyle = ProtonStyle.darkStyle;
onVisibleChanged: {
Backend.dockIconVisible = visible
}
Connections {
target: Backend
function onDiskCacheUnavailable() {
mainWindow.showAndRise()
}
function onColorSchemeNameChanged(scheme) { root.setColorScheme() }
function onHideMainWindow() {
mainWindow.hide();
}
}
} }
Component.onCompleted: { Component.onCompleted: {
if (!Backend) { if (!Backend) {
console.log("Backend not loaded") console.log("Backend not loaded");
} }
root.setColorScheme();
root.setColorScheme()
if (!Backend.users) { if (!Backend.users) {
console.log("users not loaded") console.log("users not loaded");
} }
const c = Backend.users.count;
var c = Backend.users.count const u = Backend.users.get(0);
var u = Backend.users.get(0)
// DEBUG // DEBUG
if (c !== 0) { if (c !== 0) {
console.log("users non zero", c) console.log("users non zero", c);
console.log("first user", u ) console.log("first user", u);
} }
if (c === 0) { if (c === 0) {
mainWindow.showAndRise() mainWindow.showAndRise();
} }
if (u) { if (u) {
if (c === 1 && (u.state === EUserState.SignedOut)) { if (c === 1 && (u.state === EUserState.SignedOut)) {
mainWindow.showAndRise() mainWindow.showAndRise();
} }
} }
Backend.guiReady();
Backend.guiReady() if (Backend.showOnStartup || Backend.showSplashScreen) {
mainWindow.showAndRise();
if (Backend.showOnStartup || Backend.showSplashScreen) {
mainWindow.showAndRise()
} }
}
function setColorScheme() {
if (Backend.colorSchemeName === "light") ProtonStyle.currentStyle = ProtonStyle.lightStyle
if (Backend.colorSchemeName === "dark") ProtonStyle.currentStyle = ProtonStyle.darkStyle
} }
} }

View File

@ -0,0 +1,52 @@
// Copyright (c) 2023 Proton AG
// This file is part of Proton Mail Bridge.
// Proton Mail Bridge 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 3 of the License, or
// (at your option) any later version.
// Proton Mail Bridge 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.
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
SettingsView {
id: root
signal categorySelected(int categoryId)
fillHeight: true
property var categories: Backend.bugCategories
Label {
Layout.fillWidth: true
colorScheme: root.colorScheme
text: qsTr("What do you want to report?")
type: Label.Heading
}
Repeater {
model: root.categories
CategoryItem {
Layout.fillWidth: true
actionIcon: "/qml/icons/ic-chevron-right.svg"
colorScheme: root.colorScheme
text: modelData.name
hint: modelData.hint ? modelData.hint: ""
onClicked: root.categorySelected(index)
}
}
// fill height so the footer label will always be attached to the bottom
Item {
Layout.fillHeight: true
}
}

View File

@ -0,0 +1,130 @@
// Copyright (c) 2023 Proton AG
// This file is part of Proton Mail Bridge.
// Proton Mail Bridge 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 3 of the License, or
// (at your option) any later version.
// Proton Mail Bridge 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.
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
SettingsView {
id: root
property var questions:Backend.bugQuestions
property var categoryId:0
property var questionSet:ListModel{}
property bool error: questionRepeater.error
signal questionAnswered
function setCategoryId(catId) {
root.categoryId = catId;
}
function submit() {
root.questionAnswered();
}
fillHeight: true
onCategoryIdChanged: {
root.questionSet = Backend.getQuestionSet(root.categoryId)
}
Label {
Layout.fillWidth: true
colorScheme: root.colorScheme
text: qsTr("Provide more details")
type: Label.Heading
}
Label {
Layout.fillWidth: true
colorScheme: root.colorScheme
text: qsTr(Backend.getBugCategory(root.categoryId))
type: Label.Title
}
TextEdit {
Layout.fillWidth: true
color: root.colorScheme.text_weak
font.family: ProtonStyle.font_family
font.letterSpacing: ProtonStyle.caption_letter_spacing
font.pixelSize: ProtonStyle.caption_font_size
font.weight: ProtonStyle.fontWeight_400
textFormat: Text.MarkdownText
readOnly: true
selectByMouse: true
selectedTextColor: root.colorScheme.text_invert
// No way to set lineHeight: ProtonStyle.caption_line_height
selectionColor: root.colorScheme.interaction_norm
text: qsTr("* Mandatory questions")
wrapMode: Text.WordWrap
}
Repeater {
id: questionRepeater
model: root.questionSet
property bool error :{
for (var i = 0; i < questionRepeater.count; i++) {
if (questionRepeater.itemAt(i).error)
return true;
}
return false;
}
function validate(){
for (var i = 0; i < questionRepeater.count; i++) {
questionRepeater.itemAt(i).validate()
}
}
QuestionItem {
Layout.fillWidth: true
colorScheme: root.colorScheme
showSeparator: index < (root.questionSet.length - 1)
text: root.questions[modelData].text
tips: root.questions[modelData].tips ? root.questions[modelData].tips : ""
label: root.questions[modelData].label ? root.questions[modelData].label : ""
type: root.questions[modelData].type
mandatory: root.questions[modelData].mandatory ? root.questions[modelData].mandatory : false
answerList: root.questions[modelData].answerList ? root.questions[modelData].answerList : []
maxChar: root.questions[modelData].maxChar ? root.questions[modelData].maxChar : 150
onAnswerChanged: {
Backend.setQuestionAnswer(modelData, answer);
}
Connections {
function onVisibleChanged() {
setDefaultValue(Backend.getQuestionAnswer(modelData))
}
target: root
}
}
}
// fill height so the footer label will always be attached to the bottom
Item {
Layout.fillHeight: true
}
Button {
id: continueButton
colorScheme: root.colorScheme
enabled: !loading && !root.error
text: qsTr("Continue")
onClicked: {
questionRepeater.validate()
if (!root.error)
submit();
}
}
}

View File

@ -0,0 +1,99 @@
// Copyright (c) 2023 Proton AG
// This file is part of Proton Mail Bridge.
// Proton Mail Bridge 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 3 of the License, or
// (at your option) any later version.
// Proton Mail Bridge 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.
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
import Notifications
Item {
id: root
property ColorScheme colorScheme
property string selectedAddress
property int categoryId: -1
signal back
signal bugReportWasSent
onVisibleChanged: {
root.showBugCategory();
}
function showBugCategory() {
bugReportFlow.currentIndex = 0;
}
function showBugQuestion() {
bugQuestion.setCategoryId(root.categoryId);
bugQuestion.positionViewAtBegining();
bugReportFlow.currentIndex = 1;
}
function showBugReport() {
bugReport.setCategoryId(root.categoryId);
bugReportFlow.currentIndex = 2;
}
Rectangle {
anchors.fill: parent
Layout.fillHeight: true // right content background
Layout.fillWidth: true
color: colorScheme.background_norm
StackLayout {
id: bugReportFlow
anchors.fill: parent
BugCategoryView {
// 0
id: bugCategory
colorScheme: root.colorScheme
onBack: {
root.back()
}
onCategorySelected: function(categoryId){
root.categoryId = categoryId
root.showBugQuestion();
}
}
BugQuestionView {
// 1
id: bugQuestion
colorScheme: root.colorScheme
onBack: {
root.showBugCategory();
}
onQuestionAnswered: {
root.showBugReport();
}
}
BugReportView {
// 2
id: bugReport
colorScheme: root.colorScheme
selectedAddress: root.selectedAddress
onBack: {
root.showBugQuestion();
}
onBugReportWasSent: {
Backend.clearAnswers();
root.bugReportWasSent();
}
}
}
}
}

View File

@ -1,202 +1,161 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import Proton import Proton
SettingsView { SettingsView {
id: root id: root
fillHeight: true
property var selectedAddress property var selectedAddress
property var categoryId:-1
property string category: Backend.getBugCategory(root.categoryId)
signal bugReportWasSent() signal bugReportWasSent
Label { function isValidEmail(text) {
text: qsTr("Report a problem") const reEmail = /^[^@]+@[^@]+\.[A-Za-z]+\s*$/;
colorScheme: root.colorScheme return reEmail.test(text);
type: Label.Heading
} }
function setCategoryId(catId) {
root.categoryId = catId;
}
function setDefaultValue() {
description.text = Backend.collectAnswers(root.categoryId);
address.text = root.selectedAddress;
emailClient.text = Backend.currentEmailClient;
includeLogs.checked = true;
}
function submit() {
sendButton.loading = true;
Backend.reportBug(root.category, description.text, address.text, emailClient.text, includeLogs.checked);
}
fillHeight: true
onVisibleChanged: {
root.setDefaultValue();
}
Label {
colorScheme: root.colorScheme
text: qsTr("Send report")
type: Label.Heading
}
TextArea { TextArea {
id: description id: description
property int _minLength: 150
property int _maxLength: 800
label: qsTr("Description")
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: heightForLinesVisible(4)
hint: description.text.length + "/" + _maxLength
placeholderText: qsTr("Tell us what went wrong or isn't working (min. %1 characters).").arg(_minLength)
validator: function(text) {
if (description.text.length < description._minLength) {
return qsTr("Enter a problem description (min. %1 characters).").arg(_minLength)
}
if (description.text.length > description._maxLength) {
return qsTr("Enter a problem description (max. %1 characters).").arg(_maxLength)
}
return
}
onTextChanged: {
// Rise max length error immediately while typing
if (description.text.length > description._maxLength) {
validate()
}
}
KeyNavigation.priority: KeyNavigation.BeforeItem KeyNavigation.priority: KeyNavigation.BeforeItem
KeyNavigation.tab: address KeyNavigation.tab: address
Layout.fillHeight: true
Layout.fillWidth: true
Layout.minimumHeight: heightForLinesVisible(4)
colorScheme: root.colorScheme
textFormat: Text.MarkdownText
// set implicitHeight to explicit height because se don't // set implicitHeight to explicit height because se don't
// want TextArea implicitHeight (which is height of all text) // want TextArea implicitHeight (which is height of all text)
// to be considered in SettingsView internal scroll view // to be considered in SettingsView internal scroll view
implicitHeight: height implicitHeight: height
label: "Your answers to: " + qsTr(root.category);
readOnly : true
} }
TextField { TextField {
id: address id: address
label: qsTr("Your contact email")
colorScheme: root.colorScheme
Layout.fillWidth: true Layout.fillWidth: true
colorScheme: root.colorScheme
label: qsTr("Your contact email")
placeholderText: qsTr("e.g. jane.doe@protonmail.com") placeholderText: qsTr("e.g. jane.doe@protonmail.com")
validator: function (str) {
validator: function(str) {
if (!isValidEmail(str)) { if (!isValidEmail(str)) {
return qsTr("Enter valid email address") return qsTr("Enter valid email address");
} }
return return;
} }
} }
TextField { TextField {
id: emailClient id: emailClient
label: qsTr("Your email client (including version)")
colorScheme: root.colorScheme
Layout.fillWidth: true Layout.fillWidth: true
colorScheme: root.colorScheme
label: qsTr("Your email client (including version)")
placeholderText: qsTr("e.g. Apple Mail 14.0") placeholderText: qsTr("e.g. Apple Mail 14.0")
validator: function (str) {
validator: function(str) {
if (str.length === 0) { if (str.length === 0) {
return qsTr("Enter an email client name and version") return qsTr("Enter an email client name and version");
} }
return return;
} }
} }
RowLayout { RowLayout {
CheckBox { CheckBox {
id: includeLogs id: includeLogs
text: qsTr("Include my recent logs")
colorScheme: root.colorScheme
checked: true checked: true
colorScheme: root.colorScheme
text: qsTr("Include my recent logs")
} }
Button { Button {
Layout.leftMargin: 12 Layout.leftMargin: 12
text: qsTr("View logs")
secondary: true
colorScheme: root.colorScheme colorScheme: root.colorScheme
secondary: true
text: qsTr("View logs")
onClicked: Qt.openUrlExternally(Backend.logsPath) onClicked: Qt.openUrlExternally(Backend.logsPath)
} }
} }
TextEdit { TextEdit {
text: qsTr("Reports are not end-to-end encrypted, please do not send any sensitive information.")
readOnly: true
Layout.fillWidth: true Layout.fillWidth: true
color: root.colorScheme.text_weak color: root.colorScheme.text_weak
font.family: ProtonStyle.font_family font.family: ProtonStyle.font_family
font.weight: ProtonStyle.fontWeight_400
font.pixelSize: ProtonStyle.caption_font_size
font.letterSpacing: ProtonStyle.caption_letter_spacing font.letterSpacing: ProtonStyle.caption_letter_spacing
font.pixelSize: ProtonStyle.caption_font_size
font.weight: ProtonStyle.fontWeight_400
readOnly: true
selectByMouse: true
selectedTextColor: root.colorScheme.text_invert
// No way to set lineHeight: ProtonStyle.caption_line_height // No way to set lineHeight: ProtonStyle.caption_line_height
selectionColor: root.colorScheme.interaction_norm selectionColor: root.colorScheme.interaction_norm
selectedTextColor: root.colorScheme.text_invert text: qsTr("Reports are not end-to-end encrypted, please do not send any sensitive information.")
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
selectByMouse: true
} }
Button { Button {
id: sendButton id: sendButton
text: qsTr("Send")
colorScheme: root.colorScheme colorScheme: root.colorScheme
enabled: !loading enabled: !loading
text: qsTr("Send")
onClicked: { onClicked: {
description.validate() description.validate();
address.validate() address.validate();
emailClient.validate() emailClient.validate();
if (description.error || address.error || emailClient.error) { if (description.error || address.error || emailClient.error) {
return return;
} }
submit();
submit()
} }
Connections { Connections {
function onBugReportSendSuccess() {
root.bugReportWasSent();
}
function onReportBugFinished() {
sendButton.loading = false;
}
target: Backend target: Backend
function onReportBugFinished() { sendButton.loading = false }
function onBugReportSendSuccess() { root.bugReportWasSent() }
} }
} }
function setDescription(message) {
description.text = message
}
function setDefaultValue() {
description.text = ""
address.text = root.selectedAddress
emailClient.text = Backend.currentEmailClient
includeLogs.checked = true
}
function isValidEmail(text){
var reEmail = /^[^@]+@[^@]+\.[A-Za-z]+\s*$/
return reEmail.test(text)
}
function submit() {
sendButton.loading = true
Backend.reportBug(
description.text,
address.text,
emailClient.text,
includeLogs.checked
)
}
onVisibleChanged: {
root.setDefaultValue()
}
} }

View File

@ -0,0 +1,113 @@
// Copyright (c) 2023 Proton AG
// This file is part of Proton Mail Bridge.
// Proton Mail Bridge 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 3 of the License, or
// (at your option) any later version.
// Proton Mail Bridge 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.
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
Item {
id: root
property var _bottomMargin: 20
property var _lineHeight: 1
property string actionIcon: ""
property var colorScheme
property bool showSeparator: true
property string text: "Text"
property string hint: ""
signal clicked
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
RowLayout {
anchors.fill: parent
spacing: 16
Label {
id: mainLabel
colorScheme: root.colorScheme
text: root.text
type: Label.Body
Layout.alignment: Qt.AlignVCenter
Layout.bottomMargin: root._bottomMargin
wrapMode: Text.WordWrap
}
ColorImage {
id: infoImage
Layout.alignment: Qt.AlignVCenter
Layout.bottomMargin: root._bottomMargin
color: root.colorScheme.interaction_norm
height: 21
width: 21
source: "/qml/icons/ic-info-circle.svg"
sourceSize.height: 21
sourceSize.width: 21
visible: root.hint !== ""
MouseArea {
id: imageArea
anchors.fill: infoImage
hoverEnabled: true
}
ToolTip {
id: toolTipinfo
text: root.hint
visible: imageArea.containsMouse
implicitWidth: Math.min(400, tooltipText.implicitWidth)
background: Rectangle {
radius: 4
border.color: root.colorScheme.border_weak
color: root.colorScheme.background_weak
}
contentItem: Text {
id: tooltipText
color: root.colorScheme.text_hint
text: toolTipinfo.text
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
// fill height so the footer label will always be attached to the bottom
Item {
Layout.fillWidth: true
}
Button {
id: button
Layout.alignment: Qt.AlignVCenter
Layout.bottomMargin: root._bottomMargin
colorScheme: root.colorScheme
icon.source: root.actionIcon
text: ""
secondary: true
visible: root.actionIcon !== ""
onClicked: {
if (!root.loading)
root.clicked();
}
}
}
Rectangle {
anchors.bottom: root.bottom
anchors.left: root.left
anchors.right: root.right
color: colorScheme.border_weak
height: root._lineHeight
visible: root.showSeparator
}
}

View File

@ -1,71 +1,80 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
import Proton import Proton
Rectangle { Rectangle {
id: root id: root
property int _margin: 24
property ColorScheme colorScheme property ColorScheme colorScheme
property string title
property string hostname property string hostname
property string port
property string username
property string password property string password
property string port
property string security property string security
property string title
implicitWidth: 304 property string username
implicitHeight: content.height + 2*root._margin
color: root.colorScheme.background_norm color: root.colorScheme.background_norm
implicitHeight: content.height + 2 * root._margin
implicitWidth: 304
radius: ProtonStyle.card_radius radius: ProtonStyle.card_radius
property int _margin: 24
ColumnLayout { ColumnLayout {
id: content id: content
width: root.width - 2*root._margin
anchors{
top: root.top
left: root.left
leftMargin : root._margin
rightMargin : root._margin
topMargin : root._margin
bottomMargin : root._margin
}
spacing: 12 spacing: 12
width: root.width - 2 * root._margin
anchors {
bottomMargin: root._margin
left: root.left
leftMargin: root._margin
rightMargin: root._margin
top: root.top
topMargin: root._margin
}
Label { Label {
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: root.title text: root.title
type: Label.Body_semibold type: Label.Body_semibold
} }
ConfigurationItem {
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Hostname") ; value: root.hostname } colorScheme: root.colorScheme
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Port") ; value: root.port } label: qsTr("Hostname")
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Username") ; value: root.username } value: root.hostname
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Password") ; value: root.password } }
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Security") ; value: root.security } ConfigurationItem {
colorScheme: root.colorScheme
label: qsTr("Port")
value: root.port
}
ConfigurationItem {
colorScheme: root.colorScheme
label: qsTr("Username")
value: root.username
}
ConfigurationItem {
colorScheme: root.colorScheme
label: qsTr("Password")
value: root.password
}
ConfigurationItem {
colorScheme: root.colorScheme
label: qsTr("Security")
value: root.security
}
} }
} }

View File

@ -1,35 +1,29 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
import Proton import Proton
Item { Item {
id: root id: root
Layout.fillWidth: true
property var colorScheme property var colorScheme
property string label property string label
property string value property string value
Layout.fillWidth: true
implicitHeight: children[0].implicitHeight implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth implicitWidth: children[0].implicitWidth
@ -47,45 +41,42 @@ Item {
} }
TextEdit { TextEdit {
id: valueText id: valueText
text: root.value Layout.fillWidth: true
color: root.colorScheme.text_weak color: root.colorScheme.text_weak
readOnly: true readOnly: true
selectByMouse: true
selectByKeyboard: true selectByKeyboard: true
selectByMouse: true
selectionColor: root.colorScheme.text_weak selectionColor: root.colorScheme.text_weak
text: root.value
wrapMode: Text.WrapAnywhere wrapMode: Text.WrapAnywhere
Layout.fillWidth: true
} }
} }
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
} }
ColorImage { ColorImage {
source: "/qml/icons/ic-copy.svg"
color: root.colorScheme.text_norm color: root.colorScheme.text_norm
height: root.colorScheme.body_font_size height: root.colorScheme.body_font_size
source: "/qml/icons/ic-copy.svg"
sourceSize.height: root.colorScheme.body_font_size sourceSize.height: root.colorScheme.body_font_size
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked : {
valueText.select(0, valueText.length) onClicked: {
valueText.copy() valueText.select(0, valueText.length);
valueText.deselect() valueText.copy();
valueText.deselect();
} }
onPressed: parent.scale = 0.90 onPressed: parent.scale = 0.90
onReleased: parent.scale = 1 onReleased: parent.scale = 1
} }
} }
} }
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
height: 1
color: root.colorScheme.border_norm color: root.colorScheme.border_norm
height: 1
} }
} }
} }

View File

@ -1,155 +1,138 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
import Proton import Proton
SettingsView { SettingsView {
id: root id: root
function setDefaultValues() {
imapSSLButton.checked = Backend.useSSLForIMAP;
imapSTARTTLSButton.checked = !Backend.useSSLForIMAP;
smtpSSLButton.checked = Backend.useSSLForSMTP;
smtpSTARTTLSButton.checked = !Backend.useSSLForSMTP;
}
function submit() {
submitButton.loading = true;
Backend.setMailServerSettings(Backend.imapPort, Backend.smtpPort, imapSSLButton.checked, smtpSSLButton.checked);
}
fillHeight: false fillHeight: false
onVisibleChanged: {
root.setDefaultValues();
}
Label { Label {
Layout.fillWidth: true
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Connection mode") text: qsTr("Connection mode")
type: Label.Heading type: Label.Heading
Layout.fillWidth: true
} }
Label { Label {
Layout.fillWidth: true
color: root.colorScheme.text_weak
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Change the protocol Bridge and the email client use to connect for IMAP and SMTP.") text: qsTr("Change the protocol Bridge and the email client use to connect for IMAP and SMTP.")
type: Label.Body type: Label.Body
color: root.colorScheme.text_weak
Layout.fillWidth: true
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
ColumnLayout { ColumnLayout {
spacing: 16 spacing: 16
ButtonGroup{ id: imapProtocolSelection } ButtonGroup {
id: imapProtocolSelection
}
Label { Label {
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("IMAP connection") text: qsTr("IMAP connection")
} }
RadioButton { RadioButton {
id: imapSSLButton id: imapSSLButton
colorScheme: root.colorScheme
ButtonGroup.group: imapProtocolSelection ButtonGroup.group: imapProtocolSelection
colorScheme: root.colorScheme
text: qsTr("SSL") text: qsTr("SSL")
} }
RadioButton { RadioButton {
id: imapSTARTTLSButton id: imapSTARTTLSButton
colorScheme: root.colorScheme
ButtonGroup.group: imapProtocolSelection ButtonGroup.group: imapProtocolSelection
colorScheme: root.colorScheme
text: qsTr("STARTTLS") text: qsTr("STARTTLS")
} }
} }
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
height: 1
color: root.colorScheme.border_weak color: root.colorScheme.border_weak
height: 1
} }
ColumnLayout { ColumnLayout {
spacing: 16 spacing: 16
ButtonGroup{ id: smtpProtocolSelection } ButtonGroup {
id: smtpProtocolSelection
}
Label { Label {
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("SMTP connection") text: qsTr("SMTP connection")
} }
RadioButton { RadioButton {
id: smtpSSLButton id: smtpSSLButton
colorScheme: root.colorScheme
ButtonGroup.group: smtpProtocolSelection ButtonGroup.group: smtpProtocolSelection
colorScheme: root.colorScheme
text: qsTr("SSL") text: qsTr("SSL")
} }
RadioButton { RadioButton {
id: smtpSTARTTLSButton id: smtpSTARTTLSButton
colorScheme: root.colorScheme
ButtonGroup.group: smtpProtocolSelection ButtonGroup.group: smtpProtocolSelection
colorScheme: root.colorScheme
text: qsTr("STARTTLS") text: qsTr("STARTTLS")
} }
} }
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
height: 1
color: root.colorScheme.border_weak color: root.colorScheme.border_weak
height: 1
} }
RowLayout { RowLayout {
spacing: 12 spacing: 12
Button { Button {
id: submitButton id: submitButton
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Save")
onClicked: {
submitButton.loading = true
root.submit()
}
enabled: (!loading) && ((imapSSLButton.checked !== Backend.useSSLForIMAP) || (smtpSSLButton.checked !== Backend.useSSLForSMTP)) enabled: (!loading) && ((imapSSLButton.checked !== Backend.useSSLForIMAP) || (smtpSSLButton.checked !== Backend.useSSLForSMTP))
} text: qsTr("Save")
onClicked: {
submitButton.loading = true;
root.submit();
}
}
Button { Button {
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Cancel")
onClicked: root.back()
secondary: true secondary: true
} text: qsTr("Cancel")
onClicked: root.back()
}
Connections { Connections {
target: Backend
function onChangeMailServerSettingsFinished() { function onChangeMailServerSettingsFinished() {
submitButton.loading = false submitButton.loading = false;
root.back() root.back();
} }
target: Backend
} }
} }
function submit(){
submitButton.loading = true
Backend.setMailServerSettings(Backend.imapPort, Backend.smtpPort, imapSSLButton.checked, smtpSSLButton.checked)
}
function setDefaultValues(){
imapSSLButton.checked = Backend.useSSLForIMAP
imapSTARTTLSButton.checked = !Backend.useSSLForIMAP
smtpSSLButton.checked = Backend.useSSLForSMTP
smtpSTARTTLSButton.checked = !Backend.useSSLForSMTP
}
onVisibleChanged: {
root.setDefaultValues()
}
} }

View File

@ -1,36 +1,58 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import Proton import Proton
import Notifications import Notifications
Item { Item {
id: root id: root
property ColorScheme colorScheme
property ColorScheme colorScheme
property var notifications property var notifications
signal closeWindow
signal quitBridge
signal showSetupGuide(var user, string address) signal showSetupGuide(var user, string address)
signal closeWindow()
signal quitBridge() function selectUser(userID) {
const users = Backend.users;
for (let i = 0; i < users.count; i++) {
const user = users.get(i);
if (user.id !== userID) {
continue;
}
accounts.currentIndex = i;
if (user.state === EUserState.SignedOut)
showSignIn(user.primaryEmailOrUsername());
return;
}
console.error("User with ID ", userID, " was not found in the account list");
}
function showHelp() {
rightContent.showHelpView();
}
function showLocalCacheSettings() {
rightContent.showLocalCacheSettings();
}
function showSettings() {
rightContent.showGeneralSettings();
}
function showSignIn(username) {
signIn.username = username;
rightContent.showSignIn();
}
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
@ -38,13 +60,13 @@ Item {
Rectangle { Rectangle {
id: leftBar id: leftBar
property ColorScheme colorScheme: root.colorScheme.prominent property ColorScheme colorScheme: root.colorScheme.prominent
Layout.minimumWidth: 264
Layout.maximumWidth: 320
Layout.preferredWidth: 320
Layout.fillHeight: true Layout.fillHeight: true
Layout.maximumWidth: 320
Layout.minimumWidth: 264
Layout.preferredWidth: 320
color: colorScheme.background_norm color: colorScheme.background_norm
ColumnLayout { ColumnLayout {
@ -52,24 +74,21 @@ Item {
spacing: 0 spacing: 0
RowLayout { RowLayout {
id:topLeftBar id: topLeftBar
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumHeight: 60
Layout.maximumHeight: 60 Layout.maximumHeight: 60
Layout.minimumHeight: 60
Layout.preferredHeight: 60 Layout.preferredHeight: 60
spacing: 0 spacing: 0
Status { Status {
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: 17
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.topMargin: 24 Layout.topMargin: 24
Layout.bottomMargin: 17
Layout.alignment: Qt.AlignHCenter
colorScheme: leftBar.colorScheme colorScheme: leftBar.colorScheme
notifications: root.notifications
notificationWhitelist: Notifications.Group.Connection | Notifications.Group.ForceUpdate notificationWhitelist: Notifications.Group.Connection | Notifications.Group.ForceUpdate
notifications: root.notifications
} }
// just a placeholder // just a placeholder
@ -77,47 +96,38 @@ Item {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
} }
Button { Button {
colorScheme: leftBar.colorScheme
Layout.minimumHeight: 36
Layout.maximumHeight: 36
Layout.preferredHeight: 36
Layout.minimumWidth: 36
Layout.maximumWidth: 36
Layout.preferredWidth: 36
Layout.topMargin: 16
Layout.bottomMargin: 9 Layout.bottomMargin: 9
Layout.maximumHeight: 36
Layout.maximumWidth: 36
Layout.minimumHeight: 36
Layout.minimumWidth: 36
Layout.preferredHeight: 36
Layout.preferredWidth: 36
Layout.rightMargin: 4 Layout.rightMargin: 4
Layout.topMargin: 16
colorScheme: leftBar.colorScheme
horizontalPadding: 0 horizontalPadding: 0
icon.source: "/qml/icons/ic-question-circle.svg" icon.source: "/qml/icons/ic-question-circle.svg"
onClicked: rightContent.showHelpView() onClicked: rightContent.showHelpView()
} }
Button { Button {
colorScheme: leftBar.colorScheme
Layout.minimumHeight: 36
Layout.maximumHeight: 36
Layout.preferredHeight: 36
Layout.minimumWidth: 36
Layout.maximumWidth: 36
Layout.preferredWidth: 36
Layout.topMargin: 16
Layout.bottomMargin: 9 Layout.bottomMargin: 9
Layout.maximumHeight: 36
Layout.maximumWidth: 36
Layout.minimumHeight: 36
Layout.minimumWidth: 36
Layout.preferredHeight: 36
Layout.preferredWidth: 36
Layout.rightMargin: 4 Layout.rightMargin: 4
Layout.topMargin: 16
colorScheme: leftBar.colorScheme
horizontalPadding: 0 horizontalPadding: 0
icon.source: "/qml/icons/ic-cog-wheel.svg" icon.source: "/qml/icons/ic-cog-wheel.svg"
onClicked: rightContent.showGeneralSettings() onClicked: rightContent.showGeneralSettings()
} }
Button { Button {
id: dotMenuButton id: dotMenuButton
Layout.bottomMargin: 9 Layout.bottomMargin: 9
@ -134,7 +144,7 @@ Item {
icon.source: "/qml/icons/ic-three-dots-vertical.svg" icon.source: "/qml/icons/ic-three-dots-vertical.svg"
onClicked: { onClicked: {
dotMenu.open() dotMenu.open();
} }
Menu { Menu {
@ -143,332 +153,319 @@ Item {
modal: true modal: true
y: dotMenuButton.Layout.preferredHeight + dotMenuButton.Layout.bottomMargin y: dotMenuButton.Layout.preferredHeight + dotMenuButton.Layout.bottomMargin
onClosed: {
parent.checked = false;
}
onOpened: {
parent.checked = true;
}
MenuItem { MenuItem {
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Close window") text: qsTr("Close window")
onClicked: { onClicked: {
root.closeWindow() root.closeWindow();
} }
} }
MenuItem { MenuItem {
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Quit Bridge") text: qsTr("Quit Bridge")
onClicked: {
root.quitBridge()
}
}
onClosed: { onClicked: {
parent.checked = false root.quitBridge();
} }
onOpened: {
parent.checked = true
} }
} }
} }
} }
Item {
Item {implicitHeight:10} implicitHeight: 10
}
// Separator line // Separator line
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumHeight: 1
Layout.maximumHeight: 1 Layout.maximumHeight: 1
Layout.minimumHeight: 1
color: leftBar.colorScheme.border_weak color: leftBar.colorScheme.border_weak
} }
ListView { ListView {
id: accounts id: accounts
property var _topBottomMargins: 24
property var _leftRightMargins: 16 property var _leftRightMargins: 16
property var _topBottomMargins: 24
Layout.fillWidth: true Layout.bottomMargin: accounts._topBottomMargins
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true
Layout.leftMargin: accounts._leftRightMargins Layout.leftMargin: accounts._leftRightMargins
Layout.rightMargin: accounts._leftRightMargins Layout.rightMargin: accounts._leftRightMargins
Layout.topMargin: accounts._topBottomMargins Layout.topMargin: accounts._topBottomMargins
Layout.bottomMargin: accounts._topBottomMargins
spacing: 12
clip: true
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
clip: true
model: Backend.users
spacing: 12
header: Rectangle { delegate: Item {
height: headerLabel.height+16 implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
// color: ProtonStyle.transparent implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
Label{ width: leftBar.width - 2 * accounts._leftRightMargins
AccountDelegate {
id: accountDelegate
anchors.bottomMargin: 8
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
anchors.topMargin: 8
colorScheme: leftBar.colorScheme colorScheme: leftBar.colorScheme
user: Backend.users.get(index)
}
MouseArea {
anchors.fill: parent
onClicked: {
const user = Backend.users.get(index);
accounts.currentIndex = index;
if (!user)
return;
if (user.state !== EUserState.SignedOut) {
rightContent.showAccount();
} else {
signIn.username = user.primaryEmailOrUsername();
rightContent.showSignIn();
}
}
}
}
header: Rectangle {
height: headerLabel.height + 16
// color: ProtonStyle.transparent
Label {
id: headerLabel id: headerLabel
colorScheme: leftBar.colorScheme
text: qsTr("Accounts") text: qsTr("Accounts")
type: Label.LabelType.Body type: Label.LabelType.Body
} }
} }
highlight: Rectangle { highlight: Rectangle {
color: leftBar.colorScheme.interaction_default_active color: leftBar.colorScheme.interaction_default_active
radius: ProtonStyle.account_row_radius radius: ProtonStyle.account_row_radius
} }
model: Backend.users
delegate: Item {
width: leftBar.width - 2*accounts._leftRightMargins
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
AccountDelegate {
id: accountDelegate
anchors.fill: parent
anchors.topMargin: 8
anchors.bottomMargin: 8
anchors.leftMargin: 12
anchors.rightMargin: 12
colorScheme: leftBar.colorScheme
user: Backend.users.get(index)
}
MouseArea {
anchors.fill: parent
onClicked: {
var user = Backend.users.get(index)
accounts.currentIndex = index
if (!user) return
if (user.state !== EUserState.SignedOut) {
rightContent.showAccount()
} else {
signIn.username = user.primaryEmailOrUsername()
rightContent.showSignIn()
}
}
}
}
} }
// Separator // Separator
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumHeight: 1
Layout.maximumHeight: 1 Layout.maximumHeight: 1
Layout.minimumHeight: 1
color: leftBar.colorScheme.border_weak color: leftBar.colorScheme.border_weak
} }
Item { Item {
id: bottomLeftBar id: bottomLeftBar
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumHeight: 52
Layout.maximumHeight: 52 Layout.maximumHeight: 52
Layout.minimumHeight: 52
Layout.preferredHeight: 52 Layout.preferredHeight: 52
Button { Button {
colorScheme: leftBar.colorScheme
width: 36
height: 36
anchors.left: parent.left anchors.left: parent.left
anchors.top: parent.top
anchors.leftMargin: 16 anchors.leftMargin: 16
anchors.top: parent.top
anchors.topMargin: 7 anchors.topMargin: 7
colorScheme: leftBar.colorScheme
height: 36
horizontalPadding: 0 horizontalPadding: 0
icon.source: "/qml/icons/ic-plus.svg" icon.source: "/qml/icons/ic-plus.svg"
width: 36
onClicked: { onClicked: {
signIn.username = "" signIn.username = "";
rightContent.showSignIn() rightContent.showSignIn();
} }
} }
} }
} }
} }
Rectangle {
Rectangle { // right content background Layout.fillHeight: true // right content background
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true
color: colorScheme.background_norm color: colorScheme.background_norm
StackLayout { StackLayout {
id: rightContent id: rightContent
function showAccount(index) {
if (index !== undefined && index >= 0) {
accounts.currentIndex = index;
}
rightContent.currentIndex = 0;
}
function showBugReport() {
rightContent.currentIndex = 8;
}
function showConnectionModeSettings() {
rightContent.currentIndex = 5;
}
function showGeneralSettings() {
rightContent.currentIndex = 2;
}
function showHelpView() {
rightContent.currentIndex = 7;
}
function showKeychainSettings() {
rightContent.currentIndex = 3;
}
function showLocalCacheSettings() {
rightContent.currentIndex = 6;
}
function showPortSettings() {
rightContent.currentIndex = 4;
}
function showSignIn() {
rightContent.currentIndex = 1;
signIn.focus = true;
}
anchors.fill: parent anchors.fill: parent
AccountView { // 0 AccountView {
// 0
colorScheme: root.colorScheme colorScheme: root.colorScheme
notifications: root.notifications notifications: root.notifications
user: { user: {
if (accounts.currentIndex < 0) return undefined if (accounts.currentIndex < 0)
if (Backend.users.count == 0) return undefined return undefined;
return Backend.users.get(accounts.currentIndex) if (Backend.users.count === 0)
return undefined;
return Backend.users.get(accounts.currentIndex);
}
onShowSetupGuide: function (user, address) {
root.showSetupGuide(user, address);
} }
onShowSignIn: { onShowSignIn: {
var user = this.user const user = this.user;
signIn.username = user ? user.primaryEmailOrUsername() : "" signIn.username = user ? user.primaryEmailOrUsername() : "";
rightContent.showSignIn() rightContent.showSignIn();
}
onShowSetupGuide: function(user, address) {
root.showSetupGuide(user,address)
} }
} }
GridLayout {
GridLayout { // 1 Sign In // 1 Sign In
columns: 2 columns: 2
Button { Button {
id: backButton id: backButton
Layout.alignment: Qt.AlignTop
Layout.leftMargin: 18 Layout.leftMargin: 18
Layout.topMargin: 10 Layout.topMargin: 10
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme colorScheme: root.colorScheme
onClicked: { horizontalPadding: 8
signIn.abort()
rightContent.showAccount()
}
icon.source: "/qml/icons/ic-arrow-left.svg" icon.source: "/qml/icons/ic-arrow-left.svg"
secondary: true secondary: true
horizontalPadding: 8
}
onClicked: {
signIn.abort();
rightContent.showAccount();
}
}
SignIn { SignIn {
id: signIn id: signIn
Layout.topMargin: 68
Layout.leftMargin: 80 - backButton.width - 18
Layout.rightMargin: 80
Layout.bottomMargin: 68 Layout.bottomMargin: 68
Layout.preferredWidth: 320
Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true
Layout.leftMargin: 80 - backButton.width - 18
Layout.preferredWidth: 320
Layout.rightMargin: 80
Layout.topMargin: 68
colorScheme: root.colorScheme colorScheme: root.colorScheme
} }
} }
GeneralSettings {
GeneralSettings { // 2 // 2
colorScheme: root.colorScheme colorScheme: root.colorScheme
notifications: root.notifications notifications: root.notifications
onBack: { onBack: {
rightContent.showAccount() rightContent.showAccount();
} }
} }
KeychainSettings {
KeychainSettings { // 3 // 3
colorScheme: root.colorScheme colorScheme: root.colorScheme
onBack: { onBack: {
rightContent.showGeneralSettings() rightContent.showGeneralSettings();
} }
} }
PortSettings {
PortSettings { // 4 // 4
colorScheme: root.colorScheme
notifications: root.notifications
onBack: {
rightContent.showGeneralSettings()
}
}
ConnectionModeSettings { // 5
colorScheme: root.colorScheme
onBack: {
rightContent.showGeneralSettings()
}
}
LocalCacheSettings { // 6
colorScheme: root.colorScheme colorScheme: root.colorScheme
notifications: root.notifications notifications: root.notifications
onBack: { onBack: {
rightContent.showGeneralSettings() rightContent.showGeneralSettings();
} }
} }
ConnectionModeSettings {
HelpView { // 7 // 5
colorScheme: root.colorScheme colorScheme: root.colorScheme
onBack: { onBack: {
rightContent.showAccount() rightContent.showGeneralSettings();
} }
} }
LocalCacheSettings {
// 6
colorScheme: root.colorScheme
notifications: root.notifications
BugReportView { // 8 onBack: {
rightContent.showGeneralSettings();
}
}
HelpView {
// 7
colorScheme: root.colorScheme
onBack: {
rightContent.showAccount();
}
}
BugReportFlow {
// 8
id: bugReport id: bugReport
colorScheme: root.colorScheme colorScheme: root.colorScheme
selectedAddress: { selectedAddress: {
if (accounts.currentIndex < 0) return "" if (accounts.currentIndex < 0)
if (Backend.users.count == 0) return "" return "";
var user = Backend.users.get(accounts.currentIndex) if (Backend.users.count === 0)
if (!user) return "" return "";
return user.addresses[0] const user = Backend.users.get(accounts.currentIndex);
if (!user)
return "";
return user.addresses[0];
} }
onBack: { onBack: {
rightContent.showHelpView() rightContent.showHelpView();
} }
onBugReportWasSent: { onBugReportWasSent: {
rightContent.showAccount() rightContent.showAccount();
} }
} }
function showAccount(index) {
if (index !== undefined && index >= 0){
accounts.currentIndex = index
}
rightContent.currentIndex = 0
}
function showSignIn () { rightContent.currentIndex = 1; signIn.focus = true }
function showGeneralSettings () { rightContent.currentIndex = 2 }
function showKeychainSettings () { rightContent.currentIndex = 3 }
function showPortSettings () { rightContent.currentIndex = 4 }
function showConnectionModeSettings() { rightContent.currentIndex = 5 }
function showLocalCacheSettings () { rightContent.currentIndex = 6 }
function showHelpView () { rightContent.currentIndex = 7 }
function showBugReport () { rightContent.currentIndex = 8 }
Connections { Connections {
target: Backend function onLoginAlreadyLoggedIn(index) {
rightContent.showAccount(index);
}
function onLoginFinished(index) {
rightContent.showAccount(index);
}
function onLoginFinished(index) { rightContent.showAccount(index) } target: Backend
function onLoginAlreadyLoggedIn(index) { rightContent.showAccount(index) }
} }
} }
} }
} }
function showLocalCacheSettings(){rightContent.showLocalCacheSettings() }
function showSettings(){rightContent.showGeneralSettings() }
function showHelp(){rightContent.showHelpView() }
function showSignIn(username){
signIn.username = username
rightContent.showSignIn()
}
function selectUser(userID) {
var users = Backend.users;
for (var i = 0; i < users.count; i++) {
var user = users.get(i)
if (user.id !== userID) {
continue;
}
accounts.currentIndex = i;
if (user.state === EUserState.SignedOut)
showSignIn(user.primaryEmailOrUsername())
return;
}
console.error("User with ID ", userID, " was not found in the account list")
}
function showBugReportAndPrefill(description) {
rightContent.showBugReport()
bugReport.setDescription(description)
}
} }

View File

@ -1,54 +1,45 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import "." import "."
import "./Proton" import "Proton"
Rectangle { Rectangle {
property var target: parent property var target: parent
x: target.x
y: target.y
width: target.width
height: target.height
color: "transparent"
border.color: "red" border.color: "red"
border.width: 1 border.width: 1
color: "transparent"
height: target.height
width: target.width
x: target.x
y: target.y
//z: parent.z - 1 //z: parent.z - 1
z: 10000000 z: 10000000
Label { Label {
text: parent.width + "x" + parent.height
anchors.centerIn: parent anchors.centerIn: parent
color: "black" color: "black"
colorScheme: ProtonStyle.currentStyle colorScheme: ProtonStyle.currentStyle
text: parent.width + "x" + parent.height
} }
Rectangle { Rectangle {
width: target.implicitWidth
height: target.implicitHeight
color: "transparent"
border.color: "green" border.color: "green"
border.width: 1 border.width: 1
color: "transparent"
height: target.implicitHeight
width: target.implicitWidth
//z: parent.z - 1 //z: parent.z - 1
z: 10000000 z: 10000000
} }

View File

@ -1,25 +1,19 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
import Proton import Proton
SettingsView { SettingsView {
@ -31,144 +25,138 @@ SettingsView {
fillHeight: false fillHeight: false
Label { Label {
Layout.fillWidth: true
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Settings") text: qsTr("Settings")
type: Label.Heading type: Label.Heading
Layout.fillWidth: true
} }
SettingsItem { SettingsItem {
id: autoUpdate id: autoUpdate
colorScheme: root.colorScheme
text: qsTr("Automatic updates")
description: qsTr("Bridge will automatically update in the background.")
type: SettingsItem.Toggle
checked: Backend.isAutomaticUpdateOn
onClicked: Backend.toggleAutomaticUpdate(!autoUpdate.checked)
Layout.fillWidth: true Layout.fillWidth: true
} checked: Backend.isAutomaticUpdateOn
colorScheme: root.colorScheme
description: qsTr("Bridge will automatically update in the background.")
text: qsTr("Automatic updates")
type: SettingsItem.Toggle
onClicked: Backend.toggleAutomaticUpdate(!autoUpdate.checked)
}
SettingsItem { SettingsItem {
id: autostart id: autostart
colorScheme: root.colorScheme
text: qsTr("Open on startup")
description: qsTr("Bridge will open upon startup.")
type: SettingsItem.Toggle
checked: Backend.isAutostartOn
onClicked: {
autostart.loading = true
Backend.toggleAutostart(!autostart.checked)
}
Connections{
target: Backend
function onToggleAutostartFinished() {
autostart.loading = false
}
}
Layout.fillWidth: true Layout.fillWidth: true
} checked: Backend.isAutostartOn
colorScheme: root.colorScheme
description: qsTr("Bridge will open upon startup.")
text: qsTr("Open on startup")
type: SettingsItem.Toggle
onClicked: {
autostart.loading = true;
Backend.toggleAutostart(!autostart.checked);
}
Connections {
function onToggleAutostartFinished() {
autostart.loading = false;
}
target: Backend
}
}
SettingsItem { SettingsItem {
id: beta id: beta
colorScheme: root.colorScheme Layout.fillWidth: true
text: qsTr("Beta access")
description: qsTr("Be among the first to try new features.")
type: SettingsItem.Toggle
checked: Backend.isBetaEnabled checked: Backend.isBetaEnabled
colorScheme: root.colorScheme
description: qsTr("Be among the first to try new features.")
text: qsTr("Beta access")
type: SettingsItem.Toggle
onClicked: { onClicked: {
if (!beta.checked) { if (!beta.checked) {
root.notifications.askEnableBeta() root.notifications.askEnableBeta();
} else { } else {
Backend.toggleBeta(false) Backend.toggleBeta(false);
} }
} }
Layout.fillWidth: true
} }
RowLayout { RowLayout {
ColorImage { ColorImage {
Layout.alignment: Qt.AlignCenter Layout.alignment: Qt.AlignCenter
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-down.svg" : "/qml/icons/ic-chevron-right.svg"
color: root.colorScheme.interaction_norm color: root.colorScheme.interaction_norm
height: root.colorScheme.body_font_size height: root.colorScheme.body_font_size
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-down.svg" : "/qml/icons/ic-chevron-right.svg"
sourceSize.height: root.colorScheme.body_font_size sourceSize.height: root.colorScheme.body_font_size
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: root._isAdvancedShown = !root._isAdvancedShown onClicked: root._isAdvancedShown = !root._isAdvancedShown
} }
} }
Label { Label {
id: advSettLabel id: advSettLabel
color: root.colorScheme.interaction_norm
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Advanced settings") text: qsTr("Advanced settings")
color: root.colorScheme.interaction_norm
type: Label.Body type: Label.Body
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: root._isAdvancedShown = !root._isAdvancedShown onClicked: root._isAdvancedShown = !root._isAdvancedShown
} }
} }
} }
SettingsItem { SettingsItem {
id: keychains id: keychains
visible: root._isAdvancedShown && Backend.availableKeychain.length > 1
colorScheme: root.colorScheme
text: qsTr("Change keychain")
description: qsTr("Change which keychain Bridge uses as default")
actionText: qsTr("Change")
type: SettingsItem.Button
checked: Backend.isDoHEnabled
onClicked: root.parent.showKeychainSettings()
Layout.fillWidth: true Layout.fillWidth: true
} actionText: qsTr("Change")
checked: Backend.isDoHEnabled
colorScheme: root.colorScheme
description: qsTr("Change which keychain Bridge uses as default")
text: qsTr("Change keychain")
type: SettingsItem.Button
visible: root._isAdvancedShown && Backend.availableKeychain.length > 1
onClicked: root.parent.showKeychainSettings()
}
SettingsItem { SettingsItem {
id: doh id: doh
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Alternative routing")
description: qsTr("If Protons servers are blocked in your location, alternative network routing will be used to reach Proton.")
type: SettingsItem.Toggle
checked: Backend.isDoHEnabled
onClicked: Backend.toggleDoH(!doh.checked)
Layout.fillWidth: true Layout.fillWidth: true
} checked: Backend.isDoHEnabled
colorScheme: root.colorScheme
description: qsTr("If Protons servers are blocked in your location, alternative network routing will be used to reach Proton.")
text: qsTr("Alternative routing")
type: SettingsItem.Toggle
visible: root._isAdvancedShown
onClicked: Backend.toggleDoH(!doh.checked)
}
SettingsItem { SettingsItem {
id: darkMode id: darkMode
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Dark mode")
description: qsTr("Choose dark color theme.")
type: SettingsItem.Toggle
checked: Backend.colorSchemeName == "dark"
onClicked: Backend.changeColorScheme( darkMode.checked ? "light" : "dark")
Layout.fillWidth: true Layout.fillWidth: true
} checked: Backend.colorSchemeName === "dark"
colorScheme: root.colorScheme
description: qsTr("Choose dark color theme.")
text: qsTr("Dark mode")
type: SettingsItem.Toggle
visible: root._isAdvancedShown
onClicked: Backend.changeColorScheme(darkMode.checked ? "light" : "dark")
}
SettingsItem { SettingsItem {
id: allMail id: allMail
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Show All Mail")
description: qsTr("Choose to list the All Mail folder in your local client.")
type: SettingsItem.Toggle
checked: Backend.isAllMailVisible
onClicked: root.notifications.askChangeAllMailVisibility(Backend.isAllMailVisible)
Layout.fillWidth: true Layout.fillWidth: true
} checked: Backend.isAllMailVisible
colorScheme: root.colorScheme
description: qsTr("Choose to list the All Mail folder in your local client.")
text: qsTr("Show All Mail")
type: SettingsItem.Toggle
visible: root._isAdvancedShown
onClicked: root.notifications.askChangeAllMailVisibility(Backend.isAllMailVisible)
}
SettingsItem { SettingsItem {
id: telemetry id: telemetry
Layout.fillWidth: true Layout.fillWidth: true
@ -181,73 +169,68 @@ SettingsView {
onClicked: Backend.toggleIsTelemetryDisabled(telemetry.checked) onClicked: Backend.toggleIsTelemetryDisabled(telemetry.checked)
} }
SettingsItem { SettingsItem {
id: ports id: ports
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Default ports")
actionText: qsTr("Change")
description: qsTr("Choose which ports are used by default.")
type: SettingsItem.Button
onClicked: root.parent.showPortSettings()
Layout.fillWidth: true Layout.fillWidth: true
} actionText: qsTr("Change")
colorScheme: root.colorScheme
description: qsTr("Choose which ports are used by default.")
text: qsTr("Default ports")
type: SettingsItem.Button
visible: root._isAdvancedShown
onClicked: root.parent.showPortSettings()
}
SettingsItem { SettingsItem {
id: imap id: imap
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Connection mode")
actionText: qsTr("Change")
description: qsTr("Change the protocol Bridge and the email client use to connect for IMAP and SMTP.")
type: SettingsItem.Button
onClicked: root.parent.showConnectionModeSettings()
Layout.fillWidth: true Layout.fillWidth: true
} actionText: qsTr("Change")
colorScheme: root.colorScheme
description: qsTr("Change the protocol Bridge and the email client use to connect for IMAP and SMTP.")
text: qsTr("Connection mode")
type: SettingsItem.Button
visible: root._isAdvancedShown
onClicked: root.parent.showConnectionModeSettings()
}
SettingsItem { SettingsItem {
id: cache id: cache
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Local cache")
actionText: qsTr("Configure")
description: qsTr("Configure Bridge's local cache.")
type: SettingsItem.Button
onClicked: root.parent.showLocalCacheSettings()
Layout.fillWidth: true Layout.fillWidth: true
} actionText: qsTr("Configure")
colorScheme: root.colorScheme
description: qsTr("Configure Bridge's local cache.")
text: qsTr("Local cache")
type: SettingsItem.Button
visible: root._isAdvancedShown
onClicked: root.parent.showLocalCacheSettings()
}
SettingsItem { SettingsItem {
id: exportTLSCertificates id: exportTLSCertificates
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Export TLS certificates")
actionText: qsTr("Export")
description: qsTr("Export the TLS private key and certificate used by the IMAP and SMTP servers.")
type: SettingsItem.Button
onClicked: {
Backend.exportTLSCertificates()
}
Layout.fillWidth: true Layout.fillWidth: true
actionText: qsTr("Export")
colorScheme: root.colorScheme
description: qsTr("Export the TLS private key and certificate used by the IMAP and SMTP servers.")
text: qsTr("Export TLS certificates")
type: SettingsItem.Button
visible: root._isAdvancedShown
onClicked: {
Backend.exportTLSCertificates();
}
} }
SettingsItem { SettingsItem {
id: reset id: reset
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Reset Bridge")
actionText: qsTr("Reset")
description: qsTr("Remove all accounts, clear cached data, and restore the original settings.")
type: SettingsItem.Button
onClicked: {
root.notifications.askResetBridge()
}
Layout.fillWidth: true Layout.fillWidth: true
actionText: qsTr("Reset")
colorScheme: root.colorScheme
description: qsTr("Remove all accounts, clear cached data, and restore the original settings.")
text: qsTr("Reset Bridge")
type: SettingsItem.Button
visible: root._isAdvancedShown
onClicked: {
root.notifications.askResetBridge();
}
} }
} }

View File

@ -1,126 +1,110 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import Proton import Proton
SettingsView { SettingsView {
id: root id: root
fillHeight: true fillHeight: true
Label { Label {
Layout.fillWidth: true
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Help") text: qsTr("Help")
type: Label.Heading type: Label.Heading
Layout.fillWidth: true
} }
SettingsItem { SettingsItem {
id: setupPage id: setupPage
colorScheme: root.colorScheme Layout.fillWidth: true
text: qsTr("Installation and setup")
actionText: qsTr("Go to help topics")
actionIcon: "/qml/icons/ic-external-link.svg" actionIcon: "/qml/icons/ic-external-link.svg"
actionText: qsTr("Go to help topics")
colorScheme: root.colorScheme
description: qsTr("Get help setting up your client with our instructions and FAQs.") description: qsTr("Get help setting up your client with our instructions and FAQs.")
text: qsTr("Installation and setup")
type: SettingsItem.PrimaryButton type: SettingsItem.PrimaryButton
onClicked: { onClicked: {
Backend.notifyKBArticleClicked("https://proton.me/support/bridge"); Backend.notifyKBArticleClicked("https://proton.me/support/bridge");
Qt.openUrlExternally("https://proton.me/support/bridge")} Qt.openUrlExternally("https://proton.me/support/bridge");
}
Layout.fillWidth: true
} }
SettingsItem { SettingsItem {
id: checkUpdates id: checkUpdates
colorScheme: root.colorScheme Layout.fillWidth: true
text: qsTr("Updates")
actionText: qsTr("Check now") actionText: qsTr("Check now")
description: qsTr("Check that you're using the latest version of Bridge. To stay up to date, enable auto-updates in settings.") colorScheme: root.colorScheme
description: qsTr("Check that you're using the latest version of Bridge.\nTo stay up to date, enable auto-updates in settings.")
text: qsTr("Updates")
type: SettingsItem.Button type: SettingsItem.Button
onClicked: { onClicked: {
checkUpdates.loading = true checkUpdates.loading = true;
Backend.checkUpdates() Backend.checkUpdates();
} }
Connections { Connections {
function onCheckUpdatesFinished() {
checkUpdates.loading = false;
}
target: Backend target: Backend
function onCheckUpdatesFinished() { checkUpdates.loading = false }
} }
Layout.fillWidth: true
} }
SettingsItem { SettingsItem {
id: logs id: logs
colorScheme: root.colorScheme
text: qsTr("Logs")
actionText: qsTr("View logs")
description: qsTr("Open and review logs to troubleshoot.")
type: SettingsItem.Button
onClicked: Qt.openUrlExternally(Backend.logsPath)
Layout.fillWidth: true Layout.fillWidth: true
} actionText: qsTr("View logs")
colorScheme: root.colorScheme
description: qsTr("Open and review logs to troubleshoot.")
text: qsTr("Logs")
type: SettingsItem.Button
onClicked: Qt.openUrlExternally(Backend.logsPath)
}
SettingsItem { SettingsItem {
id: reportBug id: reportBug
colorScheme: root.colorScheme
text: qsTr("Report a problem")
actionText: qsTr("Report a problem")
description: qsTr("Something not working as expected? Let us know.")
type: SettingsItem.Button
onClicked: {
Backend.updateCurrentMailClient()
Backend.notifyReportBugClicked()
root.parent.showBugReport()
}
Layout.fillWidth: true Layout.fillWidth: true
actionText: qsTr("Report problem")
colorScheme: root.colorScheme
description: qsTr("Something not working as expected? Let us know.")
text: qsTr("Report a problem")
type: SettingsItem.Button
onClicked: {
Backend.updateCurrentMailClient();
Backend.notifyReportBugClicked();
root.parent.showBugReport();
}
} }
// fill height so the footer label will be always attached to the bottom // fill height so the footer label will always be attached to the bottom
Item { Item {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
} }
Label { Label {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
colorScheme: root.colorScheme
type: Label.Caption
color: root.colorScheme.text_weak color: root.colorScheme.text_weak
textFormat: Text.StyledText colorScheme: root.colorScheme
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: qsTr("%1 v%2 (%3)<br>© 2017-%4 %5<br>%6 %7<br>%8").arg(Backend.appname).arg(Backend.version).arg(Backend.tag).arg(Backend.buildYear()).arg(Backend.vendor).arg(link(Backend.licensePath, qsTr("License"))).arg(link(Backend.dependencyLicensesLink, qsTr("Dependencies"))).arg(link(Backend.releaseNotesLink, qsTr("Release notes")))
textFormat: Text.StyledText
type: Label.Caption
text: qsTr("%1 v%2 (%3)<br>© 2017-%4 %5<br>%6 %7<br>%8"). onLinkActivated: function (link) {
arg(Backend.appname). Qt.openUrlExternally(link);
arg(Backend.version). }
arg(Backend.tag).
arg(Backend.buildYear()).
arg(Backend.vendor).
arg(link(Backend.licensePath, qsTr("License"))).
arg(link(Backend.dependencyLicensesLink, qsTr("Dependencies"))).
arg(link(Backend.releaseNotesLink, qsTr("Release notes")))
onLinkActivated: function(link) { Qt.openUrlExternally(link) }
} }
} }

View File

@ -1,116 +1,105 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
import Proton import Proton
SettingsView { SettingsView {
id: root id: root
fillHeight: false property bool _valuesChanged: keychainSelection.checkedButton && keychainSelection.checkedButton.text !== Backend.currentKeychain
property bool _valuesChanged: keychainSelection.checkedButton && keychainSelection.checkedButton.text != Backend.currentKeychain function setDefaultValues() {
for (const bi in keychainSelection.buttons) {
Label { const button = keychainSelection.buttons[bi];
colorScheme: root.colorScheme if (button.text === Backend.currentKeychain) {
text: qsTr("Default keychain") button.checked = true;
type: Label.Heading break;
Layout.fillWidth: true
}
Label {
colorScheme: root.colorScheme
text: qsTr("Change which keychain Bridge uses as default")
type: Label.Body
color: root.colorScheme.text_weak
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
ColumnLayout {
spacing: 16
ButtonGroup{ id: keychainSelection }
Repeater {
model: Backend.availableKeychain
RadioButton {
colorScheme: root.colorScheme
ButtonGroup.group: keychainSelection
text: modelData
} }
} }
} }
fillHeight: false
Rectangle { Component.onCompleted: root.setDefaultValues()
Layout.fillWidth: true onBack: {
height: 1 root.setDefaultValues();
color: root.colorScheme.border_weak
} }
Label {
Layout.fillWidth: true
colorScheme: root.colorScheme
text: qsTr("Default keychain")
type: Label.Heading
}
Label {
Layout.fillWidth: true
color: root.colorScheme.text_weak
colorScheme: root.colorScheme
text: qsTr("Change which keychain Bridge uses as default")
type: Label.Body
wrapMode: Text.WordWrap
}
ColumnLayout {
spacing: 16
ButtonGroup {
id: keychainSelection
}
Repeater {
model: Backend.availableKeychain
RadioButton {
ButtonGroup.group: keychainSelection
colorScheme: root.colorScheme
text: modelData
}
}
}
Rectangle {
Layout.fillWidth: true
color: root.colorScheme.border_weak
height: 1
}
RowLayout { RowLayout {
spacing: 12 spacing: 12
Button { Button {
id: submitButton id: submitButton
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Save and restart")
enabled: root._valuesChanged enabled: root._valuesChanged
text: qsTr("Save and restart")
onClicked: { onClicked: {
Backend.changeKeychain(keychainSelection.checkedButton.text) Backend.changeKeychain(keychainSelection.checkedButton.text);
} }
} }
Button { Button {
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Cancel")
onClicked: root.back()
secondary: true secondary: true
} text: qsTr("Cancel")
onClicked: root.back()
}
Connections { Connections {
target: Backend
function onChangeKeychainFinished() { function onChangeKeychainFinished() {
submitButton.loading = false submitButton.loading = false;
root.back() root.back();
} }
target: Backend
} }
} }
onBack: {
root.setDefaultValues()
}
function setDefaultValues(){
for (var bi in keychainSelection.buttons){
var button = keychainSelection.buttons[bi]
if (button.text == Backend.currentKeychain) {
button.checked = true
break;
}
}
}
Component.onCompleted: root.setDefaultValues()
} }

View File

@ -1,81 +1,88 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
import QtQuick.Dialogs import QtQuick.Dialogs
import Proton import Proton
SettingsView { SettingsView {
id: root id: root
fillHeight: false
property var notifications
property url diskCachePath: pathDialog.shortcuts.home property url diskCachePath: pathDialog.shortcuts.home
property var notifications
function refresh() { function refresh() {
diskCacheSetting.description = Backend.nativePath(root.diskCachePath) diskCacheSetting.description = Backend.nativePath(root.diskCachePath);
submitButton.enabled = (!submitButton.loading) && !Backend.areSameFileOrFolder(Backend.diskCachePath, root.diskCachePath) submitButton.enabled = (!submitButton.loading) && !Backend.areSameFileOrFolder(Backend.diskCachePath, root.diskCachePath);
}
function setDefaultValues() {
root.diskCachePath = Backend.diskCachePath;
root.refresh();
}
function submit() {
submitButton.loading = true;
Backend.setDiskCachePath(root.diskCachePath);
}
fillHeight: false
onBack: {
root.setDefaultValues();
}
onVisibleChanged: {
root.setDefaultValues();
} }
Label { Label {
Layout.fillWidth: true
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Local cache") text: qsTr("Local cache")
type: Label.Heading type: Label.Heading
Layout.fillWidth: true
} }
Label { Label {
Layout.fillWidth: true
color: root.colorScheme.text_weak
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Bridge stores your encrypted messages locally to optimize communication with your client.") text: qsTr("Bridge stores your encrypted messages locally to optimize communication with your client.")
type: Label.Body type: Label.Body
color: root.colorScheme.text_weak
Layout.fillWidth: true
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
SettingsItem { SettingsItem {
id: diskCacheSetting id: diskCacheSetting
colorScheme: root.colorScheme
text: qsTr("Current cache location")
actionText: qsTr("Change location")
descriptionWrap: Text.WrapAnywhere
type: SettingsItem.Button
onClicked: {
pathDialog.open()
}
Layout.fillWidth: true Layout.fillWidth: true
actionText: qsTr("Change location")
colorScheme: root.colorScheme
descriptionWrap: Text.WrapAnywhere
text: qsTr("Current cache location")
type: SettingsItem.Button
onClicked: {
pathDialog.open();
}
FolderDialog { FolderDialog {
id: pathDialog id: pathDialog
title: qsTr("Select cache location")
currentFolder: root.diskCachePath currentFolder: root.diskCachePath
onAccepted: { title: qsTr("Select cache location")
root.diskCachePath = pathDialog.selectedFolder
root.refresh()
}
}
}
onAccepted: {
root.diskCachePath = pathDialog.selectedFolder;
root.refresh();
}
}
}
RowLayout { RowLayout {
spacing: 12 spacing: 12
@ -83,43 +90,25 @@ SettingsView {
id: submitButton id: submitButton
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Save") text: qsTr("Save")
onClicked: { onClicked: {
root.submit() root.submit();
} }
} }
Button { Button {
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Cancel")
onClicked: root.back()
secondary: true secondary: true
} text: qsTr("Cancel")
onClicked: root.back()
}
Connections { Connections {
target: Backend
function onDiskCachePathChangeFinished() { function onDiskCachePathChangeFinished() {
submitButton.loading = false submitButton.loading = false;
root.setDefaultValues() root.setDefaultValues();
} }
target: Backend
} }
} }
onBack: {
root.setDefaultValues()
}
function submit() {
submitButton.loading = true
Backend.setDiskCachePath(root.diskCachePath)
}
function setDefaultValues(){
root.diskCachePath = Backend.diskCachePath
root.refresh();
}
onVisibleChanged: {
root.setDefaultValues()
}
} }

View File

@ -1,232 +1,199 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml import QtQml
import QtQuick import QtQuick
import QtQuick.Window import QtQuick.Window
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import Proton import Proton
import Notifications import Notifications
ApplicationWindow { ApplicationWindow {
id: root id: root
colorScheme: ProtonStyle.currentStyle
visible: true
property int _defaultWidth: 1080
property int _defaultHeight: 780 property int _defaultHeight: 780
width: _defaultWidth property int _defaultWidth: 1080
property var notifications
function selectUser(userID) {
contentWrapper.selectUser(userID);
}
function showAndRise() {
root.show();
root.raise();
if (!root.active) {
root.requestActivate();
}
}
function showHelp() {
contentWrapper.showHelp();
}
function showLocalCacheSettings() {
contentWrapper.showLocalCacheSettings();
}
function showSettings() {
contentWrapper.showSettings();
}
function showSetup(user, address) {
setupGuide.user = user;
setupGuide.address = address;
setupGuide.reset();
contentLayout._showSetup = !!setupGuide.user;
}
function showSignIn(username) {
if (contentLayout.currentIndex === 1)
return;
contentWrapper.showSignIn(username);
}
colorScheme: ProtonStyle.currentStyle
height: _defaultHeight height: _defaultHeight
minimumWidth: _defaultWidth minimumWidth: _defaultWidth
visible: true
property var notifications width: _defaultWidth
// show Setup Guide on every new user // show Setup Guide on every new user
Connections { Connections {
target: Backend.users
function onRowsInserted(parent, first, last) {
// considering that users are added one-by-one
var user = Backend.users.get(first)
if (user.state === EUserState.SignedOut) {
return
}
if (user.setupGuideSeen) {
return
}
root.showSetup(user,user.addresses[0])
}
function onRowsAboutToBeRemoved(parent, first, last) { function onRowsAboutToBeRemoved(parent, first, last) {
for (var i = first; i <= last; i++ ) { for (let i = first; i <= last; i++) {
var user = Backend.users.get(i) const user = Backend.users.get(i);
if (setupGuide.user === user) { if (setupGuide.user === user) {
setupGuide.user = null setupGuide.user = null;
contentLayout._showSetup = false contentLayout._showSetup = false;
return return;
} }
} }
} }
} function onRowsInserted(parent, first, _) {
// considering that users are added one-by-one
const user = Backend.users.get(first);
if (user.state === EUserState.SignedOut) {
return;
}
if (user.setupGuideSeen) {
return;
}
root.showSetup(user, user.addresses[0]);
}
target: Backend.users
}
Connections { Connections {
target: Backend
function onShowMainWindow() {
root.showAndRise()
}
function onLoginFinished(index, wasSignedOut) { function onLoginFinished(index, wasSignedOut) {
var user = Backend.users.get(index) const user = Backend.users.get(index);
if (user && !wasSignedOut) { if (user && !wasSignedOut) {
root.showSetup(user, user.addresses[0]) root.showSetup(user, user.addresses[0]);
} }
console.debug("Login finished", index) console.debug("Login finished", index);
} }
function onShowHelp() {
root.showHelp()
root.showAndRise()
}
function onShowSettings() {
root.showSettings()
root.showAndRise()
}
function onSelectUser(userID, forceShowWindow) { function onSelectUser(userID, forceShowWindow) {
contentWrapper.selectUser(userID) contentWrapper.selectUser(userID);
if (forceShowWindow) { if (forceShowWindow) {
root.showAndRise() root.showAndRise();
} }
} }
} function onShowHelp() {
root.showHelp();
root.showAndRise();
}
function onShowMainWindow() {
root.showAndRise();
}
function onShowSettings() {
root.showSettings();
root.showAndRise();
}
target: Backend
}
StackLayout { StackLayout {
id: contentLayout id: contentLayout
anchors.fill: parent
property bool _showSetup: false property bool _showSetup: false
anchors.fill: parent
currentIndex: { currentIndex: {
// show welcome when there are no users // show welcome when there are no users
if (Backend.users.count === 0) { if (Backend.users.count === 0) {
return 1 return 1;
} }
const u = Backend.users.get(0);
var u = Backend.users.get(0)
if (!u) { if (!u) {
console.trace() console.trace();
console.log("empty user") console.log("empty user");
return 1 return 1;
} }
if ((Backend.users.count === 1) && (u.state === EUserState.SignedOut)) { if ((Backend.users.count === 1) && (u.state === EUserState.SignedOut)) {
showSignIn(u.primaryEmailOrUsername()) showSignIn(u.primaryEmailOrUsername());
return 0 return 0;
} }
if (contentLayout._showSetup) { if (contentLayout._showSetup) {
return 2 return 2;
} }
return 0;
return 0
} }
ContentWrapper { // 0 ContentWrapper {
// 0
id: contentWrapper id: contentWrapper
Layout.fillHeight: true
Layout.fillWidth: true
colorScheme: root.colorScheme colorScheme: root.colorScheme
notifications: root.notifications notifications: root.notifications
Layout.fillHeight: true
Layout.fillWidth: true
onShowSetupGuide: function(user, address) {
root.showSetup(user,address)
}
onCloseWindow: { onCloseWindow: {
root.close() root.close();
} }
onQuitBridge: { onQuitBridge: {
// If we ever want to add a confirmation dialog before quitting: // If we ever want to add a confirmation dialog before quitting:
//root.notifications.askQuestion("Quit Bridge", "Insert warning message here.", "Quit", "Cancel", Backend.quit, null) //root.notifications.askQuestion("Quit Bridge", "Insert warning message here.", "Quit", "Cancel", Backend.quit, null)
root.close() root.close();
Backend.quit() Backend.quit();
}
onShowSetupGuide: function (user, address) {
root.showSetup(user, address);
} }
} }
WelcomeGuide {
WelcomeGuide { // 1
colorScheme: root.colorScheme
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true // 1
colorScheme: root.colorScheme
} }
SetupGuide {
SetupGuide { // 2 // 2
id: setupGuide id: setupGuide
colorScheme: root.colorScheme
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
colorScheme: root.colorScheme
onDismissed: { onDismissed: {
root.showSetup(null,"") root.showSetup(null, "");
} }
onFinished: { onFinished: {
// TODO: Do not close window. Trigger Backend to check that // TODO: Do not close window. Trigger Backend to check that
// there is a successfully connected client. Then Backend // there is a successfully connected client. Then Backend
// should send another signal to close the setup guide. // should send another signal to close the setup guide.
root.showSetup(null,"") root.showSetup(null, "");
} }
} }
} }
NotificationPopups { NotificationPopups {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notifications: root.notifications
mainWindow: root mainWindow: root
notifications: root.notifications
} }
SplashScreen { SplashScreen {
id: splashScreen id: splashScreen
colorScheme: root.colorScheme colorScheme: root.colorScheme
} }
function showLocalCacheSettings() { contentWrapper.showLocalCacheSettings() }
function showSettings() { contentWrapper.showSettings() }
function showHelp() { contentWrapper.showHelp() }
function selectUser(userID) { contentWrapper.selectUser(userID) }
function showBugReportAndPrefill(message) {
contentWrapper.showBugReportAndPrefill(message)
}
function showSignIn(username) {
if (contentLayout.currentIndex == 1) return
contentWrapper.showSignIn(username)
}
function showSetup(user, address) {
setupGuide.user = user
setupGuide.address = address
setupGuide.reset()
if (setupGuide.user) {
contentLayout._showSetup = true
} else {
contentLayout._showSetup = false
}
}
function showAndRise() {
root.show()
root.raise()
if (!root.active) {
root.requestActivate()
}
}
} }

View File

@ -1,118 +1,99 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml import QtQml
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import Proton import Proton
import Notifications import Notifications
Dialog { Dialog {
id: root id: root
default property alias data: additionalChildrenContainer.children
property var notification property var notification
shouldShow: notification && notification.active && !notification.dismissed
modal: true modal: true
shouldShow: notification && notification.active && !notification.dismissed
default property alias data: additionalChildrenContainer.children
ColumnLayout { ColumnLayout {
spacing: 0 spacing: 0
Image { Image {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: 16
sourceSize.width: 64
sourceSize.height: 64
Layout.preferredHeight: 64 Layout.preferredHeight: 64
Layout.preferredWidth: 64 Layout.preferredWidth: 64
Layout.bottomMargin: 16
visible: source != ""
source: { source: {
if (!root.notification) { if (!root.notification) {
return "" return "";
} }
switch (root.notification.type) { switch (root.notification.type) {
case Notification.NotificationType.Info: case Notification.NotificationType.Info:
return "/qml/icons/ic-info.svg" return "/qml/icons/ic-info.svg";
case Notification.NotificationType.Success: case Notification.NotificationType.Success:
return "/qml/icons/ic-success.svg" return "/qml/icons/ic-success.svg";
case Notification.NotificationType.Warning: case Notification.NotificationType.Warning:
case Notification.NotificationType.Danger: case Notification.NotificationType.Danger:
return "/qml/icons/ic-alert.svg" return "/qml/icons/ic-alert.svg";
} }
} }
sourceSize.height: 64
sourceSize.width: 64
visible: source != ""
} }
Label { Label {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
Layout.bottomMargin: 8 Layout.bottomMargin: 8
colorScheme: root.colorScheme colorScheme: root.colorScheme
horizontalAlignment: Text.AlignHCenter
text: root.notification.title text: root.notification.title
type: Label.LabelType.Title type: Label.LabelType.Title
} }
Label { Label {
Layout.bottomMargin: 16
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: 240 Layout.preferredWidth: 240
Layout.bottomMargin: 16
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: root.notification.description
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: root.notification.description
type: Label.LabelType.Body type: Label.LabelType.Body
onLinkActivated: function(link) { Qt.openUrlExternally(link) } wrapMode: Text.WordWrap
}
onLinkActivated: function (link) {
Qt.openUrlExternally(link);
}
}
Item { Item {
id: additionalChildrenContainer id: additionalChildrenContainer
Layout.fillWidth: true
Layout.bottomMargin: 16 Layout.bottomMargin: 16
Layout.fillWidth: true
visible: children.length > 0
implicitHeight: additionalChildrenContainer.childrenRect.height implicitHeight: additionalChildrenContainer.childrenRect.height
implicitWidth: additionalChildrenContainer.childrenRect.width implicitWidth: additionalChildrenContainer.childrenRect.width
visible: children.length > 0
} }
ColumnLayout { ColumnLayout {
spacing: 8 spacing: 8
Repeater { Repeater {
model: root.notification.action model: root.notification.action
delegate: Button { delegate: Button {
Layout.fillWidth: true Layout.fillWidth: true
colorScheme: root.colorScheme
action: modelData action: modelData
colorScheme: root.colorScheme
secondary: index > 0
loading: modelData.loading loading: modelData.loading
secondary: index > 0
} }
} }
} }

View File

@ -1,25 +1,19 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml import QtQml
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import Proton import Proton
import Notifications import Notifications
@ -27,118 +21,98 @@ Item {
id: root id: root
property ColorScheme colorScheme property ColorScheme colorScheme
property var notifications
property var mainWindow property var mainWindow
property int notificationWhitelist: NotificationFilter.FilterConsts.All
property int notificationBlacklist: NotificationFilter.FilterConsts.None property int notificationBlacklist: NotificationFilter.FilterConsts.None
property int notificationWhitelist: NotificationFilter.FilterConsts.All
property var notifications
NotificationFilter { NotificationFilter {
id: bannerNotificationFilter id: bannerNotificationFilter
source: root.notifications.all
blacklist: Notifications.Group.Dialogs blacklist: Notifications.Group.Dialogs
source: root.notifications.all
} }
Banner { Banner {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: bannerNotificationFilter.topmost
mainWindow: root.mainWindow mainWindow: root.mainWindow
notification: bannerNotificationFilter.topmost
} }
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.updateManualReady notification: root.notifications.updateManualReady
Switch { Switch {
id:autoUpdate id: autoUpdate
checked: Backend.isAutomaticUpdateOn
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Update automatically in the future") text: qsTr("Update automatically in the future")
checked: Backend.isAutomaticUpdateOn
onClicked: Backend.toggleAutomaticUpdate(autoUpdate.checked) onClicked: Backend.toggleAutomaticUpdate(autoUpdate.checked)
} }
} }
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.updateForce notification: root.notifications.updateForce
} }
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.updateForceError notification: root.notifications.updateForceError
} }
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.enableBeta notification: root.notifications.enableBeta
} }
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.cacheUnavailable notification: root.notifications.cacheUnavailable
} }
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.cacheCantMove notification: root.notifications.cacheCantMove
} }
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.diskFull notification: root.notifications.diskFull
} }
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.enableSplitMode notification: root.notifications.enableSplitMode
} }
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.resetBridge notification: root.notifications.resetBridge
} }
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.changeAllMailVisibility notification: root.notifications.changeAllMailVisibility
} }
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.deleteAccount notification: root.notifications.deleteAccount
} }
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.noKeychain notification: root.notifications.noKeychain
} }
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.rebuildKeychain notification: root.notifications.rebuildKeychain
} }
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.apiCertIssue notification: root.notifications.apiCertIssue
} }
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.noActiveKeyForRecipient notification: root.notifications.noActiveKeyForRecipient
} }
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.userBadEvent notification: root.notifications.userBadEvent
} }
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.genericError notification: root.notifications.genericError
} }
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.genericQuestion notification: root.notifications.genericQuestion

View File

@ -1,54 +1,45 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml import QtQml
import QtQuick.Controls import QtQuick.Controls
QtObject { QtObject {
id: root id: root
default property var children
enum NotificationType { enum NotificationType {
Info = 0, Info,
Success = 1, Success,
Warning = 2, Warning,
Danger = 3 Danger
} }
property list<Action> action
property bool active: false
// brief is used in status view only
property string brief
default property var children
property var data
// description is used in banners and in dialogs as description
property string description
property bool dismissed: false
property int group
property string icon
readonly property var occurred: active ? new Date() : undefined
// title is used in dialogs only // title is used in dialogs only
property string title property string title
// description is used in banners and in dialogs as description
property string description
// brief is used in status view only
property string brief
property string icon
property list<Action> action
property int type property int type
property int group
property bool dismissed: false
property bool active: false
readonly property var occurred: active ? new Date() : undefined
property var data
onActiveChanged: { onActiveChanged: {
dismissed = false dismissed = false;
} }
} }

View File

@ -1,114 +1,95 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml import QtQml
import QtQml.Models import QtQml.Models
// contains notifications that satisfy black- and whitelist and are sorted in time-occurred order // contains notifications that satisfy black- and whitelist and are sorted in time-occurred order
ListModel { ListModel {
id: root id: root
enum FilterConsts { enum FilterConsts {
None = 0, None,
All = 255 All = 255
} }
property int whitelist: NotificationFilter.FilterConsts.All
property int blacklist: NotificationFilter.FilterConsts.None property int blacklist: NotificationFilter.FilterConsts.None
property Notification topmost
property var source
property bool componentCompleted: false property bool componentCompleted: false
Component.onCompleted: { property var source
root.componentCompleted = true property Notification topmost
root.rebuildList() property int whitelist: NotificationFilter.FilterConsts.All
}
// overriding get method to ignore any role and return directly object itself // overriding get method to ignore any role and return directly object itself
function get(row) { function get(row) {
if (row < 0 || row >= count) { if (row < 0 || row >= count) {
return undefined return undefined;
} }
return data(index(row, 0), Qt.DisplayRole) return data(index(row, 0), Qt.DisplayRole);
} }
function rebuildList() { function rebuildList() {
let i;
// avoid evaluation of the list before Component.onCompleted // avoid evaluation of the list before Component.onCompleted
if (!root.componentCompleted) { if (!root.componentCompleted) {
return return;
} }
for (i = 0; i < root.count; i++) {
for (var i = 0; i < root.count; i++) { root.get(i).onActiveChanged.disconnect(root.updateList);
root.get(i).onActiveChanged.disconnect( root.updateList )
} }
root.clear();
root.clear()
if (!root.source) { if (!root.source) {
return return;
} }
for (i = 0; i < root.source.length; i++) { for (i = 0; i < root.source.length; i++) {
var obj = root.source[i] const obj = root.source[i];
if (obj.group & root.blacklist) { if (obj.group & root.blacklist) {
continue continue;
} }
if (!(obj.group & root.whitelist)) { if (!(obj.group & root.whitelist)) {
continue continue;
} }
root.append({
root.append({obj}) "obj": obj
obj.onActiveChanged.connect( root.updateList ) });
obj.onActiveChanged.connect(root.updateList);
} }
} }
function updateList() { function updateList() {
var topmost = null let topmost = null;
for (let i = 0; i < root.count; i++) {
for (var i = 0; i < root.count; i++) { const obj = root.get(i);
var obj = root.get(i)
if (!obj.active) { if (!obj.active) {
continue continue;
} }
if (topmost && (topmost.type > obj.type)) { if (topmost && (topmost.type > obj.type)) {
continue continue;
} }
if (topmost && (topmost.type === obj.type) && (topmost.occurred > obj.occurred)) { if (topmost && (topmost.type === obj.type) && (topmost.occurred > obj.occurred)) {
continue continue;
} }
topmost = obj;
topmost = obj
} }
root.topmost = topmost;
root.topmost = topmost
} }
onWhitelistChanged: { Component.onCompleted: {
root.rebuildList() root.componentCompleted = true;
root.rebuildList();
} }
onBlacklistChanged: { onBlacklistChanged: {
root.rebuildList() root.rebuildList();
} }
onSourceChanged: { onSourceChanged: {
root.rebuildList() root.rebuildList();
}
onWhitelistChanged: {
root.rebuildList();
} }
} }

View File

@ -1,178 +1,157 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
import Proton import Proton
SettingsView { SettingsView {
id: root id: root
fillHeight: false property bool _valuesChanged: (imapField.text * 1 !== Backend.imapPort || smtpField.text * 1 !== Backend.smtpPort)
property var notifications property var notifications
property bool _valuesChanged: ( function isPortFree(field) {
imapField.text*1 !== Backend.imapPort || const num = field.text * 1;
smtpField.text*1 !== Backend.smtpPort if (num === Backend.imapPort)
) return true;
if (num === Backend.smtpPort)
return true;
if (!Backend.isPortFree(num)) {
field.error = true;
field.errorString = qsTr("Port occupied");
return false;
}
return true;
}
function setDefaultValues() {
imapField.text = Backend.imapPort;
smtpField.text = Backend.smtpPort;
imapField.error = false;
smtpField.error = false;
}
function validate(port) {
const num = port * 1;
if (!(num > 1 && num < 65536)) {
return qsTr("Invalid port number");
}
if (imapField.text === smtpField.text) {
return qsTr("Port numbers must be different");
}
}
fillHeight: false
Component.onCompleted: root.setDefaultValues()
onBack: {
root.setDefaultValues();
}
Label { Label {
Layout.fillWidth: true
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Default ports") text: qsTr("Default ports")
type: Label.Heading type: Label.Heading
Layout.fillWidth: true
} }
Label { Label {
Layout.fillWidth: true
color: root.colorScheme.text_weak
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Changes require reconfiguration of your email client.") text: qsTr("Changes require reconfiguration of your email client.")
type: Label.Body type: Label.Body
color: root.colorScheme.text_weak
Layout.fillWidth: true
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
RowLayout { RowLayout {
spacing: 16 spacing: 16
TextField { TextField {
id: imapField id: imapField
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
Layout.preferredWidth: 160
colorScheme: root.colorScheme colorScheme: root.colorScheme
label: qsTr("IMAP port") label: qsTr("IMAP port")
Layout.preferredWidth: 160
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
validator: root.validate validator: root.validate
} }
TextField { TextField {
id: smtpField id: smtpField
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
Layout.preferredWidth: 160
colorScheme: root.colorScheme colorScheme: root.colorScheme
label: qsTr("SMTP port") label: qsTr("SMTP port")
Layout.preferredWidth: 160
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
validator: root.validate validator: root.validate
} }
} }
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
height: 1
color: root.colorScheme.border_weak color: root.colorScheme.border_weak
height: 1
} }
RowLayout { RowLayout {
spacing: 12 spacing: 12
Button { Button {
id: submitButton id: submitButton
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Save")
enabled: (!loading) && root._valuesChanged enabled: (!loading) && root._valuesChanged
text: qsTr("Save")
onClicked: { onClicked: {
// removing error here because we may have set it manually (port occupied) // removing error here because we may have set it manually (port occupied)
imapField.error = false imapField.error = false;
smtpField.error = false smtpField.error = false;
// checking errors separately because we want to display "same port" error only once // checking errors separately because we want to display "same port" error only once
imapField.validate() imapField.validate();
if (imapField.error) { if (imapField.error) {
return return;
} }
smtpField.validate() smtpField.validate();
if (smtpField.error) { if (smtpField.error) {
return return;
} }
submitButton.loading = true;
submitButton.loading = true
// check both ports before returning an error // check both ports before returning an error
var err = false let err = false;
err |= !isPortFree(imapField) err |= !isPortFree(imapField);
err |= !isPortFree(smtpField) err |= !isPortFree(smtpField);
if (err) { if (err) {
submitButton.loading = false submitButton.loading = false;
return return;
} }
// We turn off all port error notification. They well be restored if problems persist // We turn off all port error notification. They will be restored if problems persist
root.notifications.imapPortStartupError.active = false root.notifications.imapPortStartupError.active = false;
root.notifications.smtpPortStartupError.active = false root.notifications.smtpPortStartupError.active = false;
root.notifications.imapPortChangeError.active = false root.notifications.imapPortChangeError.active = false;
root.notifications.smtpPortChangeError.active = false root.notifications.smtpPortChangeError.active = false;
Backend.setMailServerSettings(imapField.text, smtpField.text, Backend.useSSLForIMAP, Backend.useSSLForSMTP);
Backend.setMailServerSettings(imapField.text, smtpField.text, Backend.useSSLForIMAP, Backend.useSSLForSMTP)
} }
} }
Button { Button {
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Cancel")
onClicked: root.back()
secondary: true secondary: true
} text: qsTr("Cancel")
onClicked: root.back()
}
Connections { Connections {
target: Backend
function onChangeMailServerSettingsFinished() { function onChangeMailServerSettingsFinished() {
submitButton.loading = false submitButton.loading = false;
} }
target: Backend
} }
} }
onBack: {
root.setDefaultValues()
}
function validate(port) {
var num = port*1
if (! (num > 1 && num < 65536) ) {
return qsTr("Invalid port number")
}
if (imapField.text == smtpField.text) {
return qsTr("Port numbers must be different")
}
return
}
function isPortFree(field) {
var num = field.text*1
if (num === Backend.imapPort) return true
if (num === Backend.smtpPort) return true
if (!Backend.isPortFree(num)) {
field.error = true
field.errorString = qsTr("Port occupied")
return false
}
return true
}
function setDefaultValues(){
imapField.text = Backend.imapPort
smtpField.text = Backend.smtpPort
imapField.error = false
smtpField.error = false
}
Component.onCompleted: root.setDefaultValues()
} }

View File

@ -1,20 +1,15 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Templates as T import QtQuick.Templates as T

View File

@ -1,20 +1,15 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml import QtQml
import QtQuick import QtQuick
import QtQuick.Window import QtQuick.Window
@ -25,14 +20,14 @@ import QtQuick.Templates as T
T.ApplicationWindow { T.ApplicationWindow {
id: root id: root
property ColorScheme colorScheme
// popup priority based on types // popup priority based on types
enum PopupType { enum PopupType {
Banner = 0, Banner,
Dialog = 1 Dialog
} }
property ColorScheme colorScheme
// contains currently visible popup // contains currently visible popup
property var popupVisible: null property var popupVisible: null
@ -41,85 +36,61 @@ T.ApplicationWindow {
// overriding get method to ignore any role and return directly object itself // overriding get method to ignore any role and return directly object itself
function get(row) { function get(row) {
if (row < 0 || row >= count) { if (row < 0 || row >= count) {
return undefined return undefined;
} }
return data(index(row, 0), Qt.DisplayRole) return data(index(row, 0), Qt.DisplayRole);
}
onRowsInserted: function(parent, first, last) {
for (var i = first; i <= last; i++) {
var obj = popups.get(i)
obj.onShouldShowChanged.connect( root.processPopups )
}
processPopups()
} }
onRowsAboutToBeRemoved: function (parent, first, last) { onRowsAboutToBeRemoved: function (parent, first, last) {
for (var i = first; i <= last; i++ ) { for (let i = first; i <= last; i++) {
var obj = popups.get(i) const obj = popups.get(i);
obj.onShouldShowChanged.disconnect( root.processPopups ) obj.onShouldShowChanged.disconnect(root.processPopups);
// if currently visible popup was removed // if currently visible popup was removed
if (root.popupVisible === obj) { if (root.popupVisible === obj) {
root.popupVisible.visible = false root.popupVisible.visible = false;
root.popupVisible = null root.popupVisible = null;
} }
} }
processPopups();
processPopups() }
onRowsInserted: function (parent, first, last) {
for (let i = first; i <= last; i++) {
const obj = popups.get(i);
obj.onShouldShowChanged.connect(root.processPopups);
}
processPopups();
} }
} }
function processPopups() { function processPopups() {
if ((root.popupVisible) && (!root.popupVisible.shouldShow)) { if ((root.popupVisible) && (!root.popupVisible.shouldShow)) {
root.popupVisible.visible = false root.popupVisible.visible = false;
} }
let topmost = null;
var topmost = null for (let i = 0; i < popups.count; i++) {
for (var i = 0; i < popups.count; i++) { const obj = popups.get(i);
var obj = popups.get(i)
if (obj.shouldShow === false) { if (obj.shouldShow === false) {
continue continue;
} }
if (topmost && (topmost.popupType > obj.popupType)) { if (topmost && (topmost.popupType > obj.popupType)) {
continue continue;
} }
if (topmost && (topmost.popupType === obj.popupType) && (topmost.occurred > obj.occurred)) { if (topmost && (topmost.popupType === obj.popupType) && (topmost.occurred > obj.occurred)) {
continue continue;
} }
topmost = obj;
topmost = obj
} }
if (root.popupVisible !== topmost) { if (root.popupVisible !== topmost) {
if (root.popupVisible) { if (root.popupVisible) {
root.popupVisible.visible = false root.popupVisible.visible = false;
} }
root.popupVisible = topmost root.popupVisible = topmost;
} }
if (!root.popupVisible) { if (!root.popupVisible) {
return return;
}
root.popupVisible.visible = true
}
Connections {
target: root.popupVisible
function onVisibleChanged() {
if (root.popupVisible.visible) {
return
}
root.popupVisible = null
root.processPopups()
} }
root.popupVisible.visible = true;
} }
color: root.colorScheme.background_norm color: root.colorScheme.background_norm
@ -127,8 +98,19 @@ T.ApplicationWindow {
Overlay.modal: Rectangle { Overlay.modal: Rectangle {
color: root.colorScheme.backdrop_norm color: root.colorScheme.backdrop_norm
} }
Overlay.modeless: Rectangle { Overlay.modeless: Rectangle {
color: "transparent" color: "transparent"
} }
Connections {
function onVisibleChanged() {
if (root.popupVisible.visible) {
return;
}
root.popupVisible = null;
root.processPopups();
}
target: root.popupVisible
}
} }

View File

@ -1,20 +1,15 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
@ -23,212 +18,169 @@ import QtQuick.Layouts
import "." as Proton import "." as Proton
T.Button { T.Button {
property ColorScheme colorScheme
property alias secondary: control.flat
readonly property bool primary: !secondary
readonly property bool isIcon: control.text === ""
readonly property bool hasTextAndIcon: (control.text !== "") && (iconImage.source.toString().length > 0)
property bool loading: false
property bool borderless: false
property int labelType: Proton.Label.LabelType.Body
property alias textVerticalAlignment: label.verticalAlignment
property alias textHorizontalAlignment: label.horizontalAlignment
id: control id: control
implicitWidth: Math.max( property bool borderless: false
implicitBackgroundWidth + leftInset + rightInset, property ColorScheme colorScheme
implicitContentWidth + leftPadding + rightPadding readonly property bool hasTextAndIcon: (control.text !== "") && (iconImage.source.toString().length > 0)
) readonly property bool isIcon: control.text === ""
implicitHeight: Math.max( property int labelType: Proton.Label.LabelType.Body
implicitBackgroundHeight + topInset + bottomInset, property bool loading: false
implicitContentHeight + topPadding + bottomPadding readonly property bool primary: !secondary
) property alias secondary: control.flat
property alias textHorizontalAlignment: label.horizontalAlignment
padding: 8 property alias textVerticalAlignment: label.verticalAlignment
horizontalPadding: 16
spacing: 10
font: label.font font: label.font
horizontalPadding: 16
icon.width: 16
icon.height: 16
icon.color: { icon.color: {
if (primary && !isIcon) { if (primary && !isIcon) {
return "#FFFFFF" return "#FFFFFF";
} else { } else {
return control.colorScheme.text_norm return control.colorScheme.text_norm;
} }
} }
icon.height: 16
icon.width: 16
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding)
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding)
padding: 8
spacing: 10
background: Rectangle {
border.color: {
return control.colorScheme.border_norm;
}
border.width: secondary && !borderless ? 1 : 0
color: {
if (!isIcon) {
if (primary) {
// Primary colors
if (control.down) {
return control.colorScheme.interaction_norm_active;
}
if (control.enabled && (control.highlighted || control.hovered || control.checked || control.activeFocus)) {
return control.colorScheme.interaction_norm_hover;
}
if (control.loading) {
return control.colorScheme.interaction_norm_hover;
}
return control.colorScheme.interaction_norm;
} else {
// Secondary colors
if (control.down) {
return control.colorScheme.interaction_default_active;
}
if (control.enabled && (control.highlighted || control.hovered || control.checked || control.activeFocus)) {
return control.colorScheme.interaction_default_hover;
}
if (control.loading) {
return control.colorScheme.interaction_default_hover;
}
return control.colorScheme.interaction_default;
}
} else {
if (primary) {
// Primary icon colors
if (control.down) {
return control.colorScheme.interaction_default_active;
}
if (control.enabled && (control.highlighted || control.hovered || control.checked || control.activeFocus)) {
return control.colorScheme.interaction_default_hover;
}
if (control.loading) {
return control.colorScheme.interaction_default_hover;
}
return control.colorScheme.interaction_default;
} else {
// Secondary icon colors
if (control.down) {
return control.colorScheme.interaction_default_active;
}
if (control.enabled && (control.highlighted || control.hovered || control.checked || control.activeFocus)) {
return control.colorScheme.interaction_default_hover;
}
if (control.loading) {
return control.colorScheme.interaction_default_hover;
}
return control.colorScheme.interaction_default;
}
}
}
implicitHeight: 36
implicitWidth: 36
opacity: control.enabled || control.loading ? 1.0 : 0.5
radius: ProtonStyle.button_radius
visible: true
}
contentItem: RowLayout { contentItem: RowLayout {
id: _contentItem id: _contentItem
spacing: control.hasTextAndIcon ? control.spacing : 0 spacing: control.hasTextAndIcon ? control.spacing : 0
Proton.Label { Proton.Label {
colorScheme: root.colorScheme
id: label id: label
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
elide: Text.ElideRight
horizontalAlignment: Qt.AlignHCenter
visible: !control.isIcon
text: control.text
color: { color: {
if (primary && !isIcon) { if (primary && !isIcon) {
return "#FFFFFF" return "#FFFFFF";
} else { } else {
return control.colorScheme.text_norm return control.colorScheme.text_norm;
} }
} }
colorScheme: root.colorScheme
elide: Text.ElideRight
horizontalAlignment: Qt.AlignHCenter
opacity: control.enabled || control.loading ? 1.0 : 0.5 opacity: control.enabled || control.loading ? 1.0 : 0.5
text: control.text
type: labelType type: labelType
visible: !control.isIcon
} }
ColorImage { ColorImage {
id: iconImage id: iconImage
Layout.alignment: Qt.AlignCenter Layout.alignment: Qt.AlignCenter
color: control.icon.color
height: {
if (control.loading) {
return width;
}
Math.min(control.icon.height, availableHeight);
}
source: control.loading ? "/qml/icons/Loader_16.svg" : control.icon.source
sourceSize.height: control.icon.height
sourceSize.width: control.icon.width
visible: control.loading || control.icon.source
width: { width: {
// special case for loading since we want icon to be square for rotation animation // special case for loading since we want icon to be square for rotation animation
if (control.loading) { if (control.loading) {
return Math.min(control.icon.width, availableWidth, control.icon.height, availableHeight) return Math.min(control.icon.width, availableWidth, control.icon.height, availableHeight);
} }
return Math.min(control.icon.width, availableWidth);
return Math.min(control.icon.width, availableWidth)
} }
height: {
if (control.loading) {
return width
}
Math.min(control.icon.height, availableHeight)
}
sourceSize.width: control.icon.width
sourceSize.height: control.icon.height
color: control.icon.color
source: control.loading ? "/qml/icons/Loader_16.svg" : control.icon.source
visible: control.loading || control.icon.source
RotationAnimation { RotationAnimation {
target: iconImage direction: RotationAnimation.Clockwise
loops: Animation.Infinite
duration: 1000 duration: 1000
from: 0 from: 0
to: 360 loops: Animation.Infinite
direction: RotationAnimation.Clockwise
running: control.loading running: control.loading
target: iconImage
to: 360
} }
} }
} }
background: Rectangle {
implicitWidth: 36
implicitHeight: 36
radius: ProtonStyle.button_radius
visible: true
color: {
if (!isIcon) {
if (primary) {
// Primary colors
if (control.down) {
return control.colorScheme.interaction_norm_active
}
if (control.enabled && (control.highlighted || control.hovered || control.checked || control.activeFocus)) {
return control.colorScheme.interaction_norm_hover
}
if (control.loading) {
return control.colorScheme.interaction_norm_hover
}
return control.colorScheme.interaction_norm
} else {
// Secondary colors
if (control.down) {
return control.colorScheme.interaction_default_active
}
if (control.enabled && (control.highlighted || control.hovered || control.checked || control.activeFocus)) {
return control.colorScheme.interaction_default_hover
}
if (control.loading) {
return control.colorScheme.interaction_default_hover
}
return control.colorScheme.interaction_default
}
} else {
if (primary) {
// Primary icon colors
if (control.down) {
return control.colorScheme.interaction_default_active
}
if (control.enabled && (control.highlighted || control.hovered || control.checked || control.activeFocus)) {
return control.colorScheme.interaction_default_hover
}
if (control.loading) {
return control.colorScheme.interaction_default_hover
}
return control.colorScheme.interaction_default
} else {
// Secondary icon colors
if (control.down) {
return control.colorScheme.interaction_default_active
}
if (control.enabled && (control.highlighted || control.hovered || control.checked || control.activeFocus)) {
return control.colorScheme.interaction_default_hover
}
if (control.loading) {
return control.colorScheme.interaction_default_hover
}
return control.colorScheme.interaction_default
}
}
}
border.color: {
return control.colorScheme.border_norm
}
border.width: secondary && !borderless ? 1 : 0
opacity: control.enabled || control.loading ? 1.0 : 0.5
}
Component.onCompleted: { Component.onCompleted: {
if (!control.colorScheme) { if (!control.colorScheme) {
console.trace() console.trace();
var next = root let next = root;
for (var i = 0; i<1000; i++) { for (let i = 0; i < 1000; i++) {
console.log(i, next, "colorscheme", next.colorScheme) console.log(i, next, "colorscheme", next.colorScheme);
next = next.parent next = next.parent;
if (!next) break if (!next)
break;
} }
console.error("ColorScheme not defined") console.error("ColorScheme not defined");
} }
} }
} }

View File

@ -1,97 +1,96 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
import QtQuick.Templates as T import QtQuick.Templates as T
T.CheckBox { T.CheckBox {
property ColorScheme colorScheme
property bool error: false
id: control id: control
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, property ColorScheme colorScheme
implicitContentWidth + leftPadding + rightPadding) property bool error: false
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding,
implicitIndicatorHeight + topPadding + bottomPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding, implicitIndicatorHeight + topPadding + bottomPadding)
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding)
padding: 0 padding: 0
spacing: 8 spacing: 8
contentItem: CheckLabel {
color: {
if (!enabled) {
return control.colorScheme.text_disabled;
}
if (error) {
return control.colorScheme.signal_danger;
}
return control.colorScheme.text_norm;
}
font.family: ProtonStyle.font_family
font.letterSpacing: ProtonStyle.body_letter_spacing
font.pixelSize: ProtonStyle.body_font_size
font.weight: ProtonStyle.fontWeight_400
leftPadding: control.indicator && !control.mirrored ? control.indicator.width + control.spacing : 0
lineHeight: ProtonStyle.body_line_height
lineHeightMode: Text.FixedHeight
rightPadding: control.indicator && control.mirrored ? control.indicator.width + control.spacing : 0
text: control.text
}
indicator: Rectangle { indicator: Rectangle {
implicitWidth: 20 border.color: {
if (!control.enabled) {
return control.colorScheme.field_disabled;
}
if (control.error) {
return control.colorScheme.signal_danger;
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover;
}
return control.colorScheme.field_norm;
}
border.width: control.checked ? 0 : 1
color: {
if (!checked) {
return control.colorScheme.background_norm;
}
if (!control.enabled) {
return control.colorScheme.field_disabled;
}
if (control.error) {
return control.colorScheme.signal_danger;
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover;
}
return control.colorScheme.interaction_norm;
}
implicitHeight: 20 implicitHeight: 20
implicitWidth: 20
radius: ProtonStyle.checkbox_radius radius: ProtonStyle.checkbox_radius
x: text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2 x: text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2
y: control.topPadding + (control.availableHeight - height) / 2 y: control.topPadding + (control.availableHeight - height) / 2
color: {
if (!checked) {
return control.colorScheme.background_norm
}
if (!control.enabled) {
return control.colorScheme.field_disabled
}
if (control.error) {
return control.colorScheme.signal_danger
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover
}
return control.colorScheme.interaction_norm
}
border.width: control.checked ? 0 : 1
border.color: {
if (!control.enabled) {
return control.colorScheme.field_disabled
}
if (control.error) {
return control.colorScheme.signal_danger
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover
}
return control.colorScheme.field_norm
}
ColorImage { ColorImage {
color: "#FFFFFF"
height: parent.height - 4
source: "/qml/icons/ic-check.svg"
sourceSize.height: parent.height - 4
sourceSize.width: parent.width - 4
visible: control.checkState === Qt.Checked
width: parent.width - 4
x: (parent.width - width) / 2 x: (parent.width - width) / 2
y: (parent.height - height) / 2 y: (parent.height - height) / 2
width: parent.width - 4
height: parent.height - 4
sourceSize.width: parent.width - 4
sourceSize.height: parent.height - 4
color: "#FFFFFF"
source: "/qml/icons/ic-check.svg"
visible: control.checkState === Qt.Checked
} }
// TODO: do we need PartiallyChecked state? // TODO: do we need PartiallyChecked state?
@ -105,30 +104,4 @@ T.CheckBox {
// visible: control.checkState === Qt.PartiallyChecked // visible: control.checkState === Qt.PartiallyChecked
//} //}
} }
contentItem: CheckLabel {
leftPadding: control.indicator && !control.mirrored ? control.indicator.width + control.spacing : 0
rightPadding: control.indicator && control.mirrored ? control.indicator.width + control.spacing : 0
text: control.text
color: {
if (!enabled) {
return control.colorScheme.text_disabled
}
if (error) {
return control.colorScheme.signal_danger
}
return control.colorScheme.text_norm
}
font.family: ProtonStyle.font_family
font.weight: ProtonStyle.fontWeight_400
font.pixelSize: ProtonStyle.body_font_size
lineHeight: ProtonStyle.body_line_height
lineHeightMode: Text.FixedHeight
font.letterSpacing: ProtonStyle.body_letter_spacing
}
} }

View File

@ -1,93 +1,88 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQml import QtQml
QtObject { QtObject {
// should be a pointer to ColorScheme object
property var prominent
// Primary // Backdrop
property color primary_norm property color backdrop_norm
property color background_avatar
// Interaction-norm // Background
property color interaction_norm property color background_norm
property color interaction_norm_hover property color background_strong
property color interaction_norm_active property color background_weak
// Text
property color text_norm
property color text_weak
property color text_hint
property color text_disabled
property color text_invert
// Field
property color field_norm
property color field_hover
property color field_disabled
// Border // Border
property color border_norm property color border_norm
property color border_weak property color border_weak
property color field_disabled
property color field_hover
// Background // Field
property color background_norm property color field_norm
property color background_weak
property color background_strong
property color background_avatar
// Interaction-weak
property color interaction_weak
property color interaction_weak_hover
property color interaction_weak_active
// Interaction-default // Interaction-default
property color interaction_default property color interaction_default
property color interaction_default_hover
property color interaction_default_active property color interaction_default_active
property color interaction_default_hover
// Interaction-norm
property color interaction_norm
property color interaction_norm_active
property color interaction_norm_hover
// Interaction-weak
property color interaction_weak
property color interaction_weak_active
property color interaction_weak_hover
property string logo_img
// Primary
property color primary_norm
// should be a pointer to ColorScheme object
property var prominent
property color scrollbar_hover
// Scrollbar // Scrollbar
property color scrollbar_norm property color scrollbar_norm
property color scrollbar_hover property color shadow_lifted
// Signal
property color signal_danger
property color signal_danger_hover
property color signal_danger_active
property color signal_warning
property color signal_warning_hover
property color signal_warning_active
property color signal_success
property color signal_success_hover
property color signal_success_active
property color signal_info
property color signal_info_hover
property color signal_info_active
// Shadows // Shadows
property color shadow_norm property color shadow_norm
property color shadow_lifted
// Backdrop // Signal
property color backdrop_norm property color signal_danger
property color signal_danger_active
property color signal_danger_hover
property color signal_info
property color signal_info_active
property color signal_info_hover
property color signal_success
property color signal_success_active
property color signal_success_hover
property color signal_warning
property color signal_warning_active
property color signal_warning_hover
property color text_disabled
property color text_hint
property color text_invert
// Text
property color text_norm
property color text_weak
// Images // Images
property string welcome_img property string welcome_img
property string logo_img
} }

View File

@ -1,20 +1,15 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Window import QtQuick.Window
import QtQuick.Controls import QtQuick.Controls
@ -26,148 +21,124 @@ T.ComboBox {
property ColorScheme colorScheme property ColorScheme colorScheme
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, bottomPadding: 5
implicitContentWidth + leftPadding + rightPadding) font.family: ProtonStyle.font_family
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, font.letterSpacing: ProtonStyle.body_letter_spacing
implicitContentHeight + topPadding + bottomPadding, font.pixelSize: ProtonStyle.body_font_size
implicitIndicatorHeight + topPadding + bottomPadding) font.weight: ProtonStyle.fontWeight_400
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding, implicitIndicatorHeight + topPadding + bottomPadding)
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding)
leftPadding: 12 + (!root.mirrored || !indicator || !indicator.visible ? 0 : indicator.width + spacing) leftPadding: 12 + (!root.mirrored || !indicator || !indicator.visible ? 0 : indicator.width + spacing)
rightPadding: 12 + (root.mirrored || !indicator || !indicator.visible ? 0 : indicator.width + spacing) rightPadding: 12 + (root.mirrored || !indicator || !indicator.visible ? 0 : indicator.width + spacing)
topPadding: 5
bottomPadding: 5
spacing: 8 spacing: 8
topPadding: 5
font.family: ProtonStyle.font_family background: Rectangle {
font.weight: ProtonStyle.fontWeight_400 border.color: root.colorScheme.border_norm
font.pixelSize: ProtonStyle.body_font_size border.width: 1
font.letterSpacing: ProtonStyle.body_letter_spacing color: {
if (root.down) {
return root.colorScheme.interaction_default_active;
}
if (root.enabled && root.hovered || root.activeFocus) {
return root.colorScheme.interaction_default_hover;
}
if (!root.enabled) {
return root.colorScheme.interaction_default;
}
return root.colorScheme.background_norm;
}
implicitHeight: 36
implicitWidth: 140
radius: ProtonStyle.context_item_radius
}
contentItem: T.TextField { contentItem: T.TextField {
padding: 5
text: root.editable ? root.editText : root.displayText
font: root.font
enabled: root.editable
autoScroll: root.editable autoScroll: root.editable
readOnly: root.down color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
enabled: root.editable
font: root.font
inputMethodHints: root.inputMethodHints inputMethodHints: root.inputMethodHints
padding: 5
placeholderTextColor: root.enabled ? root.colorScheme.text_hint : root.colorScheme.text_disabled
readOnly: root.down
selectedTextColor: root.colorScheme.text_invert
selectionColor: root.colorScheme.interaction_norm
text: root.editable ? root.editText : root.displayText
validator: root.validator validator: root.validator
verticalAlignment: TextInput.AlignVCenter verticalAlignment: TextInput.AlignVCenter
color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
selectionColor: root.colorScheme.interaction_norm
selectedTextColor: root.colorScheme.text_invert
placeholderTextColor: root.enabled ? root.colorScheme.text_hint : root.colorScheme.text_disabled
background: Rectangle { background: Rectangle {
radius: ProtonStyle.context_item_radius
visible: root.enabled && root.editable && !root.flat
border.color: { border.color: {
if (root.activeFocus) { if (root.activeFocus) {
return root.colorScheme.interaction_norm return root.colorScheme.interaction_norm;
} }
if (root.hovered || root.activeFocus) { if (root.hovered || root.activeFocus) {
return root.colorScheme.field_hover return root.colorScheme.field_hover;
} }
return root.colorScheme.field_norm;
return root.colorScheme.field_norm
} }
border.width: 1 border.width: 1
color: root.colorScheme.background_norm color: root.colorScheme.background_norm
radius: ProtonStyle.context_item_radius
visible: root.enabled && root.editable && !root.flat
} }
} }
background: Rectangle {
implicitWidth: 140
implicitHeight: 36
radius: ProtonStyle.context_item_radius
color: {
if (root.down) {
return root.colorScheme.interaction_default_active
}
if (root.enabled && root.hovered || root.activeFocus) {
return root.colorScheme.interaction_default_hover
}
if (!root.enabled) {
return root.colorScheme.interaction_default
}
return root.colorScheme.background_norm
}
border.color: root.colorScheme.border_norm
border.width: 1
}
indicator: ColorImage {
x: root.mirrored ? 12 : root.width - width - 12
y: root.topPadding + (root.availableHeight - height) / 2
color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
source: popup.visible ? "/qml/icons/ic-chevron-up.svg" : "/qml/icons/ic-chevron-down.svg"
sourceSize.width: 16
sourceSize.height: 16
}
delegate: ItemDelegate { delegate: ItemDelegate {
width: parent.width
text: root.textRole ? (Array.isArray(root.model) ? modelData[root.textRole] : model[root.textRole]) : modelData
palette.text: {
if (!root.enabled) {
return root.colorScheme.text_disabled
}
if (selected) {
return root.colorScheme.text_invert
}
return root.colorScheme.text_norm
}
font: root.font
hoverEnabled: root.hoverEnabled
property bool selected: root.currentIndex === index property bool selected: root.currentIndex === index
font: root.font
highlighted: root.highlightedIndex === index highlighted: root.highlightedIndex === index
hoverEnabled: root.hoverEnabled
palette.highlightedText: selected ? root.colorScheme.text_invert : root.colorScheme.text_norm palette.highlightedText: selected ? root.colorScheme.text_invert : root.colorScheme.text_norm
palette.text: {
if (!root.enabled) {
return root.colorScheme.text_disabled;
}
if (selected) {
return root.colorScheme.text_invert;
}
return root.colorScheme.text_norm;
}
text: root.textRole ? (Array.isArray(root.model) ? modelData[root.textRole] : model[root.textRole]) : modelData
width: parent.width
background: PaddedRectangle { background: PaddedRectangle {
radius: ProtonStyle.context_item_radius
color: { color: {
if (parent.down) { if (parent.down) {
return root.colorScheme.interaction_default_active return root.colorScheme.interaction_default_active;
} }
if (parent.selected) { if (parent.selected) {
return root.colorScheme.interaction_norm return root.colorScheme.interaction_norm;
} }
if (parent.hovered || parent.highlighted) { if (parent.hovered || parent.highlighted) {
return root.colorScheme.interaction_default_hover return root.colorScheme.interaction_default_hover;
} }
return root.colorScheme.interaction_default;
return root.colorScheme.interaction_default
} }
radius: ProtonStyle.context_item_radius
} }
} }
indicator: ColorImage {
color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
source: popup.visible ? "/qml/icons/ic-chevron-up.svg" : "/qml/icons/ic-chevron-down.svg"
sourceSize.height: 16
sourceSize.width: 16
x: root.mirrored ? 12 : root.width - width - 12
y: root.topPadding + (root.availableHeight - height) / 2
}
popup: T.Popup { popup: T.Popup {
y: root.height bottomMargin: 8
width: root.width
height: Math.min(contentItem.implicitHeight, root.Window.height - topMargin - bottomMargin) height: Math.min(contentItem.implicitHeight, root.Window.height - topMargin - bottomMargin)
topMargin: 8 topMargin: 8
bottomMargin: 8 width: root.width
y: root.height
background: Rectangle {
border.color: root.colorScheme.border_weak
border.width: 1
color: root.colorScheme.background_norm
radius: ProtonStyle.dialog_radius
}
contentItem: Item { contentItem: Item {
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
@ -175,21 +146,14 @@ T.ComboBox {
ListView { ListView {
anchors.fill: parent anchors.fill: parent
anchors.margins: 8 anchors.margins: 8
currentIndex: root.highlightedIndex
implicitHeight: contentHeight implicitHeight: contentHeight
model: root.delegateModel model: root.delegateModel
currentIndex: root.highlightedIndex
spacing: 4 spacing: 4
T.ScrollIndicator.vertical: ScrollIndicator { } T.ScrollIndicator.vertical: ScrollIndicator {
}
} }
} }
background: Rectangle {
color: root.colorScheme.background_norm
radius: ProtonStyle.dialog_radius
border.color: root.colorScheme.border_weak
border.width: 1
}
} }
} }

View File

@ -1,20 +1,15 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml import QtQml
import QtQuick import QtQuick
import QtQuick.Templates as T import QtQuick.Templates as T
@ -23,58 +18,46 @@ import QtQuick.Controls.impl
T.Dialog { T.Dialog {
id: root id: root
property ColorScheme colorScheme property ColorScheme colorScheme
Component.onCompleted: {
if (!ApplicationWindow.window) {
return
}
if (ApplicationWindow.window.popups === undefined) {
return
}
var obj = this
ApplicationWindow.window.popups.append( { obj } )
}
readonly property int popupType: ApplicationWindow.PopupType.Dialog
property bool shouldShow: false
readonly property var occurred: shouldShow ? new Date() : undefined readonly property var occurred: shouldShow ? new Date() : undefined
function open() { readonly property int popupType: ApplicationWindow.PopupType.Dialog
root.shouldShow = true property bool shouldShow: false
}
function close() { function close() {
root.shouldShow = false root.shouldShow = false;
}
function open() {
root.shouldShow = true;
} }
anchors.centerIn: Overlay.overlay anchors.centerIn: Overlay.overlay
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding + (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0) + (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0))
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding, implicitHeaderWidth, implicitFooterWidth)
contentWidth + leftPadding + rightPadding,
implicitHeaderWidth,
implicitFooterWidth)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
contentHeight + topPadding + bottomPadding
+ (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0)
+ (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0))
padding: 24 padding: 24
// TODO: Add DropShadow here
T.Overlay.modal: Rectangle {
color: root.colorScheme.backdrop_norm
}
T.Overlay.modeless: Rectangle {
color: "transparent"
}
background: Rectangle { background: Rectangle {
color: root.colorScheme.background_norm color: root.colorScheme.background_norm
radius: ProtonStyle.dialog_radius radius: ProtonStyle.dialog_radius
} }
// TODO: Add DropShadow here Component.onCompleted: {
if (!ApplicationWindow.window) {
T.Overlay.modal: Rectangle { return;
color: root.colorScheme.backdrop_norm }
} if (ApplicationWindow.window.popups === undefined) {
return;
T.Overlay.modeless: Rectangle { }
color: "transparent" const obj = this;
ApplicationWindow.window.popups.append({
"obj": obj
});
} }
} }

View File

@ -1,32 +1,23 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
import QtQuick.Templates as T import QtQuick.Templates as T
import "." as Proton import "." as Proton
T.Label { T.Label {
id: root id: root
property ColorScheme colorScheme
enum LabelType { enum LabelType {
// weight 700, size 28, height 36 // weight 700, size 28, height 36
Heading, Heading,
@ -47,96 +38,92 @@ T.Label {
// weight 700, size 12, height 16, spacing 0.4 // weight 700, size 12, height 16, spacing 0.4
Caption_bold Caption_bold
} }
property ColorScheme colorScheme
property int type: Proton.Label.LabelType.Body property int type: Proton.Label.LabelType.Body
function link(url, text) {
return `<a href="${url}">${text}</a>`;
}
color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
linkColor: root.colorScheme.interaction_norm
palette.link: linkColor
font.family: ProtonStyle.font_family font.family: ProtonStyle.font_family
lineHeightMode: Text.FixedHeight
font.weight: {
switch (root.type) {
case Proton.Label.LabelType.Heading:
return ProtonStyle.fontWeight_700
case Proton.Label.LabelType.Title:
return ProtonStyle.fontWeight_700
case Proton.Label.LabelType.Lead:
return ProtonStyle.fontWeight_400
case Proton.Label.LabelType.Body:
return ProtonStyle.fontWeight_400
case Proton.Label.LabelType.Body_semibold:
return ProtonStyle.fontWeight_600
case Proton.Label.LabelType.Body_bold:
return ProtonStyle.fontWeight_700
case Proton.Label.LabelType.Caption:
return ProtonStyle.fontWeight_400
case Proton.Label.LabelType.Caption_semibold:
return ProtonStyle.fontWeight_600
case Proton.Label.LabelType.Caption_bold:
return ProtonStyle.fontWeight_700
}
}
font.pixelSize: {
switch (root.type) {
case Proton.Label.LabelType.Heading:
return ProtonStyle.heading_font_size
case Proton.Label.LabelType.Title:
return ProtonStyle.title_font_size
case Proton.Label.LabelType.Lead:
return ProtonStyle.lead_font_size
case Proton.Label.LabelType.Body:
case Proton.Label.LabelType.Body_semibold:
case Proton.Label.LabelType.Body_bold:
return ProtonStyle.body_font_size
case Proton.Label.LabelType.Caption:
case Proton.Label.LabelType.Caption_semibold:
case Proton.Label.LabelType.Caption_bold:
return ProtonStyle.caption_font_size
}
}
lineHeight: {
switch (root.type) {
case Proton.Label.LabelType.Heading:
return ProtonStyle.heading_line_height
case Proton.Label.LabelType.Title:
return ProtonStyle.title_line_height
case Proton.Label.LabelType.Lead:
return ProtonStyle.lead_line_height
case Proton.Label.LabelType.Body:
case Proton.Label.LabelType.Body_semibold:
case Proton.Label.LabelType.Body_bold:
return ProtonStyle.body_line_height
case Proton.Label.LabelType.Caption:
case Proton.Label.LabelType.Caption_semibold:
case Proton.Label.LabelType.Caption_bold:
return ProtonStyle.caption_line_height
}
}
font.letterSpacing: { font.letterSpacing: {
switch (root.type) { switch (root.type) {
case Proton.Label.LabelType.Heading: case Proton.Label.LabelType.Heading:
case Proton.Label.LabelType.Title: case Proton.Label.LabelType.Title:
case Proton.Label.LabelType.Lead: case Proton.Label.LabelType.Lead:
return 0 return 0;
case Proton.Label.LabelType.Body: case Proton.Label.LabelType.Body:
case Proton.Label.LabelType.Body_semibold: case Proton.Label.LabelType.Body_semibold:
case Proton.Label.LabelType.Body_bold: case Proton.Label.LabelType.Body_bold:
return ProtonStyle.body_letter_spacing return ProtonStyle.body_letter_spacing;
case Proton.Label.LabelType.Caption: case Proton.Label.LabelType.Caption:
case Proton.Label.LabelType.Caption_semibold: case Proton.Label.LabelType.Caption_semibold:
case Proton.Label.LabelType.Caption_bold: case Proton.Label.LabelType.Caption_bold:
return ProtonStyle.caption_letter_spacing return ProtonStyle.caption_letter_spacing;
} }
} }
font.pixelSize: {
verticalAlignment: Text.AlignBottom switch (root.type) {
case Proton.Label.LabelType.Heading:
function link(url, text) { return ProtonStyle.heading_font_size;
return `<a href="${url}">${text}</a>` case Proton.Label.LabelType.Title:
return ProtonStyle.title_font_size;
case Proton.Label.LabelType.Lead:
return ProtonStyle.lead_font_size;
case Proton.Label.LabelType.Body:
case Proton.Label.LabelType.Body_semibold:
case Proton.Label.LabelType.Body_bold:
return ProtonStyle.body_font_size;
case Proton.Label.LabelType.Caption:
case Proton.Label.LabelType.Caption_semibold:
case Proton.Label.LabelType.Caption_bold:
return ProtonStyle.caption_font_size;
}
} }
font.weight: {
switch (root.type) {
case Proton.Label.LabelType.Heading:
return ProtonStyle.fontWeight_700;
case Proton.Label.LabelType.Title:
return ProtonStyle.fontWeight_700;
case Proton.Label.LabelType.Lead:
return ProtonStyle.fontWeight_400;
case Proton.Label.LabelType.Body:
return ProtonStyle.fontWeight_400;
case Proton.Label.LabelType.Body_semibold:
return ProtonStyle.fontWeight_600;
case Proton.Label.LabelType.Body_bold:
return ProtonStyle.fontWeight_700;
case Proton.Label.LabelType.Caption:
return ProtonStyle.fontWeight_400;
case Proton.Label.LabelType.Caption_semibold:
return ProtonStyle.fontWeight_600;
case Proton.Label.LabelType.Caption_bold:
return ProtonStyle.fontWeight_700;
}
}
lineHeight: {
switch (root.type) {
case Proton.Label.LabelType.Heading:
return ProtonStyle.heading_line_height;
case Proton.Label.LabelType.Title:
return ProtonStyle.title_line_height;
case Proton.Label.LabelType.Lead:
return ProtonStyle.lead_line_height;
case Proton.Label.LabelType.Body:
case Proton.Label.LabelType.Body_semibold:
case Proton.Label.LabelType.Body_bold:
return ProtonStyle.body_line_height;
case Proton.Label.LabelType.Caption:
case Proton.Label.LabelType.Caption_semibold:
case Proton.Label.LabelType.Caption_bold:
return ProtonStyle.caption_line_height;
}
}
lineHeightMode: Text.FixedHeight
linkColor: root.colorScheme.interaction_norm
palette.link: linkColor
verticalAlignment: Text.AlignBottom
} }

View File

@ -1,20 +1,15 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
@ -27,22 +22,19 @@ T.Menu {
property ColorScheme colorScheme property ColorScheme colorScheme
implicitWidth: Math.max( implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding)
implicitBackgroundWidth + leftInset + rightInset, implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding)
contentWidth + leftPadding + rightPadding
)
implicitHeight: Math.max(
implicitBackgroundHeight + topInset + bottomInset,
contentHeight + topPadding + bottomPadding
)
margins: 0 margins: 0
overlap: 1 overlap: 1
delegate: MenuItem { background: Rectangle {
colorScheme: control.colorScheme border.color: colorScheme.border_weak
border.width: 1
color: colorScheme.background_norm
implicitHeight: 40
implicitWidth: 200
radius: ProtonStyle.account_row_radius
} }
contentItem: Item { contentItem: Item {
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
@ -50,23 +42,17 @@ T.Menu {
ListView { ListView {
anchors.fill: parent anchors.fill: parent
anchors.margins: 8 anchors.margins: 8
implicitHeight: contentHeight
model: control.contentModel
interactive: Window.window ? contentHeight > Window.window.height : false
clip: true clip: true
currentIndex: control.currentIndex currentIndex: control.currentIndex
implicitHeight: contentHeight
interactive: Window.window ? contentHeight > Window.window.height : false
model: control.contentModel
ScrollIndicator.vertical: ScrollIndicator {} ScrollIndicator.vertical: ScrollIndicator {
}
} }
} }
delegate: MenuItem {
background: Rectangle { colorScheme: control.colorScheme
implicitWidth: 200
implicitHeight: 40
color: colorScheme.background_norm
border.width: 1
border.color: colorScheme.border_weak
radius: ProtonStyle.account_row_radius
} }
} }

View File

@ -1,20 +1,15 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
@ -26,46 +21,39 @@ T.MenuItem {
property ColorScheme colorScheme property ColorScheme colorScheme
width: parent.width // required. Other item overflows to the right of the menu and get clipped. font.family: ProtonStyle.font_family
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, font.letterSpacing: ProtonStyle.body_letter_spacing
implicitContentHeight + topPadding + bottomPadding, font.pixelSize: ProtonStyle.body_font_size
implicitIndicatorHeight + topPadding + bottomPadding) font.weight: ProtonStyle.fontWeight_400
icon.color: control.enabled ? control.colorScheme.text_norm : control.colorScheme.text_disabled
icon.height: 24
icon.width: 24
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding, implicitIndicatorHeight + topPadding + bottomPadding)
padding: 12 padding: 12
spacing: 6 spacing: 6
width: parent.width // required. Other item overflows to the right of the menu and get clipped.
icon.width: 24
icon.height: 24
icon.color: control.enabled ? control.colorScheme.text_norm : control.colorScheme.text_disabled
font.family: ProtonStyle.font_family
font.weight: ProtonStyle.fontWeight_400
font.pixelSize: ProtonStyle.body_font_size
font.letterSpacing: ProtonStyle.body_letter_spacing
contentItem: IconLabel {
id: iconLabel
readonly property real arrowPadding: control.subMenu && control.arrow ? control.arrow.width + control.spacing : 0
readonly property real indicatorPadding: control.checkable && control.indicator ? control.indicator.width + control.spacing : 0
leftPadding: !control.mirrored ? indicatorPadding : arrowPadding
rightPadding: control.mirrored ? indicatorPadding : arrowPadding
spacing: control.spacing
mirrored: control.mirrored
display: control.display
alignment: Qt.AlignLeft
icon: control.icon
text: control.text
font: control.font
color: control.enabled ? control.colorScheme.text_norm : control.colorScheme.text_disabled
}
background: Rectangle { background: Rectangle {
implicitWidth: 164
implicitHeight: 36
radius: ProtonStyle.button_radius
color: control.down ? control.colorScheme.interaction_default_active : control.highlighted ? control.colorScheme.interaction_default_hover : control.colorScheme.interaction_default color: control.down ? control.colorScheme.interaction_default_active : control.highlighted ? control.colorScheme.interaction_default_hover : control.colorScheme.interaction_default
implicitHeight: 36
implicitWidth: 164
radius: ProtonStyle.button_radius
}
contentItem: IconLabel {
id: iconLabel
readonly property real arrowPadding: control.subMenu && control.arrow ? control.arrow.width + control.spacing : 0
readonly property real indicatorPadding: control.checkable && control.indicator ? control.indicator.width + control.spacing : 0
alignment: Qt.AlignLeft
color: control.enabled ? control.colorScheme.text_norm : control.colorScheme.text_disabled
display: control.display
font: control.font
icon: control.icon
leftPadding: !control.mirrored ? indicatorPadding : arrowPadding
mirrored: control.mirrored
rightPadding: control.mirrored ? indicatorPadding : arrowPadding
spacing: control.spacing
text: control.text
} }
} }

View File

@ -1,20 +1,15 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml import QtQml
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
@ -23,45 +18,40 @@ import QtQuick.Templates as T
T.Popup { T.Popup {
id: root id: root
property ColorScheme colorScheme property ColorScheme colorScheme
Component.onCompleted: {
if (!ApplicationWindow.window) {
return
}
if (ApplicationWindow.window.popups === undefined) {
return
}
var obj = this
ApplicationWindow.window.popups.append( { obj } )
}
property int popupType: ApplicationWindow.PopupType.Banner
property bool shouldShow: false
readonly property var occurred: shouldShow ? new Date() : undefined readonly property var occurred: shouldShow ? new Date() : undefined
function open() { property int popupType: ApplicationWindow.PopupType.Banner
root.shouldShow = true property bool shouldShow: false
}
function close() { function close() {
root.shouldShow = false root.shouldShow = false;
}
function open() {
root.shouldShow = true;
} }
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding)
contentWidth + leftPadding + rightPadding) implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
contentHeight + topPadding + bottomPadding)
// TODO: Add DropShadow here // TODO: Add DropShadow here
T.Overlay.modal: Rectangle { T.Overlay.modal: Rectangle {
color: root.colorScheme.backdrop_norm color: root.colorScheme.backdrop_norm
} }
T.Overlay.modeless: Rectangle { T.Overlay.modeless: Rectangle {
color: "transparent" color: "transparent"
} }
Component.onCompleted: {
if (!ApplicationWindow.window) {
return;
}
if (ApplicationWindow.window.popups === undefined) {
return;
}
const obj = this;
ApplicationWindow.window.popups.append({
"obj": obj
});
}
} }

View File

@ -1,115 +1,91 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
import QtQuick.Templates as T import QtQuick.Templates as T
T.RadioButton { T.RadioButton {
property ColorScheme colorScheme
property bool error: false
id: control id: control
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, property ColorScheme colorScheme
implicitContentWidth + leftPadding + rightPadding) property bool error: false
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding,
implicitIndicatorHeight + topPadding + bottomPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding, implicitIndicatorHeight + topPadding + bottomPadding)
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding)
padding: 0 padding: 0
spacing: 8 spacing: 8
contentItem: CheckLabel {
color: {
if (!enabled) {
return control.colorScheme.text_disabled;
}
if (error) {
return control.colorScheme.signal_danger;
}
return control.colorScheme.text_norm;
}
font.family: ProtonStyle.font_family
font.letterSpacing: ProtonStyle.body_letter_spacing
font.pixelSize: ProtonStyle.body_font_size
font.weight: ProtonStyle.fontWeight_400
leftPadding: control.indicator && !control.mirrored ? control.indicator.width + control.spacing : 0
lineHeight: ProtonStyle.body_line_height
lineHeightMode: Text.FixedHeight
rightPadding: control.indicator && control.mirrored ? control.indicator.width + control.spacing : 0
text: control.text
}
indicator: Rectangle { indicator: Rectangle {
implicitWidth: 20 border.color: {
if (!control.enabled) {
return control.colorScheme.field_disabled;
}
if (control.error) {
return control.colorScheme.signal_danger;
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover;
}
return control.colorScheme.field_norm;
}
border.width: 1
color: control.colorScheme.background_norm
implicitHeight: 20 implicitHeight: 20
implicitWidth: 20
radius: width / 2 radius: width / 2
x: text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2 x: text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2
y: control.topPadding + (control.availableHeight - height) / 2 y: control.topPadding + (control.availableHeight - height) / 2
color: control.colorScheme.background_norm
border.width: 1
border.color: {
if (!control.enabled) {
return control.colorScheme.field_disabled
}
if (control.error) {
return control.colorScheme.signal_danger
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover
}
return control.colorScheme.field_norm
}
Rectangle { Rectangle {
x: (parent.width - width) / 2
y: (parent.height - height) / 2
width: 8
height: 8
radius: width / 2
color: { color: {
if (!control.enabled) { if (!control.enabled) {
return control.colorScheme.field_disabled return control.colorScheme.field_disabled;
} }
if (control.error) { if (control.error) {
return control.colorScheme.signal_danger return control.colorScheme.signal_danger;
} }
if (control.hovered || control.activeFocus) { if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover return control.colorScheme.interaction_norm_hover;
} }
return control.colorScheme.interaction_norm;
return control.colorScheme.interaction_norm
} }
height: 8
radius: width / 2
visible: control.checked visible: control.checked
width: 8
x: (parent.width - width) / 2
y: (parent.height - height) / 2
} }
} }
contentItem: CheckLabel {
leftPadding: control.indicator && !control.mirrored ? control.indicator.width + control.spacing : 0
rightPadding: control.indicator && control.mirrored ? control.indicator.width + control.spacing : 0
text: control.text
color: {
if (!enabled) {
return control.colorScheme.text_disabled
}
if (error) {
return control.colorScheme.signal_danger
}
return control.colorScheme.text_norm
}
font.family: ProtonStyle.font_family
font.weight: ProtonStyle.fontWeight_400
font.pixelSize: ProtonStyle.body_font_size
lineHeight: ProtonStyle.body_line_height
lineHeightMode: Text.FixedHeight
font.letterSpacing: ProtonStyle.body_letter_spacing
}
} }

View File

@ -1,387 +1,188 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
pragma Singleton pragma Singleton
import QtQml import QtQml
import QtQuick import QtQuick
import "."
import "./"
// https://wiki.qt.io/Qml_Styling // https://wiki.qt.io/Qml_Styling
// http://imaginativethinking.ca/make-qml-component-singleton/ // http://imaginativethinking.ca/make-qml-component-singleton/
QtObject { QtObject {
id: root id: root
// TODO: Once we will use Qt >=5.15 this should be refactored with inline components as follows:
// https://doc.qt.io/qt-5/qtqml-documents-definetypes.html#inline-components
// component ColorScheme: QtObject {
// property color primary_norm
// ...
// }
property ColorScheme lightStyle: ColorScheme {
id: _lightStyle
prominent: lightProminentStyle
// Primary
primary_norm: "#6D4AFF"
// Interaction-norm
interaction_norm: "#6D4AFF"
interaction_norm_hover: "#4D34B3"
interaction_norm_active: "#372580"
// Text
text_norm: "#0C0C14"
text_weak: "#706D6B"
text_hint: "#8F8D8A"
text_disabled: "#C2BFBC"
text_invert: "#FFFFFF"
// Field
field_norm: "#ADABA8"
field_hover: "#8F8D8A"
field_disabled: "#D1CFCD"
// Border
border_norm: "#D1CFCD"
border_weak: "#EAE7E4"
// Background
background_norm: "#FFFFFF"
background_weak: "#F5F4F2"
background_strong: "#EAE7E4"
background_avatar: "#C2BFBC"
// Interaction-weak
interaction_weak: "#D1CFCD"
interaction_weak_hover: "#C2BFBC"
interaction_weak_active: "#A8A6A3"
// Interaction-default
interaction_default: Qt.rgba(0,0,0,0)
interaction_default_hover: Qt.rgba(194./255., 191./255., 188./255., 0.2)
interaction_default_active: Qt.rgba(194./255., 191./255., 188./255., 0.4)
// Scrollbar
scrollbar_norm: "#D1CFCD"
scrollbar_hover: "#C2BFBC"
// Signal
signal_danger: "#DC3251"
signal_danger_hover: "#F74F6D"
signal_danger_active: "#B72346"
signal_warning: "#FF9900"
signal_warning_hover: "#FFB800"
signal_warning_active: "#FF851A"
signal_success: "#1EA885"
signal_success_hover: "#23C299"
signal_success_active: "#198F71"
signal_info: "#239ECE"
signal_info_hover: "#27B1E8"
signal_info_active: "#1F83B5"
// Shadows
shadow_norm: Qt.rgba(0,0,0, 0.1) // #000000 10% x:0 y:1 blur:4
shadow_lifted: Qt.rgba(0,0,0, 0.16) // #000000 16% x:0 y:8 blur:24
// Backdrop
backdrop_norm: Qt.rgba(12./255., 12./255., 20./255., 0.32)
// Images
welcome_img: "/qml/icons/img-welcome.png"
logo_img: "/qml/icons/product_logos.svg"
}
property ColorScheme lightProminentStyle: ColorScheme {
id: _lightProminentStyle
prominent: this
// Primary
primary_norm: "#8A6EFF"
// Interaction-norm
interaction_norm: "#6D4AFF"
interaction_norm_hover: "#7C5CFF"
interaction_norm_active: "#8A6EFF"
// Text
text_norm: "#FFFFFF"
text_weak: "#9282D4"
text_hint: "#544399"
text_disabled: "#4A398F"
text_invert: "#1B1340"
// Field
field_norm: "#9282D4"
field_hover: "#7C5CFF"
field_disabled: "#38277A"
// Border
border_norm: "#413085"
border_weak: "#3C2B80"
// Background
background_norm: "#1B1340"
background_weak: "#271C57"
background_strong: "#38277A"
background_avatar: "#6D4AFF"
// Interaction-weak
interaction_weak: "#4A398F"
interaction_weak_hover: "#6D4AFF"
interaction_weak_active: "#8A6EFF"
// Interaction-default
interaction_default: Qt.rgba(0,0,0,0)
interaction_default_hover: Qt.rgba(68./255., 78./255., 114./255., 0.2)
interaction_default_active: Qt.rgba(68./255., 78./255., 114./255., 0.3)
// Scrollbar
scrollbar_norm: "#413085"
scrollbar_hover: "#4A398F"
// Signal
signal_danger: "#F5385A"
signal_danger_hover: "#FF5473"
signal_danger_active: "#DC3251"
signal_warning: "#FF9900"
signal_warning_hover: "#FFB800"
signal_warning_active: "#FF8419"
signal_success: "#1EA885"
signal_success_hover: "#23C299"
signal_success_active: "#198F71"
signal_info: "#2C89DB"
signal_info_hover: "#3491E3"
signal_info_active: "#1F83B5"
// Shadows
shadow_norm: Qt.rgba(0,0,0, 0.32) // #000000 32% x:0 y:1 blur:4
shadow_lifted: Qt.rgba(0,0,0, 0.40) // #000000 40% x:0 y:8 blur:24
// Backdrop
backdrop_norm: Qt.rgba(0,0,0, 0.32)
// Images
welcome_img: "/qml/icons/img-welcome-dark.png"
logo_img: "/qml/icons/product_logos_dark.svg"
}
property ColorScheme darkStyle: ColorScheme {
id: _darkStyle
prominent: darkProminentStyle
// Primary
primary_norm: "#8A6EFF"
// Interaction-norm
interaction_norm: "#6D4AFF"
interaction_norm_hover: "#7C5CFF"
interaction_norm_active: "#8A6EFF"
// Text
text_norm: "#FFFFFF"
text_weak: "#A7A4B5"
text_hint: "#6D697D"
text_disabled: "#5B576B"
text_invert: "#1C1B24"
// Field
field_norm: "#5B576B"
field_hover: "#6D697D"
field_disabled: "#3F3B4C"
// Border
border_norm: "#4A4658"
border_weak: "#343140"
// Background
background_norm: "#1C1B24"
background_weak: "#292733"
background_strong: "#3F3B4C"
background_avatar: "#6D4AFF"
// Interaction-weak
interaction_weak: "#4A4658"
interaction_weak_hover: "#5B576B"
interaction_weak_active: "#6D697D"
// Interaction-default
interaction_default: "#00000000"
interaction_default_hover: Qt.rgba(91./255.,87./255.,107./255.,0.2)
interaction_default_active: Qt.rgba(91./255.,87./255.,107./255.,0.4)
// Scrollbar
scrollbar_norm: "#4A4658"
scrollbar_hover: "#5B576B"
// Signal
signal_danger: "#F5385A"
signal_danger_hover: "#FF5473"
signal_danger_active: "#DC3251"
signal_warning: "#FF9900"
signal_warning_hover: "#FFB800"
signal_warning_active: "#FF8419"
signal_success: "#1EA885"
signal_success_hover: "#23C299"
signal_success_active: "#198F71"
signal_info: "#239ECE"
signal_info_hover: "#27B1E8"
signal_info_active: "#1F83B5"
// Shadows
shadow_norm: Qt.rgba(0,0,0,0.4) // #000000 40% x+0 y+1 blur:4
shadow_lifted: Qt.rgba(0,0,0,0.48) // #000000 48% x+0 y+8 blur:24
// Backdrop
backdrop_norm: Qt.rgba(0,0,0,0.32)
// Images
welcome_img: "/qml/icons/img-welcome-dark.png"
logo_img: "/qml/icons/product_logos_dark.svg"
}
property real account_hover_radius: 12 * root.px // px
property real account_row_radius: 12 * root.px // px
property real avatar_radius: 8 * root.px // px
property real banner_radius: 12 * root.px // px
property real big_avatar_radius: 12 * root.px // px
property int body_font_size: 14
property real body_letter_spacing: 0.2 * root.px
property int body_line_height: 20
property real button_radius: 8 * root.px // px
property int caption_font_size: 12
property real caption_letter_spacing: 0.4 * root.px
property int caption_line_height: 16
property real card_radius: 12 * root.px // px
property real checkbox_radius: 4 * root.px // px
property real context_item_radius: 8 * root.px // px
property ColorScheme currentStyle: lightStyle
property ColorScheme darkProminentStyle: ColorScheme { property ColorScheme darkProminentStyle: ColorScheme {
id: _darkProminentStyle id: _darkProminentStyle
prominent: this // Backdrop
backdrop_norm: Qt.rgba(0, 0, 0, 0.32)
background_avatar: "#6D4AFF"
// Primary // Background
primary_norm: "#8A6EFF" background_norm: "#16141c"
background_strong: "#3F3B4C"
// Interaction-norm background_weak: "#292733"
interaction_norm: "#6D4AFF"
interaction_norm_hover: "#7C5CFF"
interaction_norm_active: "#8A6EFF"
// Text
text_norm: "#FFFFFF"
text_weak: "#A7A4B5"
text_hint: "#6D697D"
text_disabled: "#5B576B"
text_invert: "#1C1B24"
// Field
field_norm: "#5B576B"
field_hover: "#6D697D"
field_disabled: "#3F3B4C"
// Border // Border
border_norm: "#4A4658" border_norm: "#4A4658"
border_weak: "#343140" border_weak: "#343140"
field_disabled: "#3F3B4C"
field_hover: "#6D697D"
// Background // Field
background_norm: "#16141c" field_norm: "#5B576B"
background_weak: "#292733"
background_strong: "#3F3B4C"
background_avatar: "#6D4AFF"
// Interaction-weak
interaction_weak: "#4A4658"
interaction_weak_hover: "#5B576B"
interaction_weak_active: "#6D697D"
// Interaction-default // Interaction-default
interaction_default: "#00000000" interaction_default: "#00000000"
interaction_default_hover: Qt.rgba(91./255.,87./255.,107./255.,0.2) interaction_default_active: Qt.rgba(91. / 255., 87. / 255., 107. / 255., 0.4)
interaction_default_active: Qt.rgba(91./255.,87./255.,107./255.,0.4) interaction_default_hover: Qt.rgba(91. / 255., 87. / 255., 107. / 255., 0.2)
// Interaction-norm
interaction_norm: "#6D4AFF"
interaction_norm_active: "#8A6EFF"
interaction_norm_hover: "#7C5CFF"
// Interaction-weak
interaction_weak: "#4A4658"
interaction_weak_active: "#6D697D"
interaction_weak_hover: "#5B576B"
logo_img: "/qml/icons/product_logos_dark.svg"
// Primary
primary_norm: "#8A6EFF"
prominent: this
scrollbar_hover: "#5B576B"
// Scrollbar // Scrollbar
scrollbar_norm: "#4A4658" scrollbar_norm: "#4A4658"
scrollbar_hover: "#5B576B" shadow_lifted: Qt.rgba(0, 0, 0, 0.48) // #000000 48% x+0 y+8 blur:24
// Shadows
shadow_norm: Qt.rgba(0, 0, 0, 0.4) // #000000 40% x+0 y+1 blur:4
// Signal // Signal
signal_danger: "#F5385A" signal_danger: "#F5385A"
signal_danger_hover: "#FF5473"
signal_danger_active: "#DC3251" signal_danger_active: "#DC3251"
signal_warning: "#FF9900" signal_danger_hover: "#FF5473"
signal_warning_hover: "#FFB800"
signal_warning_active: "#FF8419"
signal_success: "#1EA885"
signal_success_hover: "#23C299"
signal_success_active: "#198F71"
signal_info: "#239ECE" signal_info: "#239ECE"
signal_info_hover: "#27B1E8"
signal_info_active: "#1F83B5" signal_info_active: "#1F83B5"
signal_info_hover: "#27B1E8"
signal_success: "#1EA885"
signal_success_active: "#198F71"
signal_success_hover: "#23C299"
signal_warning: "#FF9900"
signal_warning_active: "#FF8419"
signal_warning_hover: "#FFB800"
text_disabled: "#5B576B"
text_hint: "#6D697D"
text_invert: "#1C1B24"
// Shadows // Text
shadow_norm: Qt.rgba(0,0,0,0.4) // #000000 40% x+0 y+1 blur:4 text_norm: "#FFFFFF"
shadow_lifted: Qt.rgba(0,0,0,0.48) // #000000 48% x+0 y+8 blur:24 text_weak: "#A7A4B5"
// Backdrop
backdrop_norm: Qt.rgba(0,0,0,0.32)
// Images // Images
welcome_img: "/qml/icons/img-welcome-dark.png" welcome_img: "/qml/icons/img-welcome-dark.png"
logo_img: "/qml/icons/product_logos_dark.svg"
} }
property ColorScheme darkStyle: ColorScheme {
id: _darkStyle
property ColorScheme currentStyle: lightStyle // Backdrop
backdrop_norm: Qt.rgba(0, 0, 0, 0.32)
background_avatar: "#6D4AFF"
property string font_family: { // Background
switch (Qt.platform.os) { background_norm: "#1C1B24"
case "windows": background_strong: "#3F3B4C"
return "Segoe UI" background_weak: "#292733"
case "osx":
return ".AppleSystemUIFont" // should be SF Pro for the foreseeable future. Using "SF Pro Display" directly here is not allowed by the font's license. // Border
case "linux": border_norm: "#4A4658"
return "Ubuntu" border_weak: "#343140"
default: field_disabled: "#3F3B4C"
console.error("Unknown platform") field_hover: "#6D697D"
}
// Field
field_norm: "#5B576B"
// Interaction-default
interaction_default: "#00000000"
interaction_default_active: Qt.rgba(91. / 255., 87. / 255., 107. / 255., 0.4)
interaction_default_hover: Qt.rgba(91. / 255., 87. / 255., 107. / 255., 0.2)
// Interaction-norm
interaction_norm: "#6D4AFF"
interaction_norm_active: "#8A6EFF"
interaction_norm_hover: "#7C5CFF"
// Interaction-weak
interaction_weak: "#4A4658"
interaction_weak_active: "#6D697D"
interaction_weak_hover: "#5B576B"
logo_img: "/qml/icons/product_logos_dark.svg"
// Primary
primary_norm: "#8A6EFF"
prominent: darkProminentStyle
scrollbar_hover: "#5B576B"
// Scrollbar
scrollbar_norm: "#4A4658"
shadow_lifted: Qt.rgba(0, 0, 0, 0.48) // #000000 48% x+0 y+8 blur:24
// Shadows
shadow_norm: Qt.rgba(0, 0, 0, 0.4) // #000000 40% x+0 y+1 blur:4
// Signal
signal_danger: "#F5385A"
signal_danger_active: "#DC3251"
signal_danger_hover: "#FF5473"
signal_info: "#239ECE"
signal_info_active: "#1F83B5"
signal_info_hover: "#27B1E8"
signal_success: "#1EA885"
signal_success_active: "#198F71"
signal_success_hover: "#23C299"
signal_warning: "#FF9900"
signal_warning_active: "#FF8419"
signal_warning_hover: "#FFB800"
text_disabled: "#5B576B"
text_hint: "#6D697D"
text_invert: "#1C1B24"
// Text
text_norm: "#FFFFFF"
text_weak: "#A7A4B5"
// Images
welcome_img: "/qml/icons/img-welcome-dark.png"
} }
property real dialog_radius: 12 * root.px // px
property real px : 1.00 // px
property real input_radius : 8 * root.px // px
property real button_radius : 8 * root.px // px
property real checkbox_radius : 4 * root.px // px
property real avatar_radius : 8 * root.px // px
property real big_avatar_radius : 12 * root.px // px
property real account_hover_radius : 12 * root.px // px
property real account_row_radius : 12 * root.px // px
property real context_item_radius : 8 * root.px // px
property real banner_radius : 12 * root.px // px
property real dialog_radius : 12 * root.px // px
property real card_radius : 12 * root.px // px
property real progress_bar_radius : 3 * root.px // px
property real tooltip_radius : 8 * root.px // px
property int heading_font_size: 28
property int heading_line_height: 36
property int title_font_size: 20
property int title_line_height: 24
property int lead_font_size: 18
property int lead_line_height: 26
property int body_font_size: 14
property int body_line_height: 20
property real body_letter_spacing: 0.2 * root.px
property int caption_font_size: 12
property int caption_line_height: 16
property real caption_letter_spacing: 0.4 * root.px
property int fontWeight_100: Font.Thin property int fontWeight_100: Font.Thin
property int fontWeight_200: Font.Light property int fontWeight_200: Font.Light
property int fontWeight_300: Font.ExtraLight property int fontWeight_300: Font.ExtraLight
@ -391,4 +192,179 @@ QtObject {
property int fontWeight_700: Font.Bold property int fontWeight_700: Font.Bold
property int fontWeight_800: Font.ExtraBold property int fontWeight_800: Font.ExtraBold
property int fontWeight_900: Font.Black property int fontWeight_900: Font.Black
property string font_family: {
switch (Qt.platform.os) {
case "windows":
return "Segoe UI";
case "osx":
return ".AppleSystemUIFont"; // should be SF Pro for the foreseeable future. Using "SF Pro Display" directly here is not allowed by the font's license.
case "linux":
return "Ubuntu";
default:
console.error("Unknown platform");
}
}
property int heading_font_size: 28
property int heading_line_height: 36
property real input_radius: 8 * root.px // px
property int lead_font_size: 18
property int lead_line_height: 26
property ColorScheme lightProminentStyle: ColorScheme {
id: _lightProminentStyle
// Backdrop
backdrop_norm: Qt.rgba(0, 0, 0, 0.32)
background_avatar: "#6D4AFF"
// Background
background_norm: "#1B1340"
background_strong: "#38277A"
background_weak: "#271C57"
// Border
border_norm: "#413085"
border_weak: "#3C2B80"
field_disabled: "#38277A"
field_hover: "#7C5CFF"
// Field
field_norm: "#9282D4"
// Interaction-default
interaction_default: Qt.rgba(0, 0, 0, 0)
interaction_default_active: Qt.rgba(68. / 255., 78. / 255., 114. / 255., 0.3)
interaction_default_hover: Qt.rgba(68. / 255., 78. / 255., 114. / 255., 0.2)
// Interaction-norm
interaction_norm: "#6D4AFF"
interaction_norm_active: "#8A6EFF"
interaction_norm_hover: "#7C5CFF"
// Interaction-weak
interaction_weak: "#4A398F"
interaction_weak_active: "#8A6EFF"
interaction_weak_hover: "#6D4AFF"
logo_img: "/qml/icons/product_logos_dark.svg"
// Primary
primary_norm: "#8A6EFF"
prominent: this
scrollbar_hover: "#4A398F"
// Scrollbar
scrollbar_norm: "#413085"
shadow_lifted: Qt.rgba(0, 0, 0, 0.40) // #000000 40% x:0 y:8 blur:24
// Shadows
shadow_norm: Qt.rgba(0, 0, 0, 0.32) // #000000 32% x:0 y:1 blur:4
// Signal
signal_danger: "#F5385A"
signal_danger_active: "#DC3251"
signal_danger_hover: "#FF5473"
signal_info: "#2C89DB"
signal_info_active: "#1F83B5"
signal_info_hover: "#3491E3"
signal_success: "#1EA885"
signal_success_active: "#198F71"
signal_success_hover: "#23C299"
signal_warning: "#FF9900"
signal_warning_active: "#FF8419"
signal_warning_hover: "#FFB800"
text_disabled: "#4A398F"
text_hint: "#544399"
text_invert: "#1B1340"
// Text
text_norm: "#FFFFFF"
text_weak: "#9282D4"
// Images
welcome_img: "/qml/icons/img-welcome-dark.png"
}
// TODO: Once we will use Qt >=5.15 this should be refactored with inline components as follows:
// https://doc.qt.io/qt-5/qtqml-documents-definetypes.html#inline-components
// component ColorScheme: QtObject {
// property color primary_norm
// ...
// }
property ColorScheme lightStyle: ColorScheme {
id: _lightStyle
// Backdrop
backdrop_norm: Qt.rgba(12. / 255., 12. / 255., 20. / 255., 0.32)
background_avatar: "#C2BFBC"
// Background
background_norm: "#FFFFFF"
background_strong: "#EAE7E4"
background_weak: "#F5F4F2"
// Border
border_norm: "#D1CFCD"
border_weak: "#EAE7E4"
field_disabled: "#D1CFCD"
field_hover: "#8F8D8A"
// Field
field_norm: "#ADABA8"
// Interaction-default
interaction_default: Qt.rgba(0, 0, 0, 0)
interaction_default_active: Qt.rgba(194. / 255., 191. / 255., 188. / 255., 0.4)
interaction_default_hover: Qt.rgba(194. / 255., 191. / 255., 188. / 255., 0.2)
// Interaction-norm
interaction_norm: "#6D4AFF"
interaction_norm_active: "#372580"
interaction_norm_hover: "#4D34B3"
// Interaction-weak
interaction_weak: "#D1CFCD"
interaction_weak_active: "#A8A6A3"
interaction_weak_hover: "#C2BFBC"
logo_img: "/qml/icons/product_logos.svg"
// Primary
primary_norm: "#6D4AFF"
prominent: lightProminentStyle
scrollbar_hover: "#C2BFBC"
// Scrollbar
scrollbar_norm: "#D1CFCD"
shadow_lifted: Qt.rgba(0, 0, 0, 0.16) // #000000 16% x:0 y:8 blur:24
// Shadows
shadow_norm: Qt.rgba(0, 0, 0, 0.1) // #000000 10% x:0 y:1 blur:4
// Signal
signal_danger: "#DC3251"
signal_danger_active: "#B72346"
signal_danger_hover: "#F74F6D"
signal_info: "#239ECE"
signal_info_active: "#1F83B5"
signal_info_hover: "#27B1E8"
signal_success: "#1EA885"
signal_success_active: "#198F71"
signal_success_hover: "#23C299"
signal_warning: "#FF9900"
signal_warning_active: "#FF851A"
signal_warning_hover: "#FFB800"
text_disabled: "#C2BFBC"
text_hint: "#8F8D8A"
text_invert: "#FFFFFF"
// Text
text_norm: "#0C0C14"
text_weak: "#706D6B"
// Images
welcome_img: "/qml/icons/img-welcome.png"
}
property real progress_bar_radius: 3 * root.px // px
property real px: 1.00 // px
property int title_font_size: 20
property int title_line_height: 24
property real tooltip_radius: 8 * root.px // px
} }

View File

@ -1,150 +1,124 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Templates as T import QtQuick.Templates as T
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
T.Switch { T.Switch {
property ColorScheme colorScheme id: control
property ColorScheme colorScheme
property bool loading: false property bool loading: false
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding, implicitIndicatorHeight + topPadding + bottomPadding)
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding)
padding: 0
spacing: 7
contentItem: CheckLabel {
id: label
color: control.enabled || control.loading ? control.colorScheme.text_norm : control.colorScheme.text_disabled
font.family: ProtonStyle.font_family
font.letterSpacing: ProtonStyle.body_letter_spacing
font.pixelSize: ProtonStyle.body_font_size
font.weight: ProtonStyle.fontWeight_400
leftPadding: control.indicator && !control.mirrored ? control.indicator.width + control.spacing : 0
lineHeight: ProtonStyle.body_line_height
lineHeightMode: Text.FixedHeight
rightPadding: control.indicator && control.mirrored ? control.indicator.width + control.spacing : 0
text: control.text
}
indicator: Rectangle {
border.color: control.hovered ? control.colorScheme.field_hover : control.colorScheme.field_norm
border.width: control.enabled && !loading ? 1 : 0
color: control.enabled || control.loading ? control.colorScheme.background_norm : control.colorScheme.background_strong
implicitHeight: 24
implicitWidth: 40
radius: height / 2.
x: text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2
y: control.topPadding + (control.availableHeight - height) / 2
Rectangle {
color: {
if (!control.enabled) {
return control.colorScheme.field_disabled;
}
if (control.checked) {
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover;
}
return control.colorScheme.interaction_norm;
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.field_hover;
}
return control.colorScheme.field_norm;
}
height: 24
radius: parent.radius
visible: !loading
width: 24
x: Math.max(0, Math.min(parent.width - width, control.visualPosition * parent.width - (width / 2)))
y: (parent.height - height) / 2
Behavior on x {
enabled: !control.down
SmoothedAnimation {
velocity: 200
}
}
ColorImage {
color: "#FFFFFF"
height: 16
source: "/qml/icons/ic-check.svg"
sourceSize.height: 16
sourceSize.width: 16
visible: control.checked
width: 16
x: (parent.width - width) / 2
y: (parent.height - height) / 2
}
}
ColorImage {
id: loadingImage
color: control.colorScheme.interaction_norm_hover
height: 18
source: "/qml/icons/Loader_16.svg"
sourceSize.height: 18
sourceSize.width: 18
visible: control.loading
width: 18
x: parent.width - width
y: (parent.height - height) / 2
RotationAnimation {
direction: RotationAnimation.Clockwise
duration: 1000
from: 0
loops: Animation.Infinite
running: control.loading
target: loadingImage
to: 360
}
}
}
// TODO: store previous enabled state and restore it? // TODO: store previous enabled state and restore it?
// For now assuming that only enabled buttons could have loading state // For now assuming that only enabled buttons could have loading state
onLoadingChanged: { onLoadingChanged: {
if (loading) { enabled = !loading;
enabled = false
} else {
enabled = true
}
}
id: control
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
implicitContentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding,
implicitIndicatorHeight + topPadding + bottomPadding)
padding: 0
spacing: 7
indicator: Rectangle {
implicitWidth: 40
implicitHeight: 24
x: text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2
y: control.topPadding + (control.availableHeight - height) / 2
radius: height / 2.
color: control.enabled || control.loading ? control.colorScheme.background_norm : control.colorScheme.background_strong
border.width: control.enabled && !loading ? 1 : 0
border.color: control.hovered ? control.colorScheme.field_hover : control.colorScheme.field_norm
Rectangle {
x: Math.max(0, Math.min(parent.width - width, control.visualPosition * parent.width - (width / 2)))
y: (parent.height - height) / 2
width: 24
height: 24
radius: parent.radius
visible: !loading
color: {
if (!control.enabled) {
return control.colorScheme.field_disabled
}
if (control.checked) {
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover
}
return control.colorScheme.interaction_norm
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.field_hover
}
return control.colorScheme.field_norm
}
ColorImage {
x: (parent.width - width) / 2
y: (parent.height - height) / 2
width: 16
height: 16
sourceSize.width: 16
sourceSize.height: 16
color: "#FFFFFF"
source: "/qml/icons/ic-check.svg"
visible: control.checked
}
Behavior on x {
enabled: !control.down
SmoothedAnimation { velocity: 200 }
}
}
ColorImage {
id: loadingImage
x: parent.width - width
y: (parent.height - height) / 2
width: 18
height: 18
sourceSize.width: 18
sourceSize.height: 18
color: control.colorScheme.interaction_norm_hover
source: "/qml/icons/Loader_16.svg"
visible: control.loading
RotationAnimation {
target: loadingImage
loops: Animation.Infinite
duration: 1000
from: 0
to: 360
direction: RotationAnimation.Clockwise
running: control.loading
}
}
}
contentItem: CheckLabel {
id: label
leftPadding: control.indicator && !control.mirrored ? control.indicator.width + control.spacing : 0
rightPadding: control.indicator && control.mirrored ? control.indicator.width + control.spacing : 0
text: control.text
color: control.enabled || control.loading ? control.colorScheme.text_norm : control.colorScheme.text_disabled
font.family: ProtonStyle.font_family
font.weight: ProtonStyle.fontWeight_400
font.pixelSize: ProtonStyle.body_font_size
lineHeight: ProtonStyle.body_line_height
lineHeightMode: Text.FixedHeight
font.letterSpacing: ProtonStyle.body_letter_spacing
} }
} }

View File

@ -1,54 +1,37 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml import QtQml
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
import QtQuick.Templates as T import QtQuick.Templates as T
import QtQuick.Layouts import QtQuick.Layouts
import "." as Proton import "." as Proton
FocusScope { FocusScope {
id: root id: root
property ColorScheme colorScheme
property alias background: control.background
property alias bottomInset: control.bottomInset
//property alias flickable: control.flickable
property alias focusReason: control.focusReason
property alias hoverEnabled: control.hoverEnabled
property alias hovered: control.hovered
property alias implicitBackgroundHeight: control.implicitBackgroundHeight
property alias implicitBackgroundWidth: control.implicitBackgroundWidth
property alias leftInset: control.leftInset
property alias palette: control.palette
property alias placeholderText: control.placeholderText
property alias placeholderTextColor: control.placeholderTextColor
property alias rightInset: control.rightInset
property alias topInset: control.topInset
property alias activeFocusOnPress: control.activeFocusOnPress property alias activeFocusOnPress: control.activeFocusOnPress
property string assistiveText
property alias background: control.background
property alias baseUrl: control.baseUrl property alias baseUrl: control.baseUrl
property alias bottomInset: control.bottomInset
property alias bottomPadding: control.bottomPadding property alias bottomPadding: control.bottomPadding
property alias canPaste: control.canPaste property alias canPaste: control.canPaste
property alias canRedo: control.canRedo property alias canRedo: control.canRedo
property alias canUndo: control.canUndo property alias canUndo: control.canUndo
property alias color: control.color property alias color: control.color
property ColorScheme colorScheme
property alias contentHeight: control.contentHeight property alias contentHeight: control.contentHeight
property alias contentWidth: control.contentWidth property alias contentWidth: control.contentWidth
property alias cursorDelegate: control.cursorDelegate property alias cursorDelegate: control.cursorDelegate
@ -56,21 +39,36 @@ FocusScope {
property alias cursorRectangle: control.cursorRectangle property alias cursorRectangle: control.cursorRectangle
property alias cursorVisible: control.cursorVisible property alias cursorVisible: control.cursorVisible
property alias effectiveHorizontalAlignment: control.effectiveHorizontalAlignment property alias effectiveHorizontalAlignment: control.effectiveHorizontalAlignment
property bool error: false
property string errorString
//property alias flickable: control.flickable
property alias focusReason: control.focusReason
property alias font: control.font property alias font: control.font
property alias hint: hint.text
property alias horizontalAlignment: control.horizontalAlignment property alias horizontalAlignment: control.horizontalAlignment
property alias hoverEnabled: control.hoverEnabled
property alias hovered: control.hovered
property alias hoveredLink: control.hoveredLink property alias hoveredLink: control.hoveredLink
property alias implicitBackgroundHeight: control.implicitBackgroundHeight
property alias implicitBackgroundWidth: control.implicitBackgroundWidth
property alias inputMethodComposing: control.inputMethodComposing property alias inputMethodComposing: control.inputMethodComposing
property alias inputMethodHints: control.inputMethodHints property alias inputMethodHints: control.inputMethodHints
property alias label: label.text
property alias leftInset: control.leftInset
property alias leftPadding: control.leftPadding property alias leftPadding: control.leftPadding
property alias length: control.length property alias length: control.length
property alias lineCount: control.lineCount property alias lineCount: control.lineCount
property alias mouseSelectionMode: control.mouseSelectionMode property alias mouseSelectionMode: control.mouseSelectionMode
property alias overwriteMode: control.overwriteMode property alias overwriteMode: control.overwriteMode
property alias padding: control.padding property alias padding: control.padding
property alias palette: control.palette
property alias persistentSelection: control.persistentSelection property alias persistentSelection: control.persistentSelection
property alias placeholderText: control.placeholderText
property alias placeholderTextColor: control.placeholderTextColor
property alias preeditText: control.preeditText property alias preeditText: control.preeditText
property alias readOnly: control.readOnly property alias readOnly: control.readOnly
property alias renderType: control.renderType property alias renderType: control.renderType
property alias rightInset: control.rightInset
property alias rightPadding: control.rightPadding property alias rightPadding: control.rightPadding
property alias selectByKeyboard: control.selectByKeyboard property alias selectByKeyboard: control.selectByKeyboard
property alias selectByMouse: control.selectByMouse property alias selectByMouse: control.selectByMouse
@ -84,61 +82,119 @@ FocusScope {
property alias textDocument: control.textDocument property alias textDocument: control.textDocument
property alias textFormat: control.textFormat property alias textFormat: control.textFormat
property alias textMargin: control.textMargin property alias textMargin: control.textMargin
property alias topInset: control.topInset
property alias topPadding: control.topPadding property alias topPadding: control.topPadding
property bool validateOnEditingFinished: true
// We are using our own type of validators. It should be a function // We are using our own type of validators. It should be a function
// returning an error string in case of error and undefined if no error // returning an error string in case of error and undefined if no error
property var validator property var validator
property alias verticalAlignment: control.verticalAlignment property alias verticalAlignment: control.verticalAlignment
property alias wrapMode: control.wrapMode property alias wrapMode: control.wrapMode
implicitWidth: children[0].implicitWidth signal editingFinished
implicitHeight: children[0].implicitHeight
property alias label: label.text function append(text) {
property alias hint: hint.text return control.append(text);
property string assistiveText }
property string errorString function clear() {
return control.clear();
property bool error: false }
function copy() {
signal editingFinished() return control.copy();
}
function append(text) { return control.append(text) } function cut() {
function clear() { return control.clear() } return control.cut();
function copy() { return control.copy() } }
function cut() { return control.cut() } function deselect() {
function deselect() { return control.deselect() } return control.deselect();
function getFormattedText(start, end) { return control.getFormattedText(start, end) } }
function getText(start, end) { return control.getText(start, end) } function getFormattedText(start, end) {
function insert(position, text) { return control.insert(position, text) } return control.getFormattedText(start, end);
function isRightToLeft(start, end) { return control.isRightToLeft(start, end) } }
function linkAt(x, y) { return control.linkAt(x, y) } function getText(start, end) {
function moveCursorSelection(position, mode) { return control.moveCursorSelection(position, mode) } return control.getText(start, end);
function paste() { return control.paste() } }
function positionAt(x, y) { return control.positionAt(x, y) }
function positionToRectangle(position) { return control.positionToRectangle(position) }
function redo() { return control.redo() }
function remove(start, end) { return control.remove(start, end) }
function select(start, end) { return control.select(start, end) }
function selectAll() { return control.selectAll() }
function selectWord() { return control.selectWord() }
function undo() { return control.undo() }
// Calculates the height of the component to make exactly lineNum visible in edit area // Calculates the height of the component to make exactly lineNum visible in edit area
function heightForLinesVisible(lineNum) { function heightForLinesVisible(lineNum) {
var totalHeight = 0 let totalHeight = 0;
totalHeight += headerLayout.height totalHeight += headerLayout.height;
totalHeight += footerLayout.height totalHeight += footerLayout.height;
totalHeight += control.topPadding + control.bottomPadding totalHeight += control.topPadding + control.bottomPadding;
totalHeight += lineNum * fontMetrics.height totalHeight += lineNum * fontMetrics.height;
return totalHeight return totalHeight;
}
function insert(position, text) {
return control.insert(position, text);
}
function isRightToLeft(start, end) {
return control.isRightToLeft(start, end);
}
function linkAt(x, y) {
return control.linkAt(x, y);
}
function moveCursorSelection(position, mode) {
return control.moveCursorSelection(position, mode);
}
function paste() {
return control.paste();
}
function positionAt(x, y) {
return control.positionAt(x, y);
}
function positionToRectangle(position) {
return control.positionToRectangle(position);
}
function redo() {
return control.redo();
}
function remove(start, end) {
return control.remove(start, end);
}
function select(start, end) {
return control.select(start, end);
}
function selectAll() {
return control.selectAll();
}
function selectWord() {
return control.selectWord();
}
function undo() {
return control.undo();
}
function validate() {
if (validator === undefined) {
return;
}
const error = validator(text);
if (error) {
root.error = true;
root.errorString = error;
} else {
root.error = false;
root.errorString = "";
}
}
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
onEditingFinished: {
if (!validateOnEditingFinished) {
return;
}
validate();
}
onTextChanged: {
root.error = false;
root.errorString = "";
} }
FontMetrics { FontMetrics {
id: fontMetrics id: fontMetrics
font: control.font font: control.font
} }
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: 0
@ -149,154 +205,131 @@ FocusScope {
spacing: 0 spacing: 0
Proton.Label { Proton.Label {
colorScheme: root.colorScheme
id: label id: label
Layout.fillWidth: true Layout.fillWidth: true
color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
colorScheme: root.colorScheme
type: Proton.Label.LabelType.Body_semibold type: Proton.Label.LabelType.Body_semibold
} }
Proton.Label { Proton.Label {
colorScheme: root.colorScheme
id: hint id: hint
Layout.fillWidth: true Layout.fillWidth: true
color: root.enabled ? root.colorScheme.text_weak : root.colorScheme.text_disabled color: root.enabled ? root.colorScheme.text_weak : root.colorScheme.text_disabled
colorScheme: root.colorScheme
horizontalAlignment: Text.AlignRight horizontalAlignment: Text.AlignRight
type: Proton.Label.LabelType.Caption type: Proton.Label.LabelType.Caption
} }
} }
ScrollView { ScrollView {
id: controlView id: controlView
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
clip: true clip: true
T.TextArea { T.TextArea {
id: control id: control
KeyNavigation.backtab: root.KeyNavigation.backtab
implicitWidth: Math.max( KeyNavigation.down: root.KeyNavigation.down
contentWidth + leftPadding + rightPadding, KeyNavigation.left: root.KeyNavigation.left
implicitBackgroundWidth + leftInset + rightInset, KeyNavigation.priority: root.KeyNavigation.priority
placeholder.implicitWidth + leftPadding + rightPadding KeyNavigation.right: root.KeyNavigation.right
) KeyNavigation.tab: root.KeyNavigation.tab
implicitHeight: Math.max( KeyNavigation.up: root.KeyNavigation.up
contentHeight + topPadding + bottomPadding,
implicitBackgroundHeight + topInset + bottomInset,
placeholder.implicitHeight + topPadding + bottomPadding
)
topPadding: 8
bottomPadding: 8 bottomPadding: 8
leftPadding: 12 color: {
rightPadding: 12 if (!control.enabled) {
return root.colorScheme.text_disabled
font.family: ProtonStyle.font_family }
font.weight: ProtonStyle.fontWeight_400 if (control.readOnly) {
font.pixelSize: ProtonStyle.body_font_size return root.colorScheme.text_hint
font.letterSpacing: ProtonStyle.body_letter_spacing }
return root.colorScheme.text_norm
color: control.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled }
placeholderTextColor: control.enabled ? root.colorScheme.text_hint : root.colorScheme.text_disabled
selectionColor: control.palette.highlight
selectedTextColor: control.palette.highlightedText
onEditingFinished: root.editingFinished()
wrapMode: TextInput.Wrap
// enforcing default focus here within component // enforcing default focus here within component
focus: root.focus focus: root.focus
font.family: ProtonStyle.font_family
KeyNavigation.priority: root.KeyNavigation.priority font.letterSpacing: ProtonStyle.body_letter_spacing
KeyNavigation.backtab: root.KeyNavigation.backtab font.pixelSize: ProtonStyle.body_font_size
KeyNavigation.tab: root.KeyNavigation.tab font.weight: ProtonStyle.fontWeight_400
KeyNavigation.up: root.KeyNavigation.up implicitHeight: Math.max(contentHeight + topPadding + bottomPadding, implicitBackgroundHeight + topInset + bottomInset, placeholder.implicitHeight + topPadding + bottomPadding)
KeyNavigation.down: root.KeyNavigation.down implicitWidth: Math.max(contentWidth + leftPadding + rightPadding, implicitBackgroundWidth + leftInset + rightInset, placeholder.implicitWidth + leftPadding + rightPadding)
KeyNavigation.left: root.KeyNavigation.left leftPadding: 12
KeyNavigation.right: root.KeyNavigation.right placeholderTextColor: control.enabled ? root.colorScheme.text_hint : root.colorScheme.text_disabled
rightPadding: 12
selectByMouse: true selectByMouse: true
selectedTextColor: control.palette.highlightedText
cursorDelegate: Rectangle { selectionColor: control.palette.highlight
id: cursor topPadding: 8
width: 1 wrapMode: TextInput.Wrap
color: root.colorScheme.interaction_norm
visible: control.activeFocus && !control.readOnly && control.selectionStart === control.selectionEnd
Connections {
target: control
function onCursorPositionChanged() {
// keep a moving cursor visible
cursor.opacity = 1
timer.restart()
}
}
Timer {
id: timer
running: control.activeFocus && !control.readOnly
repeat: true
interval: Qt.styleHints.cursorFlashTime / 2
onTriggered: cursor.opacity = !cursor.opacity ? 1 : 0
// force the cursor visible when gaining focus
onRunningChanged: cursor.opacity = 1
}
}
PlaceholderText {
id: placeholder
x: control.leftPadding
y: control.topPadding
width: control.width - (control.leftPadding + control.rightPadding)
height: control.height - (control.topPadding + control.bottomPadding)
text: control.placeholderText
font: control.font
color: control.placeholderTextColor
verticalAlignment: control.verticalAlignment
visible: !control.length && !control.preeditText && (!control.activeFocus || control.horizontalAlignment !== Qt.AlignHCenter)
elide: Text.ElideRight
renderType: control.renderType
}
background: Rectangle { background: Rectangle {
anchors.fill: parent anchors.fill: parent
radius: ProtonStyle.input_radius
visible: true
color: root.colorScheme.background_norm
border.color: { border.color: {
if (!control.enabled) { if (!control.enabled || control.readOnly) {
return root.colorScheme.field_disabled return root.colorScheme.field_disabled;
} }
if (control.activeFocus) { if (control.activeFocus) {
return root.colorScheme.interaction_norm return root.colorScheme.interaction_norm;
} }
if (root.error) { if (root.error) {
return root.colorScheme.signal_danger return root.colorScheme.signal_danger;
} }
if (control.hovered) { if (control.hovered) {
return root.colorScheme.field_hover return root.colorScheme.field_hover;
} }
return root.colorScheme.field_norm;
return root.colorScheme.field_norm
} }
border.width: 1 border.width: 1
color: root.colorScheme.background_norm
radius: ProtonStyle.input_radius
visible: true
}
cursorDelegate: Rectangle {
id: cursor
color: root.colorScheme.interaction_norm
visible: control.activeFocus && !control.readOnly && control.selectionStart === control.selectionEnd
width: 1
Connections {
function onCursorPositionChanged() {
// keep a moving cursor visible
cursor.opacity = 1;
timer.restart();
}
target: control
}
Timer {
id: timer
interval: Qt.styleHints.cursorFlashTime / 2
repeat: true
running: control.activeFocus && !control.readOnly
// force the cursor visible when gaining focus
onRunningChanged: cursor.opacity = 1
onTriggered: cursor.opacity = !cursor.opacity ? 1 : 0
}
}
onEditingFinished: root.editingFinished()
PlaceholderText {
id: placeholder
color: control.placeholderTextColor
elide: Text.ElideRight
font: control.font
height: control.height - (control.topPadding + control.bottomPadding)
renderType: control.renderType
text: control.placeholderText
verticalAlignment: control.verticalAlignment
visible: !control.length && !control.preeditText && (!control.activeFocus || control.horizontalAlignment !== Qt.AlignHCenter)
width: control.width - (control.leftPadding + control.rightPadding)
x: control.leftPadding
y: control.topPadding
} }
} }
} }
RowLayout { RowLayout {
id: footerLayout id: footerLayout
Layout.fillWidth: true Layout.fillWidth: true
@ -304,67 +337,29 @@ FocusScope {
ColorImage { ColorImage {
id: errorIcon id: errorIcon
Layout.rightMargin: 4 Layout.rightMargin: 4
visible: root.error && (assistiveText.text.length > 0)
source: "/qml/icons/ic-exclamation-circle-filled.svg"
color: root.colorScheme.signal_danger color: root.colorScheme.signal_danger
height: assistiveText.height height: assistiveText.height
source: "/qml/icons/ic-exclamation-circle-filled.svg"
sourceSize.height: assistiveText.height sourceSize.height: assistiveText.height
visible: root.error && (assistiveText.text.length > 0)
} }
Proton.Label { Proton.Label {
colorScheme: root.colorScheme
id: assistiveText id: assistiveText
Layout.fillWidth: true Layout.fillWidth: true
text: root.error ? root.errorString : root.assistiveText
color: { color: {
if (!root.enabled) { if (!root.enabled) {
return root.colorScheme.text_disabled return root.colorScheme.text_disabled;
} }
if (root.error) { if (root.error) {
return root.colorScheme.signal_danger return root.colorScheme.signal_danger;
} }
return root.colorScheme.text_weak;
return root.colorScheme.text_weak
} }
colorScheme: root.colorScheme
text: root.error ? root.errorString : root.assistiveText
type: root.error ? Proton.Label.LabelType.Caption_semibold : Proton.Label.LabelType.Caption type: root.error ? Proton.Label.LabelType.Caption_semibold : Proton.Label.LabelType.Caption
} }
} }
} }
property bool validateOnEditingFinished: true
onEditingFinished: {
if (!validateOnEditingFinished) {
return
}
validate()
}
function validate() {
if (validator === undefined) {
return
}
var error = validator(text)
if (error) {
root.error = true
root.errorString = error
} else {
root.error = false
root.errorString = ""
}
}
onTextChanged: {
root.error = false
root.errorString = ""
}
} }

View File

@ -1,54 +1,38 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml import QtQml
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
import QtQuick.Templates as T import QtQuick.Templates as T
import QtQuick.Layouts import QtQuick.Layouts
import "." as Proton import "." as Proton
FocusScope { FocusScope {
id: root id: root
property ColorScheme colorScheme
property alias background: control.background
property alias bottomInset: control.bottomInset
property alias focusReason: control.focusReason
property alias hoverEnabled: control.hoverEnabled
property alias hovered: control.hovered
property alias implicitBackgroundHeight: control.implicitBackgroundHeight
property alias implicitBackgroundWidth: control.implicitBackgroundWidth
property alias leftInset: control.leftInset
property alias palette: control.palette
property alias placeholderText: control.placeholderText
property alias placeholderTextColor: control.placeholderTextColor
property alias rightInset: control.rightInset
property alias topInset: control.topInset
property alias acceptableInput: control.acceptableInput property alias acceptableInput: control.acceptableInput
property alias activeFocusOnPress: control.activeFocusOnPress property alias activeFocusOnPress: control.activeFocusOnPress
property string assistiveText
property alias autoScroll: control.autoScroll property alias autoScroll: control.autoScroll
property alias background: control.background
property alias bottomInset: control.bottomInset
property alias bottomPadding: control.bottomPadding property alias bottomPadding: control.bottomPadding
property alias canPaste: control.canPaste property alias canPaste: control.canPaste
property alias canRedo: control.canRedo property alias canRedo: control.canRedo
property alias canUndo: control.canUndo property alias canUndo: control.canUndo
property alias color: control.color property alias color: control.color
property ColorScheme colorScheme
//property alias contentHeight: control.contentHeight //property alias contentHeight: control.contentHeight
//property alias contentWidth: control.contentWidth //property alias contentWidth: control.contentWidth
property alias cursorDelegate: control.cursorDelegate property alias cursorDelegate: control.cursorDelegate
@ -56,24 +40,39 @@ FocusScope {
property alias cursorRectangle: control.cursorRectangle property alias cursorRectangle: control.cursorRectangle
property alias cursorVisible: control.cursorVisible property alias cursorVisible: control.cursorVisible
property alias displayText: control.displayText property alias displayText: control.displayText
property int echoMode: TextInput.Normal
property alias effectiveHorizontalAlignment: control.effectiveHorizontalAlignment property alias effectiveHorizontalAlignment: control.effectiveHorizontalAlignment
property bool error: false
property string errorString
property alias focusReason: control.focusReason
property alias font: control.font property alias font: control.font
property alias hint: hint.text
property alias horizontalAlignment: control.horizontalAlignment property alias horizontalAlignment: control.horizontalAlignment
property alias hoverEnabled: control.hoverEnabled
property alias hovered: control.hovered
property alias implicitBackgroundHeight: control.implicitBackgroundHeight
property alias implicitBackgroundWidth: control.implicitBackgroundWidth
property alias inputMask: control.inputMask property alias inputMask: control.inputMask
property alias inputMethodComposing: control.inputMethodComposing property alias inputMethodComposing: control.inputMethodComposing
property alias inputMethodHints: control.inputMethodHints property alias inputMethodHints: control.inputMethodHints
property alias label: label.text
property alias leftInset: control.leftInset
property alias leftPadding: control.leftPadding property alias leftPadding: control.leftPadding
property alias length: control.length property alias length: control.length
property alias maximumLength: control.maximumLength property alias maximumLength: control.maximumLength
property alias mouseSelectionMode: control.mouseSelectionMode property alias mouseSelectionMode: control.mouseSelectionMode
property alias overwriteMode: control.overwriteMode property alias overwriteMode: control.overwriteMode
property alias padding: control.padding property alias padding: control.padding
property alias palette: control.palette
property alias passwordCharacter: control.passwordCharacter property alias passwordCharacter: control.passwordCharacter
property alias passwordMaskDelay: control.passwordMaskDelay property alias passwordMaskDelay: control.passwordMaskDelay
property alias persistentSelection: control.persistentSelection property alias persistentSelection: control.persistentSelection
property alias placeholderText: control.placeholderText
property alias placeholderTextColor: control.placeholderTextColor
property alias preeditText: control.preeditText property alias preeditText: control.preeditText
property alias readOnly: control.readOnly property alias readOnly: control.readOnly
property alias renderType: control.renderType property alias renderType: control.renderType
property alias rightInset: control.rightInset
property alias rightPadding: control.rightPadding property alias rightPadding: control.rightPadding
property alias selectByMouse: control.selectByMouse property alias selectByMouse: control.selectByMouse
property alias selectedText: control.selectedText property alias selectedText: control.selectedText
@ -82,47 +81,102 @@ FocusScope {
property alias selectionEnd: control.selectionEnd property alias selectionEnd: control.selectionEnd
property alias selectionStart: control.selectionStart property alias selectionStart: control.selectionStart
property alias text: control.text property alias text: control.text
property alias topInset: control.topInset
property bool validateOnEditingFinished: true
// We are using our own type of validators. It should be a function // We are using our own type of validators. It should be a function
// returning an error string in case of error and undefined if no error // returning an error string in case of error and undefined if no error
property var validator property var validator
property alias verticalAlignment: control.verticalAlignment property alias verticalAlignment: control.verticalAlignment
property alias wrapMode: control.wrapMode property alias wrapMode: control.wrapMode
implicitWidth: children[0].implicitWidth signal accepted
signal editingFinished
signal textEdited
function clear() {
control.clear();
}
function copy() {
control.copy();
}
function cut() {
control.cut();
}
function deselect() {
control.deselect();
}
function ensureVisible(position) {
control.ensureVisible(position);
}
function forceActiveFocus() {
control.forceActiveFocus();
}
function getText(start, end) {
control.getText(start, end);
}
function insert(position, text) {
control.insert(position, text);
}
function isRightToLeft(start, end) {
control.isRightToLeft(start, end);
}
function moveCursorSelection(position, mode) {
control.moveCursorSelection(position, mode);
}
function paste() {
control.paste();
}
function positionAt(x, y, position) {
control.positionAt(x, y, position);
}
function positionToRectangle(pos) {
control.positionToRectangle(pos);
}
function redo() {
control.redo();
}
function remove(start, end) {
control.remove(start, end);
}
function select(start, end) {
control.select(start, end);
}
function selectAll() {
control.selectAll();
}
function selectWord() {
control.selectWord();
}
function undo() {
control.undo();
}
function validate() {
if (validator === undefined) {
return;
}
const error = validator(text);
if (error) {
root.error = true;
root.errorString = error;
} else {
root.error = false;
root.errorString = "";
}
}
implicitHeight: children[0].implicitHeight implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
property alias label: label.text onEditingFinished: {
property alias hint: hint.text if (!validateOnEditingFinished) {
property string assistiveText return;
property string errorString }
validate();
property int echoMode: TextInput.Normal }
onTextChanged: {
property bool error: false root.error = false;
root.errorString = "";
signal accepted() }
signal editingFinished()
signal textEdited()
function clear() { control.clear() }
function copy() { control.copy() }
function cut() { control.cut() }
function deselect() { control.deselect() }
function ensureVisible(position) { control.ensureVisible(position) }
function getText(start, end) { control.getText(start, end) }
function insert(position, text) { control.insert(position, text) }
function isRightToLeft(start, end) { control.isRightToLeft(start, end) }
function moveCursorSelection(position, mode) { control.moveCursorSelection(position, mode) }
function paste() { control.paste() }
function positionAt(x, y, position) { control.positionAt(x, y, position) }
function positionToRectangle(pos) { control.positionToRectangle(pos) }
function redo() { control.redo() }
function remove(start, end) { control.remove(start, end) }
function select(start, end) { control.select(start, end) }
function selectAll() { control.selectAll() }
function selectWord() { control.selectWord() }
function undo() { control.undo() }
function forceActiveFocus() { control.forceActiveFocus() }
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
@ -133,19 +187,18 @@ FocusScope {
spacing: 0 spacing: 0
Proton.Label { Proton.Label {
colorScheme: root.colorScheme
id: label id: label
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
colorScheme: root.colorScheme
type: Proton.Label.LabelType.Body_semibold type: Proton.Label.LabelType.Body_semibold
} }
Proton.Label { Proton.Label {
colorScheme: root.colorScheme
id: hint id: hint
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
color: root.enabled ? root.colorScheme.text_weak : root.colorScheme.text_disabled color: root.enabled ? root.colorScheme.text_weak : root.colorScheme.text_disabled
colorScheme: root.colorScheme
horizontalAlignment: Text.AlignRight horizontalAlignment: Text.AlignRight
type: Proton.Label.LabelType.Caption type: Proton.Label.LabelType.Caption
} }
@ -156,36 +209,29 @@ FocusScope {
// will be adjusted to background's width making text field and eye button overlap // will be adjusted to background's width making text field and eye button overlap
Rectangle { Rectangle {
id: background id: background
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
radius: ProtonStyle.input_radius
visible: true
color: root.colorScheme.background_norm
border.color: { border.color: {
if (!control.enabled) { if (!control.enabled) {
return root.colorScheme.field_disabled return root.colorScheme.field_disabled;
} }
if (control.activeFocus) { if (control.activeFocus) {
return root.colorScheme.interaction_norm return root.colorScheme.interaction_norm;
} }
if (root.error) { if (root.error) {
return root.colorScheme.signal_danger return root.colorScheme.signal_danger;
} }
if (control.hovered) { if (control.hovered) {
return root.colorScheme.field_hover return root.colorScheme.field_hover;
} }
return root.colorScheme.field_norm;
return root.colorScheme.field_norm
} }
border.width: 1 border.width: 1
color: root.colorScheme.background_norm
implicitWidth: children[0].implicitWidth
implicitHeight: children[0].implicitHeight implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
radius: ProtonStyle.input_radius
visible: true
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
@ -193,190 +239,135 @@ FocusScope {
T.TextField { T.TextField {
id: control id: control
KeyNavigation.backtab: root.KeyNavigation.backtab
KeyNavigation.down: root.KeyNavigation.down
KeyNavigation.left: root.KeyNavigation.left
KeyNavigation.priority: root.KeyNavigation.priority
KeyNavigation.right: root.KeyNavigation.right
KeyNavigation.tab: root.KeyNavigation.tab
KeyNavigation.up: root.KeyNavigation.up
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
implicitWidth: implicitBackgroundWidth + leftInset + rightInset
|| Math.max(contentWidth, placeholder.implicitWidth) + leftPadding + rightPadding
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
contentHeight + topPadding + bottomPadding,
placeholder.implicitHeight + topPadding + bottomPadding)
topPadding: 8
bottomPadding: 8 bottomPadding: 8
leftPadding: 12
rightPadding: 12
font.family: ProtonStyle.font_family
font.weight: ProtonStyle.fontWeight_400
font.pixelSize: ProtonStyle.body_font_size
font.letterSpacing: ProtonStyle.body_letter_spacing
color: control.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled color: control.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
placeholderTextColor: control.enabled ? root.colorScheme.text_hint : root.colorScheme.text_disabled
selectionColor: control.palette.highlight
selectedTextColor: control.palette.highlightedText
verticalAlignment: TextInput.AlignVCenter
echoMode: eyeButton.checked ? TextInput.Normal : root.echoMode echoMode: eyeButton.checked ? TextInput.Normal : root.echoMode
// enforcing default focus here within component // enforcing default focus here within component
focus: true focus: true
font.family: ProtonStyle.font_family
KeyNavigation.priority: root.KeyNavigation.priority font.letterSpacing: ProtonStyle.body_letter_spacing
KeyNavigation.backtab: root.KeyNavigation.backtab font.pixelSize: ProtonStyle.body_font_size
KeyNavigation.tab: root.KeyNavigation.tab font.weight: ProtonStyle.fontWeight_400
KeyNavigation.up: root.KeyNavigation.up implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding, placeholder.implicitHeight + topPadding + bottomPadding)
KeyNavigation.down: root.KeyNavigation.down implicitWidth: implicitBackgroundWidth + leftInset + rightInset || Math.max(contentWidth, placeholder.implicitWidth) + leftPadding + rightPadding
KeyNavigation.left: root.KeyNavigation.left leftPadding: 12
KeyNavigation.right: root.KeyNavigation.right placeholderTextColor: control.enabled ? root.colorScheme.text_hint : root.colorScheme.text_disabled
rightPadding: 12
selectByMouse: true selectByMouse: true
selectedTextColor: control.palette.highlightedText
selectionColor: control.palette.highlight
topPadding: 8
verticalAlignment: TextInput.AlignVCenter
background: Item {
implicitHeight: 36
implicitWidth: 80
visible: false
}
cursorDelegate: Rectangle { cursorDelegate: Rectangle {
id: cursor id: cursor
width: 1
color: root.colorScheme.interaction_norm color: root.colorScheme.interaction_norm
visible: control.activeFocus && !control.readOnly && control.selectionStart === control.selectionEnd visible: control.activeFocus && !control.readOnly && control.selectionStart === control.selectionEnd
width: 1
Connections { Connections {
target: control
function onCursorPositionChanged() { function onCursorPositionChanged() {
// keep a moving cursor visible // keep a moving cursor visible
cursor.opacity = 1 cursor.opacity = 1;
timer.restart() timer.restart();
} }
}
target: control
}
Timer { Timer {
id: timer id: timer
running: control.activeFocus && !control.readOnly
repeat: true
interval: Qt.styleHints.cursorFlashTime / 2 interval: Qt.styleHints.cursorFlashTime / 2
onTriggered: cursor.opacity = !cursor.opacity ? 1 : 0 repeat: true
running: control.activeFocus && !control.readOnly
// force the cursor visible when gaining focus // force the cursor visible when gaining focus
onRunningChanged: cursor.opacity = 1 onRunningChanged: cursor.opacity = 1
onTriggered: cursor.opacity = !cursor.opacity ? 1 : 0
} }
} }
onAccepted: {
root.accepted();
}
onEditingFinished: {
root.editingFinished();
}
onTextEdited: {
root.textEdited();
}
PlaceholderText { PlaceholderText {
id: placeholder id: placeholder
x: control.leftPadding
y: control.topPadding
width: control.width - (control.leftPadding + control.rightPadding)
height: control.height - (control.topPadding + control.bottomPadding)
text: control.placeholderText
font: control.font
color: control.placeholderTextColor color: control.placeholderTextColor
elide: Text.ElideRight
font: control.font
height: control.height - (control.topPadding + control.bottomPadding)
renderType: control.renderType
text: control.placeholderText
verticalAlignment: control.verticalAlignment verticalAlignment: control.verticalAlignment
visible: !control.length && !control.preeditText && (!control.activeFocus || control.horizontalAlignment !== Qt.AlignHCenter) visible: !control.length && !control.preeditText && (!control.activeFocus || control.horizontalAlignment !== Qt.AlignHCenter)
elide: Text.ElideRight width: control.width - (control.leftPadding + control.rightPadding)
renderType: control.renderType x: control.leftPadding
} y: control.topPadding
background: Item {
implicitWidth: 80
implicitHeight: 36
visible: false
}
onAccepted: {
root.accepted()
}
onEditingFinished: {
root.editingFinished()
}
onTextEdited: {
root.textEdited()
} }
} }
Proton.Button { Proton.Button {
colorScheme: root.colorScheme
id: eyeButton id: eyeButton
Layout.fillHeight: true Layout.fillHeight: true
visible: root.echoMode === TextInput.Password
icon.color: control.color
checkable: true checkable: true
colorScheme: root.colorScheme
icon.color: control.color
icon.source: checked ? "../icons/ic-eye-slash.svg" : "../icons/ic-eye.svg" icon.source: checked ? "../icons/ic-eye-slash.svg" : "../icons/ic-eye.svg"
visible: root.echoMode === TextInput.Password
} }
} }
} }
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 0 spacing: 0
ColorImage { ColorImage {
id: errorIcon id: errorIcon
Layout.rightMargin: 4 Layout.rightMargin: 4
visible: root.error && (assistiveText.text.length > 0)
source: "../icons/ic-exclamation-circle-filled.svg"
color: root.colorScheme.signal_danger color: root.colorScheme.signal_danger
height: assistiveText.lineHeight height: assistiveText.lineHeight
source: "../icons/ic-exclamation-circle-filled.svg"
sourceSize.height: assistiveText.lineHeight sourceSize.height: assistiveText.lineHeight
visible: root.error && (assistiveText.text.length > 0)
} }
Proton.Label { Proton.Label {
colorScheme: root.colorScheme
id: assistiveText id: assistiveText
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
wrapMode: Text.WordWrap
text: root.error ? root.errorString : root.assistiveText
color: { color: {
if (!root.enabled) { if (!root.enabled) {
return root.colorScheme.text_disabled return root.colorScheme.text_disabled;
} }
if (root.error) { if (root.error) {
return root.colorScheme.signal_danger return root.colorScheme.signal_danger;
} }
return root.colorScheme.text_weak;
return root.colorScheme.text_weak
} }
colorScheme: root.colorScheme
text: root.error ? root.errorString : root.assistiveText
type: root.error ? Proton.Label.LabelType.Caption_semibold : Proton.Label.LabelType.Caption type: root.error ? Proton.Label.LabelType.Caption_semibold : Proton.Label.LabelType.Caption
wrapMode: Text.WordWrap
} }
} }
} }
property bool validateOnEditingFinished: true
onEditingFinished: {
if (!validateOnEditingFinished) {
return
}
validate()
}
function validate() {
if (validator === undefined) {
return
}
var error = validator(text)
if (error) {
root.error = true
root.errorString = error
} else {
root.error = false
root.errorString = ""
}
}
onTextChanged: {
root.error = false
root.errorString = ""
}
} }

View File

@ -1,20 +1,15 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
@ -22,92 +17,106 @@ import QtQuick.Controls.impl
Item { Item {
id: root id: root
property var colorScheme
property bool _disabled: !enabled
property bool checked property bool checked
property var colorScheme
property bool hovered property bool hovered
property bool loading property bool loading
signal clicked signal clicked
property bool _disabled: !enabled
implicitHeight: children[0].implicitHeight implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth implicitWidth: children[0].implicitWidth
Rectangle { Rectangle {
id: indicator id: indicator
implicitWidth: 40
implicitHeight: 24
radius: width/2
color: { color: {
if (root.loading) return "transparent" if (root.loading)
if (root._disabled) return root.colorScheme.background_strong return "transparent";
return root.colorScheme.background_norm if (root._disabled)
} return root.colorScheme.background_strong;
border { return root.colorScheme.background_norm;
width: 1
color: (root._disabled || root.loading) ? "transparent" : colorScheme.field_norm
} }
implicitHeight: 24
implicitWidth: 40
radius: width / 2
border {
color: (root._disabled || root.loading) ? "transparent" : colorScheme.field_norm
width: 1
}
Rectangle { Rectangle {
anchors.verticalCenter: indicator.verticalCenter
anchors.left: indicator.left anchors.left: indicator.left
anchors.leftMargin: root.checked ? 16 : 0 anchors.leftMargin: root.checked ? 16 : 0
width: 24 anchors.verticalCenter: indicator.verticalCenter
height: 24
radius: width/2
color: { color: {
if (root.loading) return "transparent" if (root.loading)
if (root._disabled) return root.colorScheme.field_disabled return "transparent";
if (root._disabled)
return root.colorScheme.field_disabled;
if (root.checked) { if (root.checked) {
if (root.hovered) return root.colorScheme.interaction_norm_hover if (root.hovered)
return root.colorScheme.interaction_norm return root.colorScheme.interaction_norm_hover;
return root.colorScheme.interaction_norm;
} else { } else {
if (root.hovered) return root.colorScheme.field_hover if (root.hovered)
return root.colorScheme.field_norm return root.colorScheme.field_hover;
return root.colorScheme.field_norm;
} }
} }
height: 24
radius: width / 2
width: 24
ColorImage { ColorImage {
anchors.centerIn: parent anchors.centerIn: parent
source: "/qml/icons/ic-check.svg"
color: root.colorScheme.background_norm color: root.colorScheme.background_norm
height: root.colorScheme.body_font_size height: root.colorScheme.body_font_size
source: "/qml/icons/ic-check.svg"
sourceSize.height: root.colorScheme.body_font_size sourceSize.height: root.colorScheme.body_font_size
visible: root.checked visible: root.checked
} }
} }
ColorImage { ColorImage {
id: loader id: loader
anchors.centerIn: parent anchors.centerIn: parent
source: "/qml/icons/Loader_16.svg"
color: root.colorScheme.text_norm color: root.colorScheme.text_norm
height: root.colorScheme.body_font_size height: root.colorScheme.body_font_size
source: "/qml/icons/Loader_16.svg"
sourceSize.height: root.colorScheme.body_font_size sourceSize.height: root.colorScheme.body_font_size
visible: root.loading visible: root.loading
RotationAnimation { RotationAnimation {
target: loader direction: RotationAnimation.Clockwise
loops: Animation.Infinite
duration: 1000 duration: 1000
from: 0 from: 0
to: 360 loops: Animation.Infinite
direction: RotationAnimation.Clockwise
running: root.loading running: root.loading
target: loader
to: 360
} }
} }
MouseArea { MouseArea {
anchors.fill: indicator anchors.fill: indicator
hoverEnabled: true hoverEnabled: true
onEntered: {root.hovered = true }
onExited: {root.hovered = false } onClicked: {
onClicked: { if (root.enabled) root.clicked();} if (root.enabled)
onPressed: {root.hovered = true } root.clicked();
onReleased: { root.hovered = containsMouse } }
onEntered: {
root.hovered = true;
}
onExited: {
root.hovered = false;
}
onPressed: {
root.hovered = true;
}
onReleased: {
root.hovered = containsMouse;
}
} }
} }
} }

View File

@ -0,0 +1,254 @@
// Copyright (c) 2023 Proton AG
// This file is part of Proton Mail Bridge.
// Proton Mail Bridge 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 3 of the License, or
// (at your option) any later version.
// Proton Mail Bridge 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.
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
Item {
id: root
enum InputType {
TextInput = 1,
Radio,
Checkbox
}
property string _typeOpen: "open"
property string _typeChoice: "choice"
property string _typeMutlichoice: "multichoice"
property var colorScheme
property var _bottomMargin: 20
property var _lineHeight: 1
property bool showSeparator: true
property string text: ""
property string tips: ""
property string label: ""
property bool mandatory: false
property var type: root._typeOpen
property var answerList: ListModel{}
property int maxChar: 150
property string answer:{
if (type === root._typeOpen) {
return textInput.text
} else if (type === root._typeChoice) {
return selectionRadio.text
} else if (type === root._typeMutlichoice) {
return selectionCheckBox.text
}
return ""
}
property bool error: {
if (root.type === root._typeOpen)
return textInput.error;
if (root.type === root._typeChoice)
return selectionRadio.error;
if (root.type === root._typeMutlichoice)
return selectionCheckBox.error;
return false
}
function setDefaultValue(defaultValue) {
textInput.setDefaultValue(defaultValue)
selectionRadio.setDefaultValue(defaultValue)
selectionCheckBox.setDefaultValue(defaultValue)
}
function validate() {
if (root.type === root._typeOpen)
textInput.validate()
else if (root.type === root._typeChoice)
selectionRadio.validate()
else if (root.type === root._typeMutlichoice)
selectionCheckBox.validate()
}
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
ColumnLayout {
anchors.fill: parent
spacing: 16
Label {
id: mainLabel
colorScheme: root.colorScheme
text: root.mandatory ? qsTr(root.text+" *") : qsTr(root.text)
type: Label.Body
}
ColumnLayout {
spacing: 16
Layout.bottomMargin: root._bottomMargin
TextArea {
id: textInput
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: root.type === root._typeOpen ? heightForLinesVisible(2) : 0
colorScheme: root.colorScheme
property int _maxLength: root.maxChar
property int _minLength: 1
label: qsTr(root.label)
hint: textInput.text.length + "/" + _maxLength
placeholderText: qsTr(root.tips)
function setDefaultValue(defaultValue) {
textInput.text = root.type === root._typeOpen ? defaultValue : ""
}
validator: function (text) {
if (mandatory && textInput.text.length < textInput._minLength) {
return qsTr("Field is mandatory");
}
if (textInput.text.length > textInput._maxLength) {
return qsTr("max. %1 characters").arg(_maxLength);
}
return;
}
onTextChanged: {
// Rise max length error immediately while typing if mandatory field
if (textInput.text.length > textInput._maxLength) {
validate();
}
}
visible: root.type === root._typeOpen
}
ButtonGroup {
id: selectionRadio
property string text: {
return checkedButton ? checkedButton.text : "";
}
property bool error: false
function setDefaultValue(defaultValue) {
const values = root.type === root._typeChoice ? defaultValue : [];
for (var i = 0; i < buttons.length; ++i) {
buttons[i].checked = values.includes(buttons[i].text);
}
}
function validate() {
if (mandatory && selectionRadio.text.length === 0) {
error = true;
return
}
error = false;
}
onTextChanged: {
validate();
}
}
Repeater {
model: root.answerList
RadioButton {
ButtonGroup.group: selectionRadio
colorScheme: root.colorScheme
text: modelData
visible: root.type === root._typeChoice
}
}
ButtonGroup {
id: selectionCheckBox
exclusive: false
property string delimitor: ", "
property string text: {
var str = "";
for (var i = 0; i < buttons.length; ++i) {
if (buttons[i].checked) {
str += buttons[i].text + delimitor;
}
}
return str.slice(0, -delimitor.length);
}
property bool error: false
function setDefaultValue(defaultValue) {
const values = root.type === root._typeMutlichoice ? defaultValue.split(delimitor) : [];
for (var i = 0; i < buttons.length; ++i) {
buttons[i].checked = values.includes(buttons[i].text);
}
}
function validate() {
if (mandatory && selectionCheckBox.text.length === 0) {
error = true;
return
}
error = false;
}
onTextChanged: {
validate();
}
}
Repeater {
model: root.answerList
CheckBox {
ButtonGroup.group: selectionCheckBox
colorScheme: root.colorScheme
text: modelData
visible: root.type === root._typeMutlichoice
}
}
RowLayout {
id: footerLayout
Layout.fillWidth: true
spacing: 0
visible: {
if (root.type === root._typeOpen)
return false
return root.error
}
ColorImage {
id: errorIcon
Layout.rightMargin: 4
color: root.colorScheme.signal_danger
height: errorChoice.height
source: "/qml/icons/ic-exclamation-circle-filled.svg"
sourceSize.height: errorChoice.height
}
Label {
id: errorChoice
Layout.fillWidth: true
color: root.colorScheme.signal_danger
colorScheme: root.colorScheme
text: "Field is mandatory"
type: Label.LabelType.Caption_semibold
}
}
}
}
Rectangle {
anchors.bottom: root.bottom
anchors.left: root.left
anchors.right: root.right
color: root.colorScheme.border_weak
height: root._lineHeight
visible: root.showSeparator
}
// fill height so the footer label will always be attached to the bottom
Item {
Layout.fillHeight: true
}
}

View File

@ -0,0 +1,90 @@
{
"metadata": {
"version": "1.0.0"
},
"data_v1.0.0": {
"categories": [
{
"name": "I cannot find emails in my email client",
"questions": [0,5,6,4],
"hint": "For example, missing old emails or not receiving new emails"
},
{
"name": "I am not able to send emails",
"questions": [0,2,3,4],
"hint": ""
},
{
"name": "Bridge is not starting correctly",
"questions": [0,2,3,4],
"hint": "For example, Bridge does not autostart or shows an error at startup"
},
{
"name": "Bridge is running slow",
"questions": [0,2,7,4],
"hint": "For example, delay in receiving email or high resource consumption"
},
{
"name": "Something else",
"questions": [0,1,2,3],
"hint": ""
}
],
"questions": [
{
"id": 0,
"text": "Please describe what happened and include any error messages.",
"tips": "Type your answer here",
"type": "open",
"mandatory": true,
"maxChar": 1000
},
{
"id": 1,
"text": "What did you want or expect to happen?",
"tips": "Type your answer here",
"type": "open",
"maxChar": 1000
},
{
"id": 2,
"text": "What were the step-by-step actions you took that led to this happening?",
"tips": "Type your answer here",
"type": "open",
"maxChar": 1000
},
{
"id": 3,
"text": "When did the issue last occur? Is it repeating?",
"tips": "Type your answer here",
"type": "open",
"maxChar": 1000
},
{
"id": 4,
"text": "Are you running any of these software? Select all that apply.",
"type": "multichoice",
"answerList": ["VPN", "Antivirus", "Firewall", "Cache cleaner", "I'm not sure"]
},
{
"id": 5,
"text": "Are you missing emails from the email client or not receiving new ones?",
"type": "choice",
"mandatory": true,
"answerList": ["Old emails are missing", "New emails are not arriving"]
},
{
"id": 6,
"text": "Can you find the emails in the web/mobile application?",
"type": "choice",
"answerList": ["Yes", "No", "I don't know"]
},
{
"id": 7,
"text": "Which of these issues are you experiencing?",
"type": "multichoice",
"answerList": ["Emails arrive with a delay", "Emails are sending slowly", "Bridge is slow to launch", "Bridge is slowing down my computer"]
}
]
}
}

View File

@ -1,51 +1,44 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import Proton import Proton
Item { Item {
id: root id: root
property var colorScheme enum ActionType {
Toggle = 1,
property string text: "Text" Button,
property string actionText: "Action" PrimaryButton
property string actionIcon: "" }
property string description: "Lorem ipsum dolor sit amet"
property alias descriptionWrap: descriptionLabel.wrapMode
property var type: SettingsItem.ActionType.Toggle
property bool checked: true
property bool loading: false
property bool showSeparator: true
property var _bottomMargin: 20 property var _bottomMargin: 20
property var _lineWidth: 1 property var _lineWidth: 1
property var _toggleTopMargin: 6 property var _toggleTopMargin: 6
property string actionIcon: ""
property string actionText: "Action"
property bool checked: true
property var colorScheme
property string description: "Lorem ipsum dolor sit amet"
property alias descriptionWrap: descriptionLabel.wrapMode
property bool loading: false
property bool showSeparator: true
property string text: "Text"
property var type: SettingsItem.ActionType.Toggle
signal clicked signal clicked
enum ActionType {
Toggle = 1, Button = 2, PrimaryButton = 3
}
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
@ -54,10 +47,9 @@ Item {
spacing: 16 spacing: 16
ColumnLayout { ColumnLayout {
Layout.bottomMargin: root._bottomMargin
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.bottomMargin: root._bottomMargin
spacing: 4 spacing: 4
Label { Label {
@ -66,51 +58,51 @@ Item {
text: root.text text: root.text
type: Label.Body_semibold type: Label.Body_semibold
} }
Label { Label {
id: descriptionLabel id: descriptionLabel
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: parent.width Layout.preferredWidth: parent.width
color: root.colorScheme.text_weak
wrapMode: Text.WordWrap
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: root.description text: root.description
color: root.colorScheme.text_weak wrapMode: Text.WordWrap
} }
} }
Toggle { Toggle {
id: toggle
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
Layout.topMargin: root._toggleTopMargin Layout.topMargin: root._toggleTopMargin
id: toggle checked: root.checked
colorScheme: root.colorScheme colorScheme: root.colorScheme
loading: root.loading
visible: root.type === SettingsItem.ActionType.Toggle visible: root.type === SettingsItem.ActionType.Toggle
checked: root.checked onClicked: {
loading: root.loading if (!root.loading)
onClicked: { if (!root.loading) root.clicked() } root.clicked();
}
} }
Button { Button {
Layout.alignment: Qt.AlignTop
id: button id: button
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme colorScheme: root.colorScheme
visible: root.type === SettingsItem.Button || root.type === SettingsItem.PrimaryButton
text: root.actionText + (root.actionIcon != "" ? " " : "")
loading: root.loading
icon.source: root.actionIcon icon.source: root.actionIcon
onClicked: { if (!root.loading) root.clicked() } loading: root.loading
secondary: root.type !== SettingsItem.PrimaryButton secondary: root.type !== SettingsItem.PrimaryButton
text: root.actionText + (root.actionIcon !== "" ? " " : "")
visible: root.type === SettingsItem.Button || root.type === SettingsItem.PrimaryButton
onClicked: {
if (!root.loading)
root.clicked();
}
} }
} }
Rectangle { Rectangle {
anchors.bottom: root.bottom
anchors.left: root.left anchors.left: root.left
anchors.right: root.right anchors.right: root.right
anchors.bottom: root.bottom
color: colorScheme.border_weak color: colorScheme.border_weak
height: root._lineWidth height: root._lineWidth
visible: root.showSeparator visible: root.showSeparator

View File

@ -1,61 +1,57 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
import Proton import Proton
Item { Item {
id: root id: root
property var colorScheme property int _bottomMargin: 32
default property alias items: content.children
signal back()
property int _leftMargin: 64 property int _leftMargin: 64
property int _rightMargin: 64 property int _rightMargin: 64
property int _topMargin: 32
property int _bottomMargin: 32
property int _spacing: 20 property int _spacing: 20
property int _topMargin: 32
property var colorScheme
// fillHeight indicates whether the SettingsView should fill all available explicit height set // fillHeight indicates whether the SettingsView should fill all available explicit height set
property bool fillHeight: false property bool fillHeight: false
default property alias items: content.children
function positionViewAtBegining() {
scrollView.ScrollBar.vertical.position = 0
}
signal back
ScrollView { ScrollView {
id: scrollView id: scrollView
anchors.fill: parent
clip: true clip: true
anchors.fill: parent ScrollBar.vertical.policy: ScrollBar.vertical.size < 1.0 ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff;
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds // Disable the springy effect when scroll reaches top/bottom. Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds // Disable the springy effect when scroll reaches top/bottom.
Item { Item {
// can't use parent here because parent is not ScrollView (Flickable inside contentItem inside ScrollView)
width: scrollView.availableWidth
height: scrollView.availableHeight height: scrollView.availableHeight
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
// do not set implicitWidth because implicit width of ColumnLayout will be equal to maximum implicit width of // do not set implicitWidth because implicit width of ColumnLayout will be equal to maximum implicit width of
// internal items. And if one of internal items would be a Text or Label - implicit width of those is always // internal items. And if one of internal items would be a Text or Label - implicit width of those is always
// equal to non-wrapped text (i.e. one line only). That will lead to enabling horizontal scroll when not needed // equal to non-wrapped text (i.e. one line only). That will lead to enabling horizontal scroll when not needed
implicitWidth: width implicitWidth: width
// can't use parent here because parent is not ScrollView (Flickable inside contentItem inside ScrollView)
width: scrollView.availableWidth
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
@ -63,16 +59,13 @@ Item {
ColumnLayout { ColumnLayout {
id: content id: content
spacing: root._spacing
Layout.fillWidth: true
Layout.topMargin: root._topMargin
Layout.bottomMargin: root._bottomMargin Layout.bottomMargin: root._bottomMargin
Layout.fillWidth: true
Layout.leftMargin: root._leftMargin Layout.leftMargin: root._leftMargin
Layout.rightMargin: root._rightMargin Layout.rightMargin: root._rightMargin
Layout.topMargin: root._topMargin
spacing: root._spacing
} }
Item { Item {
id: filler id: filler
Layout.fillHeight: true Layout.fillHeight: true
@ -81,19 +74,20 @@ Item {
} }
} }
} }
Button { Button {
id: backButton id: backButton
anchors {
top: parent.top
left: parent.left
topMargin: root._topMargin
leftMargin: (root._leftMargin-backButton.width) / 2
}
colorScheme: root.colorScheme colorScheme: root.colorScheme
onClicked: root.back() horizontalPadding: 8
icon.source: "/qml/icons/ic-arrow-left.svg" icon.source: "/qml/icons/ic-arrow-left.svg"
secondary: true secondary: true
horizontalPadding: 8
onClicked: root.back()
anchors {
left: parent.left
leftMargin: (root._leftMargin - backButton.width) / 2
top: parent.top
topMargin: root._topMargin
}
} }
} }

View File

@ -1,110 +1,139 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
import Proton import Proton
Item { Item {
id:root id: root
property string address
property ColorScheme colorScheme property ColorScheme colorScheme
property var user property var user
property string address
signal dismissed() signal dismissed
signal finished() signal finished
function reset() {
guidePages.currentIndex = 0;
clientList.currentIndex = -1;
actionList.currentIndex = -1;
}
function setupAction(actionID, clientID) {
if (user) {
user.setupGuideSeen = true;
}
switch (actionID) {
case -1:
root.dismissed();
break; // dismiss
case 0 // automatic
:
if (user) {
switch (clientID) {
case 0:
root.user.configureAppleMail(root.address);
Backend.notifyAutoconfigClicked("AppleMail");
break;
}
}
root.finished();
break;
case 1 // manual
:
let clientObj = clients.get(clientID);
if (clientObj !== undefined && clientObj.link !== "") {
Qt.openUrlExternally(clientObj.link);
Backend.notifyKBArticleClicked(clientObj.link);
} else {
console.log("unexpected client index", actionID, clientID);
}
root.finished();
break;
default:
console.log("unexpected client setup action", actionID, clientID);
}
}
implicitHeight: children[0].implicitHeight implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth implicitWidth: children[0].implicitWidth
ListModel { ListModel {
id: clients id: clients
property string name : "Apple Mail"
property string iconSource : "/qml/icons/ic-apple-mail.svg"
property bool haveAutoSetup: true property bool haveAutoSetup: true
property string iconSource: "/qml/icons/ic-apple-mail.svg"
property string link: "https://proton.me/support/protonmail-bridge-clients-apple-mail" property string link: "https://proton.me/support/protonmail-bridge-clients-apple-mail"
property string name: "Apple Mail"
Component.onCompleted : { Component.onCompleted: {
if (Backend.goos == "darwin") { if (Backend.goos === "darwin") {
append({ append({
"name" : "Apple Mail", "name": "Apple Mail",
"iconSource" : "/qml/icons/ic-apple-mail.svg", "iconSource": "/qml/icons/ic-apple-mail.svg",
"haveAutoSetup" : true, "haveAutoSetup": true,
"link" : "https://proton.me/support/protonmail-bridge-clients-apple-mail" "link": "https://proton.me/support/protonmail-bridge-clients-apple-mail"
}) });
append({ append({
"name" : "Microsoft Outlook", "name": "Microsoft Outlook",
"iconSource" : "/qml/icons/ic-microsoft-outlook.svg", "iconSource": "/qml/icons/ic-microsoft-outlook.svg",
"haveAutoSetup" : false, "haveAutoSetup": false,
"link" : "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2019" "link": "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2019"
}) });
} }
if (Backend.goos == "windows") { if (Backend.goos === "windows") {
append({ append({
"name" : "Microsoft Outlook", "name": "Microsoft Outlook",
"iconSource" : "/qml/icons/ic-microsoft-outlook.svg", "iconSource": "/qml/icons/ic-microsoft-outlook.svg",
"haveAutoSetup" : false, "haveAutoSetup": false,
"link" : "https://proton.me/support/protonmail-bridge-clients-windows-outlook-2019" "link": "https://proton.me/support/protonmail-bridge-clients-windows-outlook-2019"
}) });
} }
append({ append({
"name" : "Mozilla Thunderbird", "name": "Mozilla Thunderbird",
"iconSource" : "/qml/icons/ic-mozilla-thunderbird.svg", "iconSource": "/qml/icons/ic-mozilla-thunderbird.svg",
"haveAutoSetup" : false, "haveAutoSetup": false,
"link" : "https://proton.me/support/protonmail-bridge-clients-windows-thunderbird" "link": "https://proton.me/support/protonmail-bridge-clients-windows-thunderbird"
}) });
append({ append({
"name" : "Other", "name": "Other",
"iconSource" : "/qml/icons/ic-other-mail-clients.svg", "iconSource": "/qml/icons/ic-other-mail-clients.svg",
"haveAutoSetup" : false, "haveAutoSetup": false,
"link" : "https://proton.me/support/protonmail-bridge-configure-client" "link": "https://proton.me/support/protonmail-bridge-configure-client"
}) });
} }
} }
Rectangle { Rectangle {
anchors.fill: root anchors.fill: root
color: root.colorScheme.background_norm color: root.colorScheme.background_norm
} }
StackLayout { StackLayout {
id: guidePages id: guidePages
anchors.bottomMargin: 70
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: 80 anchors.leftMargin: 80
anchors.rightMargin: 80 anchors.rightMargin: 80
anchors.topMargin: 30 anchors.topMargin: 30
anchors.bottomMargin: 70
ColumnLayout {
ColumnLayout { // 0: Client selection // 0: Client selection
id: clientView id: clientView
Layout.fillHeight: true
property int columnWidth: 268 property int columnWidth: 268
Layout.fillHeight: true
spacing: 8 spacing: 8
Label { Label {
@ -112,16 +141,14 @@ Item {
text: qsTr("Setting up email client") text: qsTr("Setting up email client")
type: Label.LabelType.Heading type: Label.LabelType.Heading
} }
Label { Label {
color: root.colorScheme.text_weak
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: address text: address
color: root.colorScheme.text_weak
type: Label.LabelType.Lead type: Label.LabelType.Lead
} }
RowLayout { RowLayout {
Layout.topMargin: 32-clientView.spacing Layout.topMargin: 32 - clientView.spacing
spacing: 24 spacing: 24
ColumnLayout { ColumnLayout {
@ -134,185 +161,133 @@ Item {
text: qsTr("Choose an email client") text: qsTr("Choose an email client")
type: Label.LabelType.Body_semibold type: Label.LabelType.Body_semibold
} }
ListView { ListView {
id: clientList id: clientList
Layout.fillHeight: true Layout.fillHeight: true
model: clients
width: clientView.columnWidth width: clientView.columnWidth
model: clients
highlight: Rectangle {
color: root.colorScheme.interaction_default_active
radius: ProtonStyle.context_item_radius
}
delegate: Item { delegate: Item {
implicitWidth: clientRow.width
implicitHeight: clientRow.height implicitHeight: clientRow.height
implicitWidth: clientRow.width
ColumnLayout { ColumnLayout {
id: clientRow id: clientRow
width: clientList.width width: clientList.width
RowLayout { RowLayout {
Layout.topMargin: 12
Layout.bottomMargin: 12 Layout.bottomMargin: 12
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 12
ColorImage { ColorImage {
source: model.iconSource
height: 36 height: 36
source: model.iconSource
sourceSize.height: 36 sourceSize.height: 36
} }
Label { Label {
colorScheme: root.colorScheme
Layout.leftMargin: 12 Layout.leftMargin: 12
colorScheme: root.colorScheme
text: model.name text: model.name
type: Label.LabelType.Body type: Label.LabelType.Body
} }
} }
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 1 Layout.preferredHeight: 1
color: root.colorScheme.border_weak color: root.colorScheme.border_weak
} }
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
clientList.currentIndex = index clientList.currentIndex = index;
if (!model.haveAutoSetup) { if (!model.haveAutoSetup) {
root.setupAction(1,index) root.setupAction(1, index);
} }
} }
} }
} }
highlight: Rectangle {
color: root.colorScheme.interaction_default_active
radius: ProtonStyle.context_item_radius
}
} }
} }
ColumnLayout { ColumnLayout {
id: actionColumn id: actionColumn
visible: clientList.currentIndex >= 0 && clients.get(clientList.currentIndex).haveAutoSetup
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
visible: clientList.currentIndex >= 0 && clients.get(clientList.currentIndex).haveAutoSetup
Label { Label {
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Choose configuration mode") text: qsTr("Choose configuration mode")
type: Label.LabelType.Body_semibold type: Label.LabelType.Body_semibold
} }
ListView { ListView {
id: actionList id: actionList
Layout.fillHeight: true Layout.fillHeight: true
model: [qsTr("Configure automatically"), qsTr("Configure manually")]
width: clientView.columnWidth width: clientView.columnWidth
model: [
qsTr("Configure automatically"),
qsTr("Configure manually"),
]
highlight: Rectangle {
color: root.colorScheme.interaction_default_active
radius: ProtonStyle.context_item_radius
}
delegate: Item { delegate: Item {
implicitWidth: children[0].width
implicitHeight: children[0].height implicitHeight: children[0].height
implicitWidth: children[0].width
ColumnLayout { ColumnLayout {
width: actionList.width width: actionList.width
Label { Label {
Layout.topMargin: 20
Layout.bottomMargin: 20 Layout.bottomMargin: 20
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 20
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: modelData text: modelData
type: Label.LabelType.Body type: Label.LabelType.Body
} }
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 1 Layout.preferredHeight: 1
color: root.colorScheme.border_weak color: root.colorScheme.border_weak
} }
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
actionList.currentIndex = index actionList.currentIndex = index;
root.setupAction(index,clientList.currentIndex) root.setupAction(index, clientList.currentIndex);
} }
} }
} }
highlight: Rectangle {
color: root.colorScheme.interaction_default_active
radius: ProtonStyle.context_item_radius
}
} }
} }
} }
Item {
Item { Layout.fillHeight: true } Layout.fillHeight: true
}
Button { Button {
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Set up later")
flat: true flat: true
text: qsTr("Set up later")
onClicked: { onClicked: {
root.setupAction(-1,-1) root.setupAction(-1, -1);
if (user) { if (user) {
user.setupGuideSeen = true user.setupGuideSeen = true;
} }
root.dismissed() root.dismissed();
} }
} }
} }
} }
function setupAction(actionID,clientID){
if (user) {
user.setupGuideSeen = true
}
switch (actionID) {
case -1: root.dismissed(); break; // dismiss
case 0: // automatic
if (user) {
switch (clientID) {
case 0:
root.user.configureAppleMail(root.address)
Backend.notifyAutoconfigClicked("AppleMail");
break;
}
}
root.finished()
break;
case 1: // manual
var clientObj = clients.get(clientID)
if (clientObj != undefined && clientObj.link != "" ) {
Qt.openUrlExternally(clientObj.link)
Backend.notifyKBArticleClicked(clientObj.link);
} else {
console.log("unexpected client index", actionID, clientID)
}
root.finished();
break;
default:
console.log("unexpected client setup action", actionID, clientID)
}
}
function reset(){
guidePages.currentIndex = 0
clientList.currentIndex = -1
actionList.currentIndex = -1
}
} }

View File

@ -1,473 +1,413 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml import QtQml
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
import Proton import Proton
FocusScope { FocusScope {
id: root id: root
property ColorScheme colorScheme property ColorScheme colorScheme
property alias currentIndex: stackLayout.currentIndex
function reset() { property alias username: usernameTextField.text
stackLayout.currentIndex = 0
loginNormalLayout.reset()
login2FALayout.reset()
login2PasswordLayout.reset()
}
function abort() { function abort() {
root.reset() root.reset();
Backend.loginAbort(usernameTextField.text) Backend.loginAbort(usernameTextField.text);
}
function reset() {
stackLayout.currentIndex = 0;
loginNormalLayout.reset();
login2FALayout.reset();
login2PasswordLayout.reset();
} }
implicitHeight: children[0].implicitHeight implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth implicitWidth: children[0].implicitWidth
property alias username: usernameTextField.text
state: "Page 1" state: "Page 1"
property alias currentIndex: stackLayout.currentIndex states: [
State {
name: "Page 1"
PropertyChanges {
currentIndex: 0
target: stackLayout
}
},
State {
name: "Page 2"
PropertyChanges {
currentIndex: 1
target: stackLayout
}
},
State {
name: "Page 3"
PropertyChanges {
currentIndex: 2
target: stackLayout
}
}
]
StackLayout { StackLayout {
id: stackLayout id: stackLayout
function loginFailed() {
signInButton.loading = false;
usernameTextField.enabled = true;
usernameTextField.error = true;
passwordTextField.enabled = true;
passwordTextField.error = true;
}
anchors.fill: parent anchors.fill: parent
function loginFailed() {
signInButton.loading = false
usernameTextField.enabled = true
usernameTextField.error = true
passwordTextField.enabled = true
passwordTextField.error = true
}
Connections { Connections {
target: Backend function onLogin2FAError(_) {
console.assert(stackLayout.currentIndex === 1, "Unexpected login2FAError");
function onLoginUsernamePasswordError(errorMsg) { twoFAButton.loading = false;
console.assert(stackLayout.currentIndex == 0, "Unexpected loginUsernamePasswordError") twoFactorPasswordTextField.enabled = true;
console.assert(signInButton.loading == true, "Unexpected loginUsernamePasswordError") twoFactorPasswordTextField.error = true;
twoFactorPasswordTextField.errorString = qsTr("Your code is incorrect");
stackLayout.loginFailed() twoFactorPasswordTextField.focus = true;
if (errorMsg!="") errorLabel.text = errorMsg
else errorLabel.text = qsTr("Incorrect login credentials")
} }
function onLogin2FAErrorAbort(_) {
function onLoginFreeUserError() { console.assert(stackLayout.currentIndex === 1, "Unexpected login2FAErrorAbort");
console.assert(stackLayout.currentIndex == 0, "Unexpected loginFreeUserError") root.reset();
stackLayout.loginFailed() errorLabel.text = qsTr("Incorrect login credentials. Please try again.");
} }
function onLogin2FARequested(username) {
function onLoginConnectionError(errorMsg) { console.assert(stackLayout.currentIndex === 0, "Unexpected login2FARequested");
if (stackLayout.currentIndex == 0 ) { twoFactorUsernameLabel.text = username;
stackLayout.loginFailed() stackLayout.currentIndex = 1;
twoFactorPasswordTextField.focus = true;
}
function onLogin2PasswordError(_) {
console.assert(stackLayout.currentIndex === 2, "Unexpected login2PasswordError");
secondPasswordButton.loading = false;
secondPasswordTextField.enabled = true;
secondPasswordTextField.error = true;
secondPasswordTextField.errorString = qsTr("Your mailbox password is incorrect");
secondPasswordTextField.focus = true;
}
function onLogin2PasswordErrorAbort(_) {
console.assert(stackLayout.currentIndex === 2, "Unexpected login2PasswordErrorAbort");
root.reset();
errorLabel.text = qsTr("Incorrect login credentials. Please try again.");
}
function onLogin2PasswordRequested() {
console.assert(stackLayout.currentIndex === 0 || stackLayout.currentIndex === 1, "Unexpected login2PasswordRequested");
stackLayout.currentIndex = 2;
secondPasswordTextField.focus = true;
}
function onLoginAlreadyLoggedIn(_) {
stackLayout.currentIndex = 0;
root.reset();
}
function onLoginConnectionError(_) {
if (stackLayout.currentIndex === 0) {
stackLayout.loginFailed();
} }
} }
function onLoginFinished(_) {
function onLogin2FARequested(username) { stackLayout.currentIndex = 0;
console.assert(stackLayout.currentIndex == 0, "Unexpected login2FARequested") root.reset();
twoFactorUsernameLabel.text = username }
stackLayout.currentIndex = 1 function onLoginFreeUserError() {
twoFactorPasswordTextField.focus = true console.assert(stackLayout.currentIndex === 0, "Unexpected loginFreeUserError");
stackLayout.loginFailed();
}
function onLoginUsernamePasswordError(errorMsg) {
console.assert(stackLayout.currentIndex === 0, "Unexpected loginUsernamePasswordError");
stackLayout.loginFailed();
if (errorMsg !== "")
errorLabel.text = errorMsg;
else
errorLabel.text = qsTr("Incorrect login credentials");
} }
function onLogin2FAError(errorMsg) { target: Backend
console.assert(stackLayout.currentIndex == 1, "Unexpected login2FAError")
twoFAButton.loading = false
twoFactorPasswordTextField.enabled = true
twoFactorPasswordTextField.error = true
twoFactorPasswordTextField.errorString = qsTr("Your code is incorrect")
twoFactorPasswordTextField.focus = true
}
function onLogin2FAErrorAbort(errorMsg) {
console.assert(stackLayout.currentIndex == 1, "Unexpected login2FAErrorAbort")
root.reset()
errorLabel.text = qsTr("Incorrect login credentials. Please try again.")
}
function onLogin2PasswordRequested() {
console.assert(stackLayout.currentIndex == 0 || stackLayout.currentIndex == 1, "Unexpected login2PasswordRequested")
stackLayout.currentIndex = 2
secondPasswordTextField.focus = true
}
function onLogin2PasswordError(errorMsg) {
console.assert(stackLayout.currentIndex == 2, "Unexpected login2PasswordError")
secondPasswordButton.loading = false
secondPasswordTextField.enabled = true
secondPasswordTextField.error = true
secondPasswordTextField.errorString = qsTr("Your mailbox password is incorrect")
secondPasswordTextField.focus = true
}
function onLogin2PasswordErrorAbort(errorMsg) {
console.assert(stackLayout.currentIndex == 2, "Unexpected login2PasswordErrorAbort")
root.reset()
errorLabel.text = qsTr("Incorrect login credentials. Please try again.")
}
function onLoginFinished(index) {
stackLayout.currentIndex = 0
root.reset()
}
} }
ColumnLayout { ColumnLayout {
id: loginNormalLayout id: loginNormalLayout
function reset() { function reset() {
signInButton.loading = false signInButton.loading = false;
errorLabel.text = "";
errorLabel.text = "" usernameTextField.enabled = true;
usernameTextField.error = false;
usernameTextField.enabled = true usernameTextField.errorString = "";
usernameTextField.error = false usernameTextField.focus = true;
usernameTextField.errorString = "" passwordTextField.enabled = true;
usernameTextField.focus = true passwordTextField.error = false;
passwordTextField.errorString = "";
passwordTextField.enabled = true passwordTextField.text = "";
passwordTextField.error = false
passwordTextField.errorString = ""
passwordTextField.text = ""
} }
spacing: 0 spacing: 0
Label { Label {
colorScheme: root.colorScheme
text: qsTr("Sign in")
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 16 Layout.topMargin: 16
colorScheme: root.colorScheme
text: qsTr("Sign in")
type: Label.LabelType.Title type: Label.LabelType.Title
} }
Label { Label {
colorScheme: root.colorScheme
id: subTitle id: subTitle
text: qsTr("Enter your Proton Account details.")
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 8 Layout.topMargin: 8
color: root.colorScheme.text_weak color: root.colorScheme.text_weak
colorScheme: root.colorScheme
text: qsTr("Enter your Proton Account details.")
type: Label.LabelType.Body type: Label.LabelType.Body
} }
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 36 Layout.topMargin: 36
spacing: 0 spacing: 0
visible: errorLabel.text.length > 0 visible: errorLabel.text.length > 0
ColorImage { ColorImage {
color: root.colorScheme.signal_danger color: root.colorScheme.signal_danger
source: "/qml/icons/ic-exclamation-circle-filled.svg"
height: errorLabel.lineHeight height: errorLabel.lineHeight
source: "/qml/icons/ic-exclamation-circle-filled.svg"
sourceSize.height: errorLabel.lineHeight sourceSize.height: errorLabel.lineHeight
} }
Label { Label {
colorScheme: root.colorScheme
id: errorLabel id: errorLabel
wrapMode: Text.WordWrap Layout.fillWidth: true
Layout.fillWidth: true;
Layout.leftMargin: 4 Layout.leftMargin: 4
color: root.colorScheme.signal_danger color: root.colorScheme.signal_danger
colorScheme: root.colorScheme
type: root.error ? Label.LabelType.Caption_semibold : Label.LabelType.Caption type: root.error ? Label.LabelType.Caption_semibold : Label.LabelType.Caption
wrapMode: Text.WordWrap
} }
} }
TextField { TextField {
colorScheme: root.colorScheme
id: usernameTextField id: usernameTextField
label: qsTr("Email or username")
focus: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 24 Layout.topMargin: 24
colorScheme: root.colorScheme
focus: true
label: qsTr("Email or username")
validateOnEditingFinished: false validateOnEditingFinished: false
validator: function (str) {
onTextChanged: {
// remove "invalid username / password error"
if (error || errorLabel.text.length > 0) {
errorLabel.text = ""
usernameTextField.error = false
passwordTextField.error = false
}
}
validator: function(str) {
if (str.length === 0) { if (str.length === 0) {
return qsTr("Enter email or username") return qsTr("Enter email or username");
} }
return
} }
onAccepted: passwordTextField.forceActiveFocus() onAccepted: passwordTextField.forceActiveFocus()
}
TextField {
colorScheme: root.colorScheme
id: passwordTextField
label: qsTr("Password")
Layout.fillWidth: true
Layout.topMargin: 8
echoMode: TextInput.Password
validateOnEditingFinished: false
onTextChanged: { onTextChanged: {
// remove "invalid username / password error" // remove "invalid username / password error"
if (error || errorLabel.text.length > 0) { if (error || errorLabel.text.length > 0) {
errorLabel.text = "" errorLabel.text = "";
usernameTextField.error = false usernameTextField.error = false;
passwordTextField.error = false passwordTextField.error = false;
} }
} }
}
validator: function(str) { TextField {
id: passwordTextField
Layout.fillWidth: true
Layout.topMargin: 8
colorScheme: root.colorScheme
echoMode: TextInput.Password
label: qsTr("Password")
validateOnEditingFinished: false
validator: function (str) {
if (str.length === 0) { if (str.length === 0) {
return qsTr("Enter password") return qsTr("Enter password");
} }
return
} }
onAccepted: signInButton.checkAndSignIn() onAccepted: signInButton.checkAndSignIn()
} onTextChanged: {
// remove "invalid username / password error"
Button { if (error || errorLabel.text.length > 0) {
colorScheme: root.colorScheme errorLabel.text = "";
id: signInButton usernameTextField.error = false;
text: loading ? qsTr("Signing in") : qsTr("Sign in") passwordTextField.error = false;
enabled: !loading
Layout.fillWidth: true
Layout.topMargin: 24
onClicked: checkAndSignIn()
function checkAndSignIn() {
usernameTextField.validate()
passwordTextField.validate()
if (usernameTextField.error || passwordTextField.error) {
return
} }
usernameTextField.enabled = false
passwordTextField.enabled = false
loading = true
Backend.login(usernameTextField.text, Qt.btoa(passwordTextField.text))
} }
} }
Button {
id: signInButton
function checkAndSignIn() {
usernameTextField.validate();
passwordTextField.validate();
if (usernameTextField.error || passwordTextField.error) {
return;
}
usernameTextField.enabled = false;
passwordTextField.enabled = false;
loading = true;
Backend.login(usernameTextField.text, Qt.btoa(passwordTextField.text));
}
Label { Layout.fillWidth: true
Layout.topMargin: 24
colorScheme: root.colorScheme colorScheme: root.colorScheme
textFormat: Text.StyledText enabled: !loading
text: link("https://proton.me/mail/pricing", qsTr("Create or upgrade your account")) text: loading ? qsTr("Signing in") : qsTr("Sign in")
onClicked: {
checkAndSignIn();
}
}
Label {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 24 Layout.topMargin: 24
colorScheme: root.colorScheme
text: link("https://proton.me/mail/pricing", qsTr("Create or upgrade your account"))
textFormat: Text.StyledText
type: Label.LabelType.Body type: Label.LabelType.Body
onLinkActivated: { onLinkActivated: {
Qt.openUrlExternally(link) Qt.openUrlExternally(link);
} }
} }
} }
ColumnLayout { ColumnLayout {
id: login2FALayout id: login2FALayout
function reset() { function reset() {
twoFAButton.loading = false twoFAButton.loading = false;
twoFactorPasswordTextField.enabled = true;
twoFactorPasswordTextField.enabled = true twoFactorPasswordTextField.error = false;
twoFactorPasswordTextField.error = false twoFactorPasswordTextField.errorString = "";
twoFactorPasswordTextField.errorString = "" twoFactorPasswordTextField.text = "";
twoFactorPasswordTextField.text = ""
} }
spacing: 0 spacing: 0
Label { Label {
Layout.alignment: Qt.AlignCenter
Layout.topMargin: 16
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Two-factor authentication") text: qsTr("Two-factor authentication")
Layout.topMargin: 16
Layout.alignment: Qt.AlignCenter
type: Label.LabelType.Heading type: Label.LabelType.Heading
} }
Label { Label {
colorScheme: root.colorScheme
id: twoFactorUsernameLabel id: twoFactorUsernameLabel
Layout.alignment: Qt.AlignCenter Layout.alignment: Qt.AlignCenter
Layout.topMargin: 8 Layout.topMargin: 8
type: Label.LabelType.Lead
color: root.colorScheme.text_weak color: root.colorScheme.text_weak
}
TextField {
colorScheme: root.colorScheme colorScheme: root.colorScheme
type: Label.LabelType.Lead
}
TextField {
id: twoFactorPasswordTextField id: twoFactorPasswordTextField
label: qsTr("Two-factor code")
assistiveText: qsTr("Enter the 6-digit code")
validateOnEditingFinished: false
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 32 Layout.topMargin: 32
assistiveText: qsTr("Enter the 6-digit code")
validator: function(str) { colorScheme: root.colorScheme
label: qsTr("Two-factor code")
validateOnEditingFinished: false
validator: function (str) {
if (str.length === 0) { if (str.length === 0) {
return qsTr("Enter the 6-digit code") return qsTr("Enter the 6-digit code");
}
}
onTextChanged: {
if (text.length >= 6) {
twoFAButton.onClicked()
} }
} }
onAccepted: { onAccepted: {
twoFAButton.onClicked() twoFAButton.onClicked();
}
onTextChanged: {
if (text.length >= 6) {
twoFAButton.onClicked();
}
} }
} }
Button { Button {
colorScheme: root.colorScheme
id: twoFAButton id: twoFAButton
text: loading ? qsTr("Authenticating") : qsTr("Authenticate")
enabled: !loading
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 24 Layout.topMargin: 24
colorScheme: root.colorScheme
enabled: !loading
text: loading ? qsTr("Authenticating") : qsTr("Authenticate")
onClicked: { onClicked: {
twoFactorPasswordTextField.validate() twoFactorPasswordTextField.validate();
if (twoFactorPasswordTextField.error) { if (twoFactorPasswordTextField.error) {
return return;
} }
twoFactorPasswordTextField.enabled = false;
twoFactorPasswordTextField.enabled = false loading = true;
loading = true Backend.login2FA(usernameTextField.text, Qt.btoa(twoFactorPasswordTextField.text));
Backend.login2FA(usernameTextField.text, Qt.btoa(twoFactorPasswordTextField.text))
} }
} }
} }
ColumnLayout { ColumnLayout {
id: login2PasswordLayout id: login2PasswordLayout
function reset() { function reset() {
secondPasswordButton.loading = false secondPasswordButton.loading = false;
secondPasswordTextField.enabled = true;
secondPasswordTextField.enabled = true secondPasswordTextField.error = false;
secondPasswordTextField.error = false secondPasswordTextField.errorString = "";
secondPasswordTextField.errorString = "" secondPasswordTextField.text = "";
secondPasswordTextField.text = ""
} }
spacing: 0 spacing: 0
Label { Label {
Layout.alignment: Qt.AlignCenter
Layout.topMargin: 16
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Unlock your mailbox") text: qsTr("Unlock your mailbox")
Layout.topMargin: 16
Layout.alignment: Qt.AlignCenter
type: Label.LabelType.Heading type: Label.LabelType.Heading
} }
TextField { TextField {
colorScheme: root.colorScheme
id: secondPasswordTextField id: secondPasswordTextField
label: qsTr("Mailbox password")
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 + implicitHeight + 24 + subTitle.implicitHeight Layout.topMargin: 8 + implicitHeight + 24 + subTitle.implicitHeight
colorScheme: root.colorScheme
echoMode: TextInput.Password echoMode: TextInput.Password
label: qsTr("Mailbox password")
validateOnEditingFinished: false validateOnEditingFinished: false
validator: function (str) {
validator: function(str) {
if (str.length === 0) { if (str.length === 0) {
return qsTr("Enter password") return qsTr("Enter password");
} }
return
} }
onAccepted: { onAccepted: {
secondPasswordButton.onClicked() secondPasswordButton.onClicked();
} }
} }
Button { Button {
colorScheme: root.colorScheme
id: secondPasswordButton id: secondPasswordButton
text: loading ? qsTr("Unlocking") : qsTr("Unlock")
enabled: !loading
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 24 Layout.topMargin: 24
colorScheme: root.colorScheme
enabled: !loading
text: loading ? qsTr("Unlocking") : qsTr("Unlock")
onClicked: { onClicked: {
secondPasswordTextField.validate() secondPasswordTextField.validate();
if (secondPasswordTextField.error) { if (secondPasswordTextField.error) {
return return;
} }
secondPasswordTextField.enabled = false;
secondPasswordTextField.enabled = false loading = true;
loading = true Backend.login2Password(usernameTextField.text, Qt.btoa(secondPasswordTextField.text));
Backend.login2Password(usernameTextField.text, Qt.btoa(secondPasswordTextField.text))
} }
} }
} }
} }
states: [
State {
name: "Page 1"
PropertyChanges {
target: stackLayout
currentIndex: 0
}
},
State {
name: "Page 2"
PropertyChanges {
target: stackLayout
currentIndex: 1
}
},
State {
name: "Page 3"
PropertyChanges {
target: stackLayout
currentIndex: 2
}
}
]
} }

View File

@ -1,194 +1,169 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml import QtQml
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
import Proton import Proton
Dialog { Dialog {
id: root id: root
leftPadding: 0
shouldShow: Backend.showSplashScreen
modal: true modal: true
rightPadding: 0
topPadding : 0 shouldShow: Backend.showSplashScreen
leftPadding : 0 topPadding: 0
rightPadding : 0
ColumnLayout { ColumnLayout {
spacing: 20 spacing: 20
Image { Image {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
sourceSize.width: 384
sourceSize.height: 144
Layout.preferredWidth: 384
Layout.preferredHeight: 144 Layout.preferredHeight: 144
Layout.preferredWidth: 384
source: "./icons/img-splash.png" source: "./icons/img-splash.png"
sourceSize.height: 144
sourceSize.width: 384
} }
Label { Label {
colorScheme: root.colorScheme; Layout.alignment: Qt.AlignHCenter
Layout.alignment: Qt.AlignHCenter;
Layout.leftMargin: 24 Layout.leftMargin: 24
Layout.rightMargin: 24
Layout.preferredWidth: 336 Layout.preferredWidth: 336
Layout.rightMargin: 24
type: Label.Title colorScheme: root.colorScheme
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: qsTr("What's new in Bridge") text: qsTr("What's new in Bridge")
type: Label.Title
} }
RowLayout { RowLayout {
width: root.width width: root.width
Item { Item {
Layout.fillHeight: true Layout.fillHeight: true
width: 24
Layout.leftMargin: 32 Layout.leftMargin: 32
Layout.rightMargin: 16 Layout.rightMargin: 16
width: 24
Image { Image {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
sourceSize.width: 24
sourceSize.height: 24
source: "./icons/ic-splash-check.svg" source: "./icons/ic-splash-check.svg"
sourceSize.height: 24
sourceSize.width: 24
} }
} }
Label { Label {
colorScheme: root.colorScheme Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter;
Layout.preferredWidth: 264
Layout.leftMargin: 0 Layout.leftMargin: 0
Layout.preferredWidth: 264
Layout.rightMargin: 24 Layout.rightMargin: 24
wrapMode: Text.WordWrap colorScheme: root.colorScheme
type: Label.Body
horizontalAlignment: Text.AlignLeft horizontalAlignment: Text.AlignLeft
textFormat: Text.StyledText
text: qsTr("<b>New IMAP engine</b><br/>For improved stability and performance.") text: qsTr("<b>New IMAP engine</b><br/>For improved stability and performance.")
textFormat: Text.StyledText
type: Label.Body
wrapMode: Text.WordWrap
} }
} }
RowLayout { RowLayout {
width: root.width width: root.width
Item { Item {
Layout.fillHeight: true Layout.fillHeight: true
width: 24
Layout.leftMargin: 32 Layout.leftMargin: 32
Layout.rightMargin: 16 Layout.rightMargin: 16
width: 24
Image { Image {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
sourceSize.width: 24
sourceSize.height: 24
source: "./icons/ic-splash-check.svg" source: "./icons/ic-splash-check.svg"
sourceSize.height: 24
sourceSize.width: 24
} }
} }
Label { Label {
colorScheme: root.colorScheme Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter;
Layout.preferredWidth: 264
Layout.leftMargin: 0 Layout.leftMargin: 0
Layout.preferredWidth: 264
Layout.rightMargin: 24 Layout.rightMargin: 24
wrapMode: Text.WordWrap colorScheme: root.colorScheme
type: Label.Body
horizontalAlignment: Text.AlignLeft horizontalAlignment: Text.AlignLeft
textFormat: Text.StyledText
text: qsTr("<b>Faster than ever</b><br/>Up to 10x faster syncing and receiving.") text: qsTr("<b>Faster than ever</b><br/>Up to 10x faster syncing and receiving.")
textFormat: Text.StyledText
type: Label.Body
wrapMode: Text.WordWrap
} }
} }
RowLayout { RowLayout {
width: root.width width: root.width
Item { Item {
Layout.fillHeight: true Layout.fillHeight: true
width: 24
Layout.leftMargin: 32 Layout.leftMargin: 32
Layout.rightMargin: 16 Layout.rightMargin: 16
width: 24
Image { Image {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
sourceSize.width: 24
sourceSize.height: 24
source: "./icons/ic-splash-check.svg" source: "./icons/ic-splash-check.svg"
sourceSize.height: 24
sourceSize.width: 24
} }
} }
Label { Label {
colorScheme: root.colorScheme Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter;
Layout.preferredWidth: 264
Layout.leftMargin: 0 Layout.leftMargin: 0
Layout.preferredWidth: 264
Layout.rightMargin: 24 Layout.rightMargin: 24
wrapMode: Text.WordWrap colorScheme: root.colorScheme
type: Label.Body
horizontalAlignment: Text.AlignLeft horizontalAlignment: Text.AlignLeft
textFormat: Text.StyledText
text: qsTr("<b>Extra security</b><br/>New, encrypted local database and keychain improvements.") text: qsTr("<b>Extra security</b><br/>New, encrypted local database and keychain improvements.")
textFormat: Text.StyledText
type: Label.Body
wrapMode: Text.WordWrap
} }
} }
Button { Button {
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: 24 Layout.leftMargin: 24
Layout.rightMargin: 24 Layout.rightMargin: 24
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: "Got it" text: "Got it"
onClicked: Backend.showSplashScreen = false onClicked: Backend.showSplashScreen = false
} }
Label { Label {
colorScheme: root.colorScheme Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter;
Layout.preferredWidth: 336
Layout.leftMargin: 24 Layout.leftMargin: 24
Layout.preferredWidth: 336
Layout.rightMargin: 24 Layout.rightMargin: 24
colorScheme: root.colorScheme
horizontalAlignment: Text.AlignHCenter
text: qsTr("Note that your client will redownload all the emails.<br/>") + link("https://proton.me/blog/new-proton-mail-bridge", qsTr("Learn more about new Bridge."))
textFormat: Text.StyledText
type: Label.Body
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
type: Label.Body onLinkActivated: function (link) {
horizontalAlignment: Text.AlignHCenter Qt.openUrlExternally(link);
textFormat: Text.StyledText }
text: qsTr("Note that your client will redownload all the emails.<br/>") + link("https://proton.me/blog/new-proton-mail-bridge", qsTr("Learn more about new Bridge."))
onLinkActivated: function(link) { Qt.openUrlExternally(link) }
} }
} }
} }

View File

@ -1,111 +1,93 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl import QtQuick.Controls.impl
import Proton import Proton
import Notifications import Notifications
Item { Item {
id: root id: root
property var notifications
property ColorScheme colorScheme
property int notificationWhitelist: NotificationFilter.FilterConsts.All
property int notificationBlacklist: NotificationFilter.FilterConsts.None
readonly property Notification activeNotification: notificationFilter.topmost readonly property Notification activeNotification: notificationFilter.topmost
property ColorScheme colorScheme
property int notificationBlacklist: NotificationFilter.FilterConsts.None
property int notificationWhitelist: NotificationFilter.FilterConsts.All
property var notifications
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
NotificationFilter { NotificationFilter {
id: notificationFilter id: notificationFilter
blacklist: root.notificationBlacklist
source: root.notifications ? root.notifications.all : undefined source: root.notifications ? root.notifications.all : undefined
whitelist: root.notificationWhitelist whitelist: root.notificationWhitelist
blacklist: root.notificationBlacklist
onTopmostChanged: { onTopmostChanged: {
if (!topmost) { if (!topmost) {
image.source = "/qml/icons/ic-connected.svg" image.source = "/qml/icons/ic-connected.svg";
image.color = root.colorScheme.signal_success image.color = root.colorScheme.signal_success;
label.text = qsTr("Connected") label.text = qsTr("Connected");
label.color = root.colorScheme.signal_success label.color = root.colorScheme.signal_success;
return; return;
} }
image.source = topmost.icon;
image.source = topmost.icon label.text = topmost.brief;
label.text = topmost.brief
switch (topmost.type) { switch (topmost.type) {
case Notification.NotificationType.Danger: case Notification.NotificationType.Danger:
image.color = root.colorScheme.signal_danger image.color = root.colorScheme.signal_danger;
label.color = root.colorScheme.signal_danger label.color = root.colorScheme.signal_danger;
break; break;
case Notification.NotificationType.Warning: case Notification.NotificationType.Warning:
image.color = root.colorScheme.signal_warning image.color = root.colorScheme.signal_warning;
label.color = root.colorScheme.signal_warning label.color = root.colorScheme.signal_warning;
break; break;
case Notification.NotificationType.Success: case Notification.NotificationType.Success:
image.color = root.colorScheme.signal_success image.color = root.colorScheme.signal_success;
label.color = root.colorScheme.signal_success label.color = root.colorScheme.signal_success;
break; break;
case Notification.NotificationType.Info: case Notification.NotificationType.Info:
image.color = root.colorScheme.signal_info image.color = root.colorScheme.signal_info;
label.color = root.colorScheme.signal_info label.color = root.colorScheme.signal_info;
break; break;
} }
} }
} }
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
spacing: 8 spacing: 8
ColorImage { ColorImage {
id: image id: image
width: 16
height: 16
sourceSize.width: width
sourceSize.height: height
source: "/qml/icons/ic-connected.svg"
color: root.colorScheme.signal_success color: root.colorScheme.signal_success
height: 16
source: "/qml/icons/ic-connected.svg"
sourceSize.height: height
sourceSize.width: width
width: 16
} }
Label { Label {
colorScheme: root.colorScheme
id: label id: label
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
text: qsTr("Connected")
color: root.colorScheme.signal_success color: root.colorScheme.signal_success
colorScheme: root.colorScheme
horizontalAlignment: Text.AlignLeft
text: qsTr("Connected")
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
} }
} }
} }

View File

@ -1,25 +1,19 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify // Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful, // Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml import QtQml
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import Proton import Proton
Item { Item {
@ -34,24 +28,46 @@ Item {
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: 0
Rectangle { states: [
color: root.colorScheme.background_norm State {
name: "Page 1"
PropertyChanges {
currentIndex: 0
target: signInItem
}
},
State {
name: "Page 2"
PropertyChanges {
currentIndex: 1
target: signInItem
}
},
State {
name: "Page 3"
PropertyChanges {
currentIndex: 2
target: signInItem
}
}
]
Rectangle {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
color: root.colorScheme.background_norm
implicitHeight: children[0].implicitHeight implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth implicitWidth: children[0].implicitWidth
visible: signInItem.currentIndex === 0
visible: signInItem.currentIndex == 0
GridLayout { GridLayout {
anchors.fill: parent anchors.fill: parent
columnSpacing: 0 columnSpacing: 0
rowSpacing: 0
columns: 3 columns: 3
rowSpacing: 0
// top margin // top margin
Item { Item {
@ -59,141 +75,123 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
// Using binding component here instead of direct binding to avoid binding loop during construction of element // Using binding component here instead of direct binding to avoid binding loop during construction of element
Binding on Layout.preferredHeight { Binding on Layout.preferredHeight {
value: (parent.height - welcomeContentItem.height) / 4 value: (parent.height - welcomeContentItem.height) / 4
} }
} }
// left margin // left margin
Item { Item {
Layout.minimumWidth: 48
Layout.maximumWidth: 80
Layout.fillWidth: true Layout.fillWidth: true
Layout.maximumWidth: 80
Layout.minimumWidth: 48
Layout.preferredHeight: welcomeContentItem.height Layout.preferredHeight: welcomeContentItem.height
} }
ColumnLayout { ColumnLayout {
id: welcomeContentItem id: welcomeContentItem
Layout.fillWidth: true Layout.fillWidth: true
spacing: 0 spacing: 0
Image { Image {
source: colorScheme.welcome_img
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 16 Layout.topMargin: 16
source: colorScheme.welcome_img
sourceSize.height: 148 sourceSize.height: 148
sourceSize.width: 264 sourceSize.width: 264
} }
Label { Label {
colorScheme: root.colorScheme
text: qsTr("Welcome to\nProton Mail Bridge")
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
colorScheme: root.colorScheme
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: qsTr("Welcome to\nProton Mail Bridge")
type: Label.LabelType.Heading type: Label.LabelType.Heading
} }
Label { Label {
colorScheme: root.colorScheme
id: longTextLabel id: longTextLabel
text: qsTr("Add your Proton Mail account to securely access and manage your messages in your favorite email client. Bridge runs in the background and encrypts and decrypts your messages seamlessly.")
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16
Layout.preferredWidth: 320 Layout.preferredWidth: 320
Layout.topMargin: 16
wrapMode: Text.WordWrap colorScheme: root.colorScheme
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: qsTr("Add your Proton Mail account to securely access and manage your messages in your favorite email client. Bridge runs in the background and encrypts and decrypts your messages seamlessly.")
type: Label.LabelType.Body type: Label.LabelType.Body
wrapMode: Text.WordWrap
} }
} }
// Right margin // Right margin
Item { Item {
Layout.minimumWidth: 48
Layout.maximumWidth: 80
Layout.fillWidth: true Layout.fillWidth: true
Layout.maximumWidth: 80
Layout.minimumWidth: 48
Layout.preferredHeight: welcomeContentItem.height Layout.preferredHeight: welcomeContentItem.height
} }
// bottom margin // bottom margin
Item { Item {
Layout.columnSpan: 3 Layout.columnSpan: 3
Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true
implicitHeight: children[0].implicitHeight + children[0].anchors.bottomMargin + children[0].anchors.topMargin implicitHeight: children[0].implicitHeight + children[0].anchors.bottomMargin + children[0].anchors.topMargin
implicitWidth: children[0].implicitWidth implicitWidth: children[0].implicitWidth
Image { Image {
id: logoImage id: logoImage
source: colorScheme.logo_img
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.topMargin: 48
anchors.bottomMargin: 48 anchors.bottomMargin: 48
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 48
source: colorScheme.logo_img
sourceSize.height: 25 sourceSize.height: 25
sourceSize.width: 200 sourceSize.width: 200
} }
} }
} }
} }
Rectangle { Rectangle {
color: (signInItem.currentIndex == 0) ? root.colorScheme.background_weak : root.colorScheme.background_norm
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
color: (signInItem.currentIndex == 0) ? root.colorScheme.background_weak : root.colorScheme.background_norm
implicitHeight: children[0].implicitHeight implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth implicitWidth: children[0].implicitWidth
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: 0
Item { Item {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: signInItem.currentIndex == 0 ? 0 : parent.width / 4 Layout.preferredWidth: signInItem.currentIndex == 0 ? 0 : parent.width / 4
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
Button { Button {
colorScheme: root.colorScheme
anchors.left: parent.left
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: 80
anchors.left: parent.left
anchors.leftMargin: 80 anchors.leftMargin: 80
anchors.rightMargin: 80 anchors.rightMargin: 80
anchors.topMargin: 80 anchors.topMargin: 80
anchors.bottomMargin: 80 colorScheme: root.colorScheme
visible: signInItem.currentIndex != 0
secondary: true secondary: true
text: qsTr("Back") text: qsTr("Back")
visible: signInItem.currentIndex != 0
onClicked: { onClicked: {
signInItem.abort() signInItem.abort();
} }
} }
} }
GridLayout { GridLayout {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
columnSpacing: 0 columnSpacing: 0
rowSpacing: 0
columns: 3 columns: 3
rowSpacing: 0
// top margin // top margin
Item { Item {
@ -201,76 +199,47 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
// Using binding component here instead of direct binding to avoid binding loop during construction of element // Using binding component here instead of direct binding to avoid binding loop during construction of element
Binding on Layout.preferredHeight { Binding on Layout.preferredHeight {
value: (parent.height - signInItem.height) / 4 value: (parent.height - signInItem.height) / 4
} }
} }
// left margin // left margin
Item { Item {
Layout.minimumWidth: 48
Layout.maximumWidth: 80
Layout.fillWidth: true Layout.fillWidth: true
Layout.maximumWidth: 80
Layout.minimumWidth: 48
Layout.preferredHeight: signInItem.height Layout.preferredHeight: signInItem.height
} }
SignIn { SignIn {
id: signInItem id: signInItem
colorScheme: root.colorScheme
Layout.preferredWidth: 320
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: 320
colorScheme: root.colorScheme
focus: true focus: true
username: Backend.users.count === 1 && Backend.users.get(0) && (Backend.users.get(0).state === EUserState.SignedOut) ? Backend.users.get(0).username : "" username: Backend.users.count === 1 && Backend.users.get(0) && (Backend.users.get(0).state === EUserState.SignedOut) ? Backend.users.get(0).username : ""
} }
// Right margin // Right margin
Item { Item {
Layout.minimumWidth: 48
Layout.maximumWidth: 80
Layout.fillWidth: true Layout.fillWidth: true
Layout.maximumWidth: 80
Layout.minimumWidth: 48
Layout.preferredHeight: signInItem.height Layout.preferredHeight: signInItem.height
} }
// bottom margin // bottom margin
Item { Item {
Layout.columnSpan: 3 Layout.columnSpan: 3
Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true
} }
} }
Item { Item {
Layout.fillHeight: true Layout.fillHeight: true
Layout.preferredWidth: signInItem.currentIndex == 0 ? 0 : parent.width / 4 Layout.preferredWidth: signInItem.currentIndex === 0 ? 0 : parent.width / 4
} }
} }
} }
states: [
State {
name: "Page 1"
PropertyChanges {
target: signInItem
currentIndex: 0
}
},
State {
name: "Page 2"
PropertyChanges {
target: signInItem
currentIndex: 1
}
},
State {
name: "Page 3"
PropertyChanges {
target: signInItem
currentIndex: 2
}
}
]
} }
} }

View File

@ -0,0 +1,5 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.75 15.5V8H8.75V9.5H10.25V15.5H8.75V17H13.25V15.5H11.75Z" fill="#0C0C14"/>
<path d="M11.75 6.5V5H10.25V6.5H11.75Z" fill="#0C0C14"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.5 11C0.5 5.20101 5.20101 0.5 11 0.5C13.7848 0.5 16.4555 1.60625 18.4246 3.57538C20.3938 5.54451 21.5 8.21523 21.5 11C21.5 16.799 16.799 21.5 11 21.5C5.20101 21.5 0.5 16.799 0.5 11ZM2 11C2 15.9706 6.02944 20 11 20C13.3869 20 15.6761 19.0518 17.364 17.364C19.0518 15.6761 20 13.3869 20 11C20 6.02944 15.9706 2 11 2C6.02944 2 2 6.02944 2 11Z" fill="#0C0C14"/>
</svg>

After

Width:  |  Height:  |  Size: 656 B

View File

@ -1,76 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge 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 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge 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.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Window
import QtQuick.Layouts
import QtQuick.Controls
import "../Proton"
RowLayout {
id: root
property ColorScheme colorScheme
// Primary buttons
ButtonsColumn {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.fillHeight: true
iconLoading: "/qml/icons/Loader_16.svg"
}
// Secondary buttons
ButtonsColumn {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.fillHeight: true
secondary: true
iconLoading: "/qml/icons/Loader_16.svg"
}
// Secondary icons
ButtonsColumn {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.fillHeight: true
secondary: true
textNormal: ""
iconNormal: "/qml/icons/ic-cross-close.svg"
textDisabled: ""
iconDisabled: "/qml/icons/ic-cross-close.svg"
textLoading: ""
iconLoading: "/qml/icons/Loader_16.svg"
}
// Icons
ButtonsColumn {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.fillHeight: true
textNormal: ""
iconNormal: "/qml/icons/ic-cross-close.svg"
textDisabled: ""
iconDisabled: "/qml/icons/ic-cross-close.svg"
textLoading: ""
iconLoading: "/qml/icons/Loader_16.svg"
}
}

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