forked from Silverfish/proton-bridge
Compare commits
167 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b7ef6e1486 | |||
| 0d03f84711 | |||
| 949666724d | |||
| bbe19bf960 | |||
| bfe25e3a46 | |||
| 236c958703 | |||
| e6b312b437 | |||
| 45d2e9ea63 | |||
| 86e8a566c7 | |||
| 7689139cb3 | |||
| 6269b1ab88 | |||
| 79524185a8 | |||
| 635b81314a | |||
| 4c76e35a2d | |||
| b0ac20425e | |||
| 21a56a0725 | |||
| c57b52ef23 | |||
| 44e103edd5 | |||
| 970de4c205 | |||
| a189c35899 | |||
| c9323a40c8 | |||
| 7a192d50db | |||
| a5a9bd762d | |||
| dd7db00f74 | |||
| a6c20f698c | |||
| f7252ed40e | |||
| 66c30716ea | |||
| 06d7fdf26f | |||
| 8aee732fe8 | |||
| 47233752f5 | |||
| cb6e51531b | |||
| 0b9b886039 | |||
| 1fa0d77b10 | |||
| efbe84964f | |||
| aa77a67a1c | |||
| 78f7cbdc79 | |||
| a731237701 | |||
| f557666b4d | |||
| 5f89e85139 | |||
| 9fb922d18f | |||
| 884c6ed932 | |||
| db77bd4983 | |||
| 85bfcf7158 | |||
| 09a5f8ac0f | |||
| 81f81a63e8 | |||
| e724fafe2f | |||
| cba73997cd | |||
| 29f44fc312 | |||
| 41c125f65e | |||
| 0a555bf767 | |||
| 24331f9715 | |||
| a5500629e5 | |||
| a46533dcf2 | |||
| 12183fbf05 | |||
| c90248920a | |||
| ccb9b7f81c | |||
| 78c0651661 | |||
| d72980e443 | |||
| b24937b666 | |||
| 5ca9ec6674 | |||
| 4ee6da4baa | |||
| e8ce8942be | |||
| 9d0d3708af | |||
| 4c908aac7c | |||
| 6eb1878f66 | |||
| 826dc2e5c3 | |||
| e6ab874308 | |||
| 20b188368a | |||
| ded4f370dc | |||
| 519a6ecdb7 | |||
| c35344d6f1 | |||
| a9865976a3 | |||
| 9a96588afb | |||
| 1f2c573803 | |||
| cc33423c1f | |||
| 1b95c290f1 | |||
| 9b88778c43 | |||
| ae4705ba70 | |||
| 243ddf47ab | |||
| 80d729e3e5 | |||
| 3d64c5f894 | |||
| c4bcc38c53 | |||
| 2c2f816f3a | |||
| 86e115b2f3 | |||
| 1a2783a63b | |||
| cbbab71f5c | |||
| 80add80be2 | |||
| f0e2688a8e | |||
| 06639ff6cd | |||
| 716de7f45a | |||
| 750de0cd31 | |||
| 823ca4d207 | |||
| a187747c7c | |||
| 11ebb16933 | |||
| 0048767022 | |||
| c4b75c6f34 | |||
| 32448063dc | |||
| 86bde91958 | |||
| 90c34406ba | |||
| 334a256638 | |||
| da528f2d9b | |||
| 1b0f930471 | |||
| 09c523e2d2 | |||
| 0b35f41ffd | |||
| 7be1a8ae8a | |||
| bdbf1bdd76 | |||
| 776976a8a2 | |||
| 040ddadb7a | |||
| 11f6f84dd6 | |||
| 82efa16d65 | |||
| 110286b81c | |||
| bc66841cdc | |||
| 8d028966c7 | |||
| d120bbeffc | |||
| eda49483e2 | |||
| 3ef526333a | |||
| f454f1a74f | |||
| e97a4d8847 | |||
| cb8174dbfd | |||
| cfca429067 | |||
| 1ffb9089ba | |||
| c0fc23d7cd | |||
| 42178806d1 | |||
| e34050282e | |||
| efb6ba0f1b | |||
| 80194ad797 | |||
| cda6b2a728 | |||
| 44bde86fde | |||
| 6cb52cacc9 | |||
| 8248491833 | |||
| fbdede28f0 | |||
| 66bc911652 | |||
| c860741ffa | |||
| 300d243331 | |||
| c4abb14ae6 | |||
| a06fd31f49 | |||
| 016319784e | |||
| 1d595b933a | |||
| 39cd165bd1 | |||
| afef730870 | |||
| f691795ca1 | |||
| 9798cdec5c | |||
| 9ae899f420 | |||
| f2a8990972 | |||
| 8db30a89c0 | |||
| 449b580056 | |||
| 38397accf5 | |||
| 5d49cf2c09 | |||
| c1918e2b1b | |||
| 60a779c653 | |||
| 4adc05d354 | |||
| 86fbf4415a | |||
| b4ea667858 | |||
| 8f45fe823a | |||
| 3b1c3b9d44 | |||
| 60256fd076 | |||
| 9c1b9e8df2 | |||
| fb44de4f18 | |||
| c821d02f67 | |||
| ec2a4f9111 | |||
| 968a01053f | |||
| 6f914a4973 | |||
| 76bdc21fef | |||
| dd29ff4731 | |||
| 8b94a28e00 | |||
| 60100ad7f0 | |||
| 62a50fd7fc |
@ -47,6 +47,19 @@ stages:
|
||||
allow_failure: true
|
||||
- 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-windows:
|
||||
before_script:
|
||||
@ -118,6 +131,15 @@ lint:
|
||||
tags:
|
||||
- medium
|
||||
|
||||
bug-report-preview:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make lint-bug-report-preview
|
||||
tags:
|
||||
- medium
|
||||
|
||||
.script-test:
|
||||
stage: test
|
||||
extends:
|
||||
@ -134,6 +156,15 @@ test-linux:
|
||||
tags:
|
||||
- large
|
||||
|
||||
fuzz-linux:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
script:
|
||||
- make fuzz
|
||||
tags:
|
||||
- large
|
||||
|
||||
test-linux-race:
|
||||
extends:
|
||||
- test-linux
|
||||
@ -154,11 +185,19 @@ test-integration-race:
|
||||
script:
|
||||
- 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:
|
||||
extends:
|
||||
- .env-windows
|
||||
- .script-test
|
||||
- .rules-branch-and-MR-manual
|
||||
|
||||
test-darwin:
|
||||
extends:
|
||||
@ -168,15 +207,16 @@ test-darwin:
|
||||
test-coverage:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
- .rules-branch-manual-scheduled-and-test-branch-always
|
||||
script:
|
||||
- ./utils/coverage.sh
|
||||
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
|
||||
needs:
|
||||
- test-linux
|
||||
#- test-windows
|
||||
- test-windows
|
||||
- test-darwin
|
||||
- test-integration
|
||||
- test-integration-nightly
|
||||
tags:
|
||||
- small
|
||||
artifacts:
|
||||
|
||||
149
Changelog.md
149
Changelog.md
@ -3,6 +3,99 @@
|
||||
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
|
||||
|
||||
### Fixed
|
||||
* GODT-2859: Trigger user resync while updating from 3.4.0 to 3.4.1.
|
||||
* GODT-2833: Fix migration of message flags.
|
||||
* GODT-2759: Use examine rather than select for fetching.
|
||||
|
||||
|
||||
## Trift Bridge 3.4.0
|
||||
|
||||
### Added
|
||||
@ -41,7 +134,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* GODT-2578: Refresh literals appended to Sent folder.
|
||||
* GODT-2753: Vault test now check that value auto-assigned is first available port.
|
||||
* 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-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.
|
||||
@ -126,7 +219,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* GODT-2437: Silence harmless report to sentry.
|
||||
* GODT-2649: Clean up cache files after failed connector create (Gluon).
|
||||
* 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.
|
||||
* Test: fix flaky tests.
|
||||
* GODT-2628: Attempt to fix closed channel panic on logout.
|
||||
@ -186,7 +279,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* GODT-2574: Fix label/unlabel of large amounts of messages.
|
||||
* GODT-2573: Handle invalid header fields in message.
|
||||
* 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
|
||||
|
||||
@ -327,7 +420,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* GODT-2429: Do not report context cancel to sentry.
|
||||
|
||||
### 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-2427: Parsing header issues.
|
||||
* GODT-2426: Fix crash on user delete.
|
||||
@ -344,7 +437,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* GODT-2404: Handle unexpected EOF.
|
||||
* GODT-2400: Allow state updates to be applied if command fails.
|
||||
* 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-2414: Multiple deletion bug in WriteControlledStore.
|
||||
|
||||
@ -409,7 +502,7 @@ GODT-1804: Preserve MIME parameters when uploading attachments.
|
||||
* GODT-2223: Improve event handling.
|
||||
* GODT-2305: Detect missing gluon DB.
|
||||
* 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-2258: suggest email as login when signing in via status window.
|
||||
* Other: Report corrupt and/or insecure vaults to sentry.
|
||||
@ -689,7 +782,7 @@ GODT-1804: Preserve MIME parameters when uploading attachments.
|
||||
## [Bridge 2.4.6] Osney
|
||||
|
||||
### 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-2046: Bridge-gui log is included in optional archive sent with bug reports.
|
||||
* GODT-2039: Bridge monitors bridge-gui via its PID.
|
||||
@ -843,7 +936,7 @@ GODT-1804: Preserve MIME parameters when uploading attachments.
|
||||
* GODT-1260: Renaming.
|
||||
* GODT-1502: Rebranding: color and radius.
|
||||
* GODT-1549: Add notification when address list changes.
|
||||
* GODT-1560: Dependecy licenses update and link.
|
||||
* GODT-1560: Dependency licenses update and link.
|
||||
|
||||
### Changed
|
||||
* GODT-1543: Using one buffered event for off and on connection.
|
||||
@ -940,7 +1033,7 @@ GODT-1537: Manual in-app update mechanism.
|
||||
* GODT-1338: GODT-1343 Help view buttons.
|
||||
* GODT-1340: Not crashing, user list updating in main thread.
|
||||
* 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-1210: Add "free user" banner.
|
||||
* GODT-1314: Limit description field length within 150/800 bounds.
|
||||
@ -982,7 +1075,7 @@ GODT-1537: Manual in-app update mechanism.
|
||||
* GODT-1381 Treat readonly folder as failure for cache on disk.
|
||||
* GODT-1431 Prevent watcher when not using disk on cache.
|
||||
* 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: moved cache related functions to separate file.
|
||||
* GODT-1332 moving cache does not work on Windows.
|
||||
@ -1233,7 +1326,7 @@ GODT-1537: Manual in-app update mechanism.
|
||||
### Fixed
|
||||
* GODT-1029 Fix tray icon not updating under certain conditions.
|
||||
* 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-1055 Fix flaky empty trash test.
|
||||
|
||||
@ -1323,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-870 Added GUI notification on error during silent update.
|
||||
* 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-874 Added manual triggers to Updater module.
|
||||
* GODT-851 Added support of UID EXPUNGE.
|
||||
@ -1647,7 +1740,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
||||
|
||||
### Changed
|
||||
* 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-387 Store factory to make store optional.
|
||||
* GODT-386 Renamed bridge to general users and keep bridge only for bridge stuff.
|
||||
@ -1812,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-95 Do not throw error when trying to create new mailbox in IMAP root.
|
||||
* 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).
|
||||
* Using go.enmime@v0.6.1 snapshot.
|
||||
* Better detection of non-auth-error.
|
||||
* 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.
|
||||
* #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.
|
||||
* #1050 pollNow waits for events to be processed.
|
||||
* #1047 Fix fetch of empty mailbox.
|
||||
@ -1944,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.
|
||||
* Closing body after checking internet connection.
|
||||
* 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).
|
||||
* #888 TLS Cert.
|
||||
* Error bar and modal with explanation in GUI.
|
||||
@ -1952,7 +2045,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
||||
* Add pinning to bridge (only for live API builds).
|
||||
* #887 #883:
|
||||
* 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.
|
||||
* Panic handler for all goroutines.
|
||||
* CD for linux.
|
||||
@ -2000,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.
|
||||
* #880, #884, #885, #886 fix of informing user about outgoing non-encrypted e-mail.
|
||||
* #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.
|
||||
* Fix aliases in split mode.
|
||||
* Fix decrypted data in log notification.
|
||||
@ -2074,7 +2167,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
||||
|
||||
### Changed
|
||||
* Fix custom message format.
|
||||
* #802 acumulated long lines while parsing body structure.
|
||||
* #802 accumulated long lines while parsing body structure.
|
||||
* Process `AddressEvent` before `MessageEvent`.
|
||||
* #791 updated crypto: fix wrong signature format.
|
||||
* #793 fix returning size.
|
||||
@ -2096,7 +2189,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
||||
|
||||
### Changed
|
||||
* #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.
|
||||
* #750 Status is using DB status instead of API.
|
||||
* Format panic error as string instead of struct dump.
|
||||
@ -2113,7 +2206,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
||||
* Full version of program visible on release notes.
|
||||
|
||||
### Changed
|
||||
* #720 only one concurent DB sync.
|
||||
* #720 only one concurrent DB sync.
|
||||
* #720 sync every 3 pages.
|
||||
* #512 extending list of charsets go-pm-mime!4.
|
||||
|
||||
@ -2137,7 +2230,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
||||
* Fix srp modulus issue with new `ProtonMail/crypto`.
|
||||
* Generate version files from main file.
|
||||
* 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.
|
||||
* Copyright 2019.
|
||||
* Exclude `protontech` repos from credits.
|
||||
@ -2156,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 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`.
|
||||
* 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.
|
||||
* #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).
|
||||
@ -2228,7 +2321,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
||||
* Start with new versioning.
|
||||
|
||||
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)
|
||||
`------- major version (big changes, once per year, breaking changes, api force upgrade)
|
||||
|
||||
@ -2294,7 +2387,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
||||
* All `client.Do` errors are interpreted as connection issue.
|
||||
* Moved to internal gitlab.
|
||||
* 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.
|
||||
|
||||
### Changed
|
||||
@ -2326,12 +2419,12 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
||||
* Keychain format and function refactor.
|
||||
* Create crash file on panic with full trace.
|
||||
* 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.
|
||||
|
||||
### Added
|
||||
* 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
|
||||
* 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).
|
||||
@ -2356,7 +2449,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
||||
* Bug report window.
|
||||
* Checkbox and with label (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.
|
||||
* Input box style (used in bug report).
|
||||
* Input field style (used in add account and change port).
|
||||
|
||||
43
Makefile
43
Makefile
@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||
|
||||
# Keep version hardcoded so app build works also without Git repository.
|
||||
BRIDGE_APP_VERSION?=3.4.0+git
|
||||
BRIDGE_APP_VERSION?=3.5.1+git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
APP_FULL_NAME:=Proton Mail Bridge
|
||||
APP_VENDOR:=Proton AG
|
||||
@ -246,7 +246,7 @@ test-race: gofiles
|
||||
test-integration: gofiles
|
||||
mkdir -p coverage/integration
|
||||
go test \
|
||||
-v -timeout=60m -p=1 -count=1 \
|
||||
-v -timeout=60m -p=1 -count=1 -tags=test_integration \
|
||||
${GOCOVERAGE} \
|
||||
github.com/ProtonMail/proton-bridge/v3/tests \
|
||||
${GOCOVERDIR}/integration
|
||||
@ -258,6 +258,22 @@ test-integration-debug: gofiles
|
||||
test-integration-race: gofiles
|
||||
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:
|
||||
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
|
||||
@ -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/telemetry HeartbeatManager > internal/telemetry/mocks/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:
|
||||
./utils/missing_license.sh check
|
||||
@ -292,6 +321,12 @@ lint-golang:
|
||||
$(info linting with GOMAXPROCS=${GOMAXPROCS})
|
||||
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 -wait -cache -config utils/gobinsec_conf.yml ${EXE_TARGET} ${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE}
|
||||
|
||||
|
||||
@ -2,13 +2,13 @@
|
||||
|
||||
## 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
|
||||
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
|
||||
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
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
@ -58,8 +58,8 @@ sequenceDiagram
|
||||
C ->> B: IMAP SELECT directory
|
||||
C ->> B: IMAP SEARCH e-mails UIDs
|
||||
C ->> B: IMAP FETCH of e-mail UID
|
||||
B ->> S: Fetch body and attachements
|
||||
Note right of B: Decrypt message<br/>and attachement
|
||||
B ->> S: Fetch body and attachments
|
||||
Note right of B: Decrypt message<br/>and attachment
|
||||
B ->> C: IMAP response
|
||||
end
|
||||
```
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
# 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
|
||||
* Manual in-app update
|
||||
* Manual install
|
||||
|
||||
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.
|
||||
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
|
||||
* 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
|
||||
|
||||
4
go.mod
4
go.mod
@ -5,9 +5,9 @@ go 1.20
|
||||
require (
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||
github.com/Masterminds/semver/v3 v3.2.0
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230706110757-a9327fb18611
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20230829112217-5d5c25c504b5
|
||||
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/PuerkitoBio/goquery v1.8.1
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||
|
||||
8
go.sum
8
go.sum
@ -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/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/gluon v0.16.1-0.20230706110757-a9327fb18611 h1:QVydPr/+pgz5xihc2ujNNV+qnq3oTidIXvF0PgkcY6U=
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230706110757-a9327fb18611/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20230829112217-5d5c25c504b5 h1:C/8P5NHAKi2yCKez+OZ5rSR8SsL7k8si4pK4SE2QtV8=
|
||||
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/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||
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-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-proton-api v0.4.1-0.20230727082922-9115b4750ec7 h1:Rmg3TPK6vFGNWR4hxmPoBhV75Sl716iB46wEi2U4Q+c=
|
||||
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 h1:JVMK2w90bCWayUCXJIb3wkQ5+j2P/NbnrX3BrDoLzsc=
|
||||
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/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc=
|
||||
|
||||
@ -52,6 +52,9 @@ const (
|
||||
flagCPUProfile = "cpu-prof"
|
||||
flagCPUProfileShort = "p"
|
||||
|
||||
flagTraceProfile = "trace-prof"
|
||||
flagTraceProfileShort = "t"
|
||||
|
||||
flagMemProfile = "mem-prof"
|
||||
flagMemProfileShort = "m"
|
||||
|
||||
@ -96,6 +99,11 @@ func New() *cli.App {
|
||||
Aliases: []string{flagCPUProfileShort},
|
||||
Usage: "Generate CPU profile",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagTraceProfile,
|
||||
Aliases: []string{flagTraceProfileShort},
|
||||
Usage: "Generate Trace profile",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagMemProfile,
|
||||
Aliases: []string{flagMemProfileShort},
|
||||
@ -379,6 +387,11 @@ func withProfiler(c *cli.Context, fn func() error) error {
|
||||
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) {
|
||||
logrus.Debug("Running with memory profiling")
|
||||
defer profile.Start(profile.MemProfile, profile.MemProfileAllocs, profile.ProfilePath(".")).Stop()
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// 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/>.
|
||||
|
||||
//go:build !build_qa
|
||||
//go:build !build_qa && !test_integration
|
||||
|
||||
package bridge
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// 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/>.
|
||||
|
||||
//go:build build_qa
|
||||
//go:build build_qa || test_integration
|
||||
|
||||
package bridge
|
||||
|
||||
|
||||
@ -23,7 +23,6 @@ import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
@ -38,8 +37,11 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"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/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/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
@ -59,7 +61,7 @@ type Bridge struct {
|
||||
// api manages user API clients.
|
||||
api *proton.Manager
|
||||
proxyCtl ProxyController
|
||||
identifier Identifier
|
||||
identifier identifier.Identifier
|
||||
|
||||
// tlsConfig holds the bridge TLS config used by the IMAP and SMTP servers.
|
||||
tlsConfig *tls.Config
|
||||
@ -125,9 +127,8 @@ type Bridge struct {
|
||||
// goHeartbeat triggers a check/sending if heartbeat is needed.
|
||||
goHeartbeat func()
|
||||
|
||||
uidValidityGenerator imap.UIDValidityGenerator
|
||||
|
||||
serverManager *ServerManager
|
||||
serverManager *imapsmtpserver.Service
|
||||
syncService *syncservice.Service
|
||||
}
|
||||
|
||||
// New creates a new bridge.
|
||||
@ -140,7 +141,7 @@ func New(
|
||||
|
||||
apiURL string, // the URL of the API 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
|
||||
roundTripper http.RoundTripper, // the round tripper to use for API requests
|
||||
proxyCtl ProxyController, // the DoH controller
|
||||
@ -207,7 +208,7 @@ func newBridge(
|
||||
reporter reporter.Reporter,
|
||||
|
||||
api *proton.Manager,
|
||||
identifier Identifier,
|
||||
identifier identifier.Identifier,
|
||||
proxyCtl ProxyController,
|
||||
uidValidityGenerator imap.UIDValidityGenerator,
|
||||
|
||||
@ -269,17 +270,26 @@ func newBridge(
|
||||
firstStart: firstStart,
|
||||
lastVersion: lastVersion,
|
||||
|
||||
tasks: tasks,
|
||||
|
||||
uidValidityGenerator: uidValidityGenerator,
|
||||
|
||||
serverManager: newServerManager(),
|
||||
tasks: tasks,
|
||||
syncService: syncservice.NewService(reporter, panicHandler),
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
bridge.syncService.Run(bridge.tasks)
|
||||
|
||||
return bridge, nil
|
||||
}
|
||||
|
||||
@ -407,11 +417,6 @@ func (bridge *Bridge) GetErrors() []error {
|
||||
func (bridge *Bridge) Close(ctx context.Context) {
|
||||
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.
|
||||
safe.Lock(func() {
|
||||
for _, user := range bridge.users {
|
||||
@ -419,6 +424,11 @@ func (bridge *Bridge) Close(ctx context.Context) {
|
||||
}
|
||||
}, 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.
|
||||
bridge.tasks.CancelAndWait()
|
||||
|
||||
@ -527,24 +537,6 @@ func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
|
||||
}, 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 {
|
||||
if a < b {
|
||||
return a
|
||||
|
||||
@ -44,6 +44,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||
"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/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||
@ -54,6 +55,7 @@ import (
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -621,6 +623,10 @@ func TestBridge_AddressWithoutKeys(t *testing.T) {
|
||||
defer m.Close()
|
||||
|
||||
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.
|
||||
userID, _, err := s.CreateUser("nokeys", []byte("password"))
|
||||
require.NoError(t, err)
|
||||
@ -641,10 +647,6 @@ func TestBridge_AddressWithoutKeys(t *testing.T) {
|
||||
// Remove the address keys.
|
||||
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.
|
||||
require.NoError(t, getErr(bridge.LoginFull(context.Background(), "nokeys", []byte("password"), nil, nil)))
|
||||
require.NoError(t, err)
|
||||
@ -700,10 +702,10 @@ func TestBridge_InitGluonDirectory(t *testing.T) {
|
||||
configDir, err := b.GetGluonDataDir()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
|
||||
_, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
|
||||
require.False(t, os.IsNotExist(err))
|
||||
|
||||
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir))
|
||||
_, err = os.ReadDir(imapsmtpserver.ApplyGluonConfigPathSuffix(configDir))
|
||||
require.False(t, os.IsNotExist(err))
|
||||
})
|
||||
})
|
||||
@ -776,16 +778,16 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Old store should no more exists.
|
||||
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(currentCacheDir))
|
||||
_, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(currentCacheDir))
|
||||
require.True(t, os.IsNotExist(err))
|
||||
// Database should not have changed.
|
||||
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir))
|
||||
_, err = os.ReadDir(imapsmtpserver.ApplyGluonConfigPathSuffix(configDir))
|
||||
require.False(t, os.IsNotExist(err))
|
||||
|
||||
// New path should have Gluon sub-folder.
|
||||
require.Equal(t, filepath.Join(newCacheDir, "gluon"), b.GetGluonCacheDir())
|
||||
// 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))
|
||||
|
||||
// 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.
|
||||
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...)
|
||||
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()) {
|
||||
outCh := make(chan Out)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
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))
|
||||
}
|
||||
|
||||
outCh <- out
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case outCh <- out:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return outCh, done
|
||||
return outCh, func() {
|
||||
cancel()
|
||||
done()
|
||||
}
|
||||
}
|
||||
|
||||
type eventWaiter struct {
|
||||
|
||||
@ -33,7 +33,7 @@ const (
|
||||
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
|
||||
|
||||
if info, err := bridge.QueryUserInfo(username); err == nil {
|
||||
@ -82,7 +82,7 @@ func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, descript
|
||||
OS: osType,
|
||||
OSVersion: osVersion,
|
||||
|
||||
Title: "[Bridge] Bug",
|
||||
Title: "[Bridge] Bug - " + title,
|
||||
Description: description,
|
||||
|
||||
Client: client,
|
||||
|
||||
@ -79,7 +79,7 @@ func (bridge *Bridge) CheckClientState(ctx context.Context, checkFlags bool, pro
|
||||
}
|
||||
|
||||
log.Debug("Building state")
|
||||
state, err := meta.BuildMailboxToMessageMap(usr)
|
||||
state, err := meta.BuildMailboxToMessageMap(ctx, usr)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to build state")
|
||||
return result, err
|
||||
@ -230,7 +230,7 @@ func (bridge *Bridge) DebugDownloadFailedMessages(
|
||||
}
|
||||
|
||||
func clientGetMessageIDs(client *goimapclient.Client, mailbox string) (map[string]imap.FlagSet, error) {
|
||||
status, err := client.Select(mailbox, false)
|
||||
status, err := client.Select(mailbox, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
45
internal/bridge/events.go
Normal file
45
internal/bridge/events.go
Normal 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)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -20,23 +20,12 @@ package bridge
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon"
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
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/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -45,16 +34,6 @@ func (bridge *Bridge) restartIMAP(ctx context.Context) error {
|
||||
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) {
|
||||
switch event := event.(type) {
|
||||
case imapEvents.UserAdded:
|
||||
@ -92,108 +71,59 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
||||
}
|
||||
}
|
||||
|
||||
func ApplyGluonCachePathSuffix(basePath string) string {
|
||||
return filepath.Join(basePath, "backend", "store")
|
||||
type bridgeIMAPSettings struct {
|
||||
b *Bridge
|
||||
}
|
||||
|
||||
func ApplyGluonConfigPathSuffix(basePath string) string {
|
||||
return filepath.Join(basePath, "backend", "db")
|
||||
func (b *bridgeIMAPSettings) EventPublisher() imapsmtpserver.IMAPEventPublisher {
|
||||
return b
|
||||
}
|
||||
|
||||
func newIMAPServer(
|
||||
gluonCacheDir, gluonConfigDir string,
|
||||
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)
|
||||
func (b *bridgeIMAPSettings) TLSConfig() *tls.Config {
|
||||
return b.b.tlsConfig
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"gluonStore": gluonCacheDir,
|
||||
"gluonDB": gluonConfigDir,
|
||||
"version": version,
|
||||
"logClient": logClient,
|
||||
"logServer": logServer,
|
||||
}).Info("Creating IMAP server")
|
||||
func (b *bridgeIMAPSettings) LogClient() bool {
|
||||
return b.b.logIMAPClient
|
||||
}
|
||||
|
||||
if logClient || logServer {
|
||||
log := logrus.WithField("protocol", "IMAP")
|
||||
log.Warning("================================================")
|
||||
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
|
||||
log.Warning("================================================")
|
||||
func (b *bridgeIMAPSettings) LogServer() bool {
|
||||
return b.b.logIMAPServer
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
26
internal/bridge/imapsmtp_telemetry.go
Normal file
26
internal/bridge/imapsmtp_telemetry.go
Normal 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)
|
||||
}
|
||||
@ -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) {
|
||||
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{}))
|
||||
defer done()
|
||||
|
||||
|
||||
@ -467,7 +467,9 @@ SGVsbG8gd29ybGQK
|
||||
require.Eventually(t, func() bool {
|
||||
messages, err := clientFetch(senderIMAPClient, `Sent`, imap.FetchBodyStructure)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 4, len(messages))
|
||||
if len(messages) != 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
// messages may not be in order
|
||||
for _, message := range messages {
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -20,11 +20,10 @@ package bridge
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"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/vault"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -131,26 +130,41 @@ func (bridge *Bridge) GetGluonDataDir() (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)
|
||||
}
|
||||
|
||||
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 {
|
||||
return bridge.vault.GetProxyAllowed()
|
||||
}
|
||||
@ -318,16 +332,3 @@ func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ import (
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"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) {
|
||||
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) {
|
||||
|
||||
@ -21,43 +21,37 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/identifier"
|
||||
)
|
||||
|
||||
func (bridge *Bridge) restartSMTP(ctx context.Context) error {
|
||||
return bridge.serverManager.RestartSMTP(ctx)
|
||||
}
|
||||
|
||||
func newSMTPServer(bridge *Bridge, tlsConfig *tls.Config, logSMTP bool) *smtp.Server {
|
||||
logrus.WithField("logSMTP", logSMTP).Info("Creating SMTP server")
|
||||
|
||||
smtpServer := smtp.NewServer(&smtpBackend{Bridge: bridge})
|
||||
|
||||
smtpServer.TLSConfig = tlsConfig
|
||||
smtpServer.Domain = constants.Host
|
||||
smtpServer.AllowInsecureAuth = true
|
||||
smtpServer.MaxLineLength = 1 << 16
|
||||
smtpServer.ErrorLog = logging.NewSMTPLogger()
|
||||
|
||||
// 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 {
|
||||
return sasl.NewLoginServer(func(username, password string) error {
|
||||
return conn.Session().AuthPlain(username, password)
|
||||
})
|
||||
})
|
||||
|
||||
if logSMTP {
|
||||
log := logrus.WithField("protocol", "SMTP")
|
||||
log.Warning("================================================")
|
||||
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
|
||||
log.Warning("================================================")
|
||||
|
||||
smtpServer.Debug = logging.NewSMTPDebugLogger()
|
||||
}
|
||||
|
||||
return smtpServer
|
||||
type bridgeSMTPSettings struct {
|
||||
b *Bridge
|
||||
}
|
||||
|
||||
func (b *bridgeSMTPSettings) TLSConfig() *tls.Config {
|
||||
return b.b.tlsConfig
|
||||
}
|
||||
|
||||
func (b *bridgeSMTPSettings) Log() bool {
|
||||
return b.b.logSMTP
|
||||
}
|
||||
|
||||
func (b *bridgeSMTPSettings) Port() int {
|
||||
return b.b.vault.GetSMTPPort()
|
||||
}
|
||||
|
||||
func (b *bridgeSMTPSettings) SetPort(i int) error {
|
||||
return b.b.vault.SetSMTPPort(i)
|
||||
}
|
||||
|
||||
func (b *bridgeSMTPSettings) UseSSL() bool {
|
||||
return b.b.vault.GetSMTPSSL()
|
||||
}
|
||||
|
||||
func (b *bridgeSMTPSettings) Identifier() identifier.UserAgentUpdater {
|
||||
return &bridgeUserAgentUpdater{Bridge: b.b}
|
||||
}
|
||||
|
||||
@ -21,9 +21,11 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
@ -252,14 +254,17 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
||||
|
||||
// Login the user; its sync should fail.
|
||||
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{}))
|
||||
defer done()
|
||||
syncFailedCh, syncFailedDone := chToType[events.Event, events.SyncFailed](b.GetEvents(events.SyncFailed{}))
|
||||
defer syncFailedDone()
|
||||
|
||||
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
require.Equal(t, userID, (<-syncFailedCh).UserID)
|
||||
|
||||
info, err := b.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
@ -282,11 +287,7 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
||||
|
||||
// Remove the network limit, allowing the sync to finish.
|
||||
netCtl.SetReadLimit(0)
|
||||
|
||||
{
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
require.Equal(t, userID, (<-syncCh).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)))
|
||||
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.
|
||||
require.Eventually(t, func() bool {
|
||||
status, err := client.Select(`Folders/folder2`, true)
|
||||
@ -321,6 +316,269 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
||||
}, 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
|
||||
m := proton.New(
|
||||
proton.WithHostURL(s.GetHostURL()),
|
||||
|
||||
@ -31,7 +31,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Disabled due to flakyness.
|
||||
// Disabled due to flakiness.
|
||||
func _TestBridge_SyncExistsWithErrorWhenTooManyFilesAreOpen(t *testing.T) { //nolint:unused
|
||||
var rlimitCurrent syscall.Rlimit
|
||||
|
||||
|
||||
@ -32,15 +32,7 @@ type Locator interface {
|
||||
GetLicenseFilePath() string
|
||||
GetDependencyLicensesLink() string
|
||||
Clear(...string) error
|
||||
}
|
||||
|
||||
type Identifier interface {
|
||||
GetUserAgent() string
|
||||
HasClient() bool
|
||||
SetClient(name, version string)
|
||||
SetPlatform(platform string)
|
||||
SetClientString(client string)
|
||||
GetClientString() string
|
||||
ProvideIMAPSyncConfigPath() (string, error)
|
||||
}
|
||||
|
||||
type ProxyController interface {
|
||||
|
||||
@ -30,6 +30,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"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/user"
|
||||
"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 {
|
||||
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 {
|
||||
if !bridge.vault.HasUser(userID) {
|
||||
return ErrNoSuchUser
|
||||
@ -252,6 +258,10 @@ func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
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{
|
||||
UserID: userID,
|
||||
AddressMode: mode,
|
||||
@ -335,13 +337,7 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
|
||||
logrus.WithError(rerr).Error("Failed to report feedback failure")
|
||||
}
|
||||
|
||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||
}
|
||||
|
||||
user.BadEventFeedbackResync(ctx)
|
||||
|
||||
return nil
|
||||
return user.BadEventFeedbackResync(ctx)
|
||||
}
|
||||
|
||||
if rerr := bridge.reporter.ReportMessageWithContext(
|
||||
@ -524,6 +520,11 @@ func (bridge *Bridge) addUserWithVault(
|
||||
return fmt.Errorf("failed to get Statistics directory: %w", err)
|
||||
}
|
||||
|
||||
syncSettingsPath, err := bridge.locator.ProvideIMAPSyncConfigPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get IMAP sync config path: %w", err)
|
||||
}
|
||||
|
||||
user, err := user.New(
|
||||
ctx,
|
||||
vault,
|
||||
@ -535,16 +536,16 @@ func (bridge *Bridge) addUserWithVault(
|
||||
bridge.vault.GetMaxSyncMemory(),
|
||||
statsPath,
|
||||
bridge,
|
||||
bridge.serverManager,
|
||||
bridge.serverManager,
|
||||
&bridgeEventSubscription{b: bridge},
|
||||
bridge.syncService,
|
||||
syncSettingsPath,
|
||||
)
|
||||
if err != nil {
|
||||
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.
|
||||
// For example, if the user's addresses change, we need to update them in gluon.
|
||||
bridge.tasks.Once(func(ctx context.Context) {
|
||||
@ -554,11 +555,8 @@ func (bridge *Bridge) addUserWithVault(
|
||||
"event": event,
|
||||
}).Debug("Received user event")
|
||||
|
||||
if err := bridge.handleUserEvent(ctx, user, event); err != nil {
|
||||
logrus.WithError(err).Error("Failed to handle user event")
|
||||
} else {
|
||||
bridge.publish(event)
|
||||
}
|
||||
bridge.handleUserEvent(ctx, user, event)
|
||||
bridge.publish(event)
|
||||
})
|
||||
})
|
||||
|
||||
@ -609,10 +607,6 @@ func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI,
|
||||
"withData": withData,
|
||||
}).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 {
|
||||
logrus.WithError(err).Error("Failed to logout user")
|
||||
}
|
||||
|
||||
@ -19,44 +19,17 @@ package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal"
|
||||
"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/internal/vault"
|
||||
"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) {
|
||||
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:
|
||||
bridge.handleUserDeauth(ctx, user)
|
||||
|
||||
@ -66,102 +39,6 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
|
||||
case events.UncategorizedEventError:
|
||||
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) {
|
||||
@ -171,7 +48,7 @@ func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
||||
}, 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() {
|
||||
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
|
||||
"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")
|
||||
}
|
||||
|
||||
user.CancelSyncAndEventPoll()
|
||||
|
||||
// Disable IMAP user
|
||||
if err := bridge.removeIMAPUser(context.Background(), user, false); err != nil {
|
||||
logrus.WithError(err).Error("Failed to remove IMAP user")
|
||||
}
|
||||
user.OnBadEvent(ctx)
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,13 @@
|
||||
|
||||
package events
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/gluon/watcher"
|
||||
)
|
||||
|
||||
type Event interface {
|
||||
fmt.Stringer
|
||||
@ -28,3 +34,30 @@ type Event interface {
|
||||
type eventBase struct{}
|
||||
|
||||
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{}
|
||||
}
|
||||
|
||||
48
internal/events/mocks/mocks.go
Normal file
48
internal/events/mocks/mocks.go
Normal 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)
|
||||
}
|
||||
@ -37,6 +37,22 @@ func (event IMAPServerStopped) String() string {
|
||||
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 {
|
||||
eventBase
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// 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
|
||||
package files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -24,7 +24,7 @@ import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func moveDir(from, to string) error {
|
||||
func MoveDir(from, to string) error {
|
||||
entries, err := os.ReadDir(from)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -36,7 +36,7 @@ func moveDir(from, to string) error {
|
||||
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
|
||||
}
|
||||
|
||||
@ -61,12 +61,12 @@ func moveFile(from, to string) error {
|
||||
return os.Rename(from, to)
|
||||
}
|
||||
|
||||
func copyDir(from, to string) error {
|
||||
func CopyDir(from, to string) error {
|
||||
entries, err := os.ReadDir(from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := createIfNotExists(to, 0o700); err != nil {
|
||||
if err := CreateIfNotExists(to, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
@ -74,11 +74,11 @@ func copyDir(from, to string) error {
|
||||
destPath := filepath.Join(to, entry.Name())
|
||||
|
||||
if entry.IsDir() {
|
||||
if err := copyDir(sourcePath, destPath); err != nil {
|
||||
if err := CopyDir(sourcePath, destPath); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := copyFile(sourcePath, destPath); err != nil {
|
||||
if err := CopyFile(sourcePath, destPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -86,7 +86,7 @@ func copyDir(from, to string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(srcFile, dstFile string) error {
|
||||
func CopyFile(srcFile, dstFile string) error {
|
||||
out, err := os.Create(filepath.Clean(dstFile))
|
||||
defer func(out *os.File) {
|
||||
_ = out.Close()
|
||||
@ -113,7 +113,7 @@ func copyFile(srcFile, dstFile string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func exists(filePath string) bool {
|
||||
func Exists(filePath string) bool {
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
@ -121,8 +121,8 @@ func exists(filePath string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func createIfNotExists(dir string, perm os.FileMode) error {
|
||||
if exists(dir) {
|
||||
func CreateIfNotExists(dir string, perm os.FileMode) error {
|
||||
if Exists(dir) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// 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
|
||||
package files
|
||||
|
||||
import (
|
||||
"os"
|
||||
@ -41,7 +41,7 @@ func TestMoveDir(t *testing.T) {
|
||||
}
|
||||
|
||||
// Move the files.
|
||||
if err := moveDir(from, to); err != nil {
|
||||
if err := MoveDir(from, to); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@ -385,6 +385,14 @@ Status GRPCService::Login(ServerContext *, LoginRequest const *request, Empty *)
|
||||
app().log().debug(__FUNCTION__);
|
||||
UsersTab &usersTab = app().mainWindow().usersTab();
|
||||
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()) {
|
||||
qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::USERNAME_PASSWORD_ERROR, usersTab.usernamePasswordErrorMessage()));
|
||||
return Status::OK;
|
||||
@ -826,7 +834,7 @@ bool GRPCService::sendEvent(SPStreamEvent const &event) {
|
||||
//****************************************************************************************************************************************************
|
||||
void GRPCService::finishLogin() {
|
||||
UsersTab &usersTab = app().mainWindow().usersTab();
|
||||
SPUser user = usersTab.userWithUsername(loginUsername_);
|
||||
SPUser user = usersTab.userWithUsernameOrEmail(loginUsername_);
|
||||
bool const alreadyExist = user.get();
|
||||
if (!user) {
|
||||
user = randomUser();
|
||||
|
||||
@ -272,8 +272,8 @@ bridgepp::SPUser UsersTab::userWithID(QString const &userID) {
|
||||
/// \return The user with the given username.
|
||||
/// \return A null pointer if the user is not in the list.
|
||||
//****************************************************************************************************************************************************
|
||||
bridgepp::SPUser UsersTab::userWithUsername(QString const &username) {
|
||||
return users_.userWithUsername(username);
|
||||
bridgepp::SPUser UsersTab::userWithUsernameOrEmail(QString const &username) {
|
||||
return users_.userWithUsernameOrEmail(username);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -38,7 +38,7 @@ public: // member functions.
|
||||
UsersTab &operator=(UsersTab &&) = delete; ///< Disabled move assignment operator.
|
||||
UserTable &userTable(); ///< Returns a reference to the user table.
|
||||
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 nextUserFreeUserError() const; ///< Check if next user login should trigger a Free user error.
|
||||
bool nextUserTFARequired() const; ///< Check if next user login should requires 2FA.
|
||||
|
||||
@ -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 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 {
|
||||
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;
|
||||
|
||||
@ -40,7 +40,7 @@ public: // member functions.
|
||||
void append(bridgepp::SPUser const &user); ///< Append a user.
|
||||
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 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.
|
||||
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).
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
#include "QMLBackend.h"
|
||||
#include "SentryUtils.h"
|
||||
#include "Settings.h"
|
||||
#include <bridgepp/CLI/CLIUtils.h>
|
||||
#include <bridgepp/GRPC/GRPCClient.h>
|
||||
#include <bridgepp/Exception/Exception.h>
|
||||
#include <bridgepp/ProcessMonitor.h>
|
||||
@ -101,19 +102,23 @@ void AppController::onFatalError(Exception const &exception) {
|
||||
qApp->exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] isCrashing Is the restart triggered by a crash.
|
||||
//****************************************************************************************************************************************************
|
||||
void AppController::restart(bool isCrashing) {
|
||||
if (!launcher_.isEmpty()) {
|
||||
QProcess p;
|
||||
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();
|
||||
if (launcher_.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -37,6 +37,15 @@
|
||||
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->setUseSSLForSMTP(sslForSMTP);
|
||||
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.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -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.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -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] address The email address.
|
||||
/// \param[in] emailClient The email client.
|
||||
/// \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(
|
||||
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::reportBugFinished, this, &QMLBackend::reportBugFinished);
|
||||
connect(client, &GRPCClient::reportBugSuccess, this, &QMLBackend::bugReportSendSuccess);
|
||||
connect(client, &GRPCClient::reportBugFallback, this, &QMLBackend::bugReportSendFallback);
|
||||
connect(client, &GRPCClient::reportBugError, this, &QMLBackend::bugReportSendError);
|
||||
connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); });
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
#include "BuildConfig.h"
|
||||
#include "TrayIcon.h"
|
||||
#include "UserList.h"
|
||||
#include <bridgepp/BugReportFlow/BugReportFlow.h>
|
||||
#include <bridgepp/GRPC/GRPCClient.h>
|
||||
#include <bridgepp/GRPC/GRPCUtils.h>
|
||||
#include <bridgepp/Worker/Overseer.h>
|
||||
@ -51,12 +52,18 @@ public: // member functions.
|
||||
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).
|
||||
|
||||
// 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 QPoint getCursorPos() const; ///< Retrieve the cursor position.
|
||||
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 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)
|
||||
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(QStringList availableKeychain READ availableKeychain NOTIFY availableKeychainChanged)
|
||||
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(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.
|
||||
QStringList availableKeychain() const; ///< Getter for the 'availableKeychain' 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.
|
||||
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 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 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 hostnameChanged(QString const &hostname); ///<Signal for the change of the 'hostname' 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 installUpdate() const; ///< Slot for the update install.
|
||||
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 onResetFinished(); ///< Slot for the reset finish 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 reportBugFinished(); ///< Signal for the 'reportBugFinished' 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 showMainWindow(); ///< Signal for the 'showMainWindow' 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?
|
||||
QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'.
|
||||
std::unique_ptr<TrayIcon> trayIcon_; ///< The tray icon for the application.
|
||||
bridgepp::BugReportFlow reportFlow_; ///< The bug report flow.
|
||||
friend class AppController;
|
||||
};
|
||||
|
||||
|
||||
@ -6,7 +6,11 @@
|
||||
<file>qml/Banner.qml</file>
|
||||
<file>qml/Bridge.qml</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/CategoryItem.qml</file>
|
||||
<file>qml/Configuration.qml</file>
|
||||
<file>qml/ConfigurationItem.qml</file>
|
||||
<file>qml/ContentWrapper.qml</file>
|
||||
@ -32,6 +36,7 @@
|
||||
<file>qml/icons/ic-eye-slash.svg</file>
|
||||
<file>qml/icons/ic-eye.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.svg</file>
|
||||
<file>qml/icons/ic-microsoft-outlook.svg</file>
|
||||
@ -95,6 +100,8 @@
|
||||
<file>qml/Proton/TextArea.qml</file>
|
||||
<file>qml/Proton/TextField.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/SettingsView.qml</file>
|
||||
<file>qml/SetupGuide.qml</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)
|
||||
connect(this, &TrayIcon::messageClicked, []() { app().backend().showMainWindow("tray icon popup notification clicked"); });
|
||||
}
|
||||
|
||||
this->setIcon();
|
||||
this->show();
|
||||
|
||||
// TrayIcon does not expose its screen, so we connect relevant screen events to our DPI change handler.
|
||||
|
||||
@ -63,7 +63,7 @@ BRIDGE_BUILD_ENV= ${BRIDGE_BUILD_ENV:-"dev"}
|
||||
git submodule update --init --recursive ${VCPKG_ROOT}
|
||||
check_exit "Failed to initialize vcpkg as a submodule."
|
||||
|
||||
echo submodule udpated
|
||||
echo submodule updated
|
||||
|
||||
VCPKG_EXE="${VCPKG_ROOT}/vcpkg"
|
||||
VCPKG_BOOTSTRAP="${VCPKG_ROOT}/bootstrap-vcpkg.sh"
|
||||
|
||||
@ -137,12 +137,21 @@ bool checkSingleInstance(QLockFile &lock) {
|
||||
if (lock.getLockInfo(&pid, &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));
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@ -1,251 +1,252 @@
|
||||
// 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 ColorScheme colorScheme
|
||||
property var user
|
||||
enum ViewType {
|
||||
SmallView,
|
||||
LargeView
|
||||
}
|
||||
|
||||
property var _spacing: 12 * ProtonStyle.px
|
||||
|
||||
property color progressColor : {
|
||||
if (!root.enabled) return root.colorScheme.text_weak
|
||||
if (root.type == AccountDelegate.SmallView) return root.colorScheme.text_weak
|
||||
if (root.user && root.user.isSyncing) 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 ColorScheme colorScheme
|
||||
property color progressColor: {
|
||||
if (!root.enabled)
|
||||
return root.colorScheme.text_weak;
|
||||
if (root.type === AccountDelegate.SmallView)
|
||||
return root.colorScheme.text_weak;
|
||||
if (root.user && root.user.isSyncing)
|
||||
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: {
|
||||
if (!root.user)
|
||||
return 0
|
||||
return root.user.isSyncing ? root.user.syncProgress : reasonableFraction(root.user.usedBytes, root.user.totalBytes)
|
||||
return 0;
|
||||
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 var type: AccountDelegate.SmallView
|
||||
property string usedSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.usedBytes) : 0)
|
||||
|
||||
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]
|
||||
}
|
||||
property var user
|
||||
|
||||
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
|
||||
implicitHeight : children[0].implicitHeight
|
||||
|
||||
enum ViewType{
|
||||
SmallView, LargeView
|
||||
}
|
||||
property var type : AccountDelegate.SmallView
|
||||
implicitHeight: children[0].implicitHeight
|
||||
|
||||
RowLayout {
|
||||
spacing: root._spacing
|
||||
|
||||
anchors {
|
||||
top: root.top
|
||||
left: root.left
|
||||
right: root.right
|
||||
top: root.top
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: avatar
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: height
|
||||
|
||||
color: root.colorScheme.background_avatar
|
||||
radius: ProtonStyle.avatar_radius
|
||||
|
||||
color: root.colorScheme.background_avatar
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
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: {
|
||||
switch (root.type) {
|
||||
case AccountDelegate.SmallView: return Label.Body
|
||||
case AccountDelegate.LargeView: return Label.Title
|
||||
case AccountDelegate.SmallView:
|
||||
return Label.Body;
|
||||
case AccountDelegate.LargeView:
|
||||
return Label.Title;
|
||||
}
|
||||
}
|
||||
font.weight: Font.Normal
|
||||
color: "#FFFFFF"
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: account
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
Label {
|
||||
id: labelEmail
|
||||
Layout.maximumWidth: root.width - (root._spacing + avatar.width)
|
||||
colorScheme: root.colorScheme
|
||||
elide: Text.ElideMiddle
|
||||
text: primaryEmail()
|
||||
type: {
|
||||
switch (root.type) {
|
||||
case AccountDelegate.SmallView: return Label.Body
|
||||
case AccountDelegate.LargeView: return Label.Title
|
||||
case AccountDelegate.SmallView:
|
||||
return Label.Body;
|
||||
case AccountDelegate.LargeView:
|
||||
return Label.Title;
|
||||
}
|
||||
}
|
||||
elide: Text.ElideMiddle
|
||||
|
||||
MouseArea {
|
||||
id: labelArea
|
||||
anchors.fill:parent
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
ToolTip {
|
||||
id: toolTipEmail
|
||||
visible: labelArea.containsMouse && labelEmail.truncated
|
||||
text: primaryEmail()
|
||||
delay: 1000
|
||||
text: primaryEmail()
|
||||
visible: labelArea.containsMouse && labelEmail.truncated
|
||||
|
||||
background: Rectangle {
|
||||
border.color: root.colorScheme.background_strong
|
||||
color: root.colorScheme.background_norm
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
color: root.colorScheme.text_norm
|
||||
text: toolTipEmail.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { implicitHeight: root.type == AccountDelegate.LargeView ? 6 * ProtonStyle.px : 0 }
|
||||
|
||||
Item {
|
||||
implicitHeight: root.type === AccountDelegate.LargeView ? 6 * ProtonStyle.px : 0
|
||||
}
|
||||
RowLayout {
|
||||
spacing: 0
|
||||
|
||||
Label {
|
||||
color: root.progressColor
|
||||
colorScheme: root.colorScheme
|
||||
text: {
|
||||
if (!root.user)
|
||||
return qsTr("Signed out")
|
||||
return qsTr("Signed out");
|
||||
switch (root.user.state) {
|
||||
case EUserState.SignedOut:
|
||||
default:
|
||||
return qsTr("Signed out")
|
||||
return qsTr("Signed out");
|
||||
case EUserState.Locked:
|
||||
return qsTr("Connecting") + dotsTimer.dots
|
||||
return qsTr("Connecting") + dotsTimer.dots;
|
||||
case EUserState.Connected:
|
||||
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
|
||||
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: {
|
||||
switch (root.type) {
|
||||
case AccountDelegate.SmallView: return Label.Caption
|
||||
case AccountDelegate.LargeView: return Label.Body
|
||||
case AccountDelegate.SmallView:
|
||||
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 {
|
||||
colorScheme: root.colorScheme
|
||||
text: root.user && root.user.state == EUserState.Connected && !root.user.isSyncing ? " / " + root.totalSpace : ""
|
||||
color: root.colorScheme.text_weak
|
||||
colorScheme: root.colorScheme
|
||||
text: root.user && root.user.state === EUserState.Connected && !root.user.isSyncing ? " / " + root.totalSpace : ""
|
||||
type: {
|
||||
switch (root.type) {
|
||||
case AccountDelegate.SmallView: return Label.Caption
|
||||
case AccountDelegate.LargeView: return Label.Body
|
||||
case AccountDelegate.SmallView:
|
||||
return Label.Caption;
|
||||
case AccountDelegate.LargeView:
|
||||
return Label.Body;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { implicitHeight: root.type == AccountDelegate.LargeView ? 3 * ProtonStyle.px : 0 }
|
||||
|
||||
Item {
|
||||
implicitHeight: root.type === AccountDelegate.LargeView ? 3 * ProtonStyle.px : 0
|
||||
}
|
||||
Rectangle {
|
||||
id: progress_bar
|
||||
visible: root.user ? root.type == AccountDelegate.LargeView : false
|
||||
width: 140 * ProtonStyle.px
|
||||
color: root.colorScheme.border_weak
|
||||
height: 4 * ProtonStyle.px
|
||||
radius: ProtonStyle.progress_bar_radius
|
||||
color: root.colorScheme.border_weak
|
||||
visible: root.user ? root.type === AccountDelegate.LargeView : false
|
||||
width: 140 * ProtonStyle.px
|
||||
|
||||
Rectangle {
|
||||
id: progress_bar_filled
|
||||
radius: ProtonStyle.progress_bar_radius
|
||||
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 {
|
||||
top : parent.top
|
||||
bottom : parent.bottom
|
||||
left : parent.left
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
}
|
||||
width: Math.min(1,Math.max(0.02,root.progressRatio)) * parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
@ -1,42 +1,35 @@
|
||||
// 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 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 var notifications
|
||||
property var user
|
||||
|
||||
signal showSignIn
|
||||
|
||||
signal showSetupGuide(var user, string address)
|
||||
|
||||
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
|
||||
signal showSignIn
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
@ -45,6 +38,7 @@ Item {
|
||||
ScrollView {
|
||||
id: scrollView
|
||||
anchors.fill: parent
|
||||
|
||||
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds
|
||||
|
||||
ColumnLayout {
|
||||
@ -54,16 +48,16 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
id: topArea
|
||||
color: root.colorScheme.background_norm
|
||||
clip: true
|
||||
Layout.fillWidth: true
|
||||
clip: true
|
||||
color: root.colorScheme.background_norm
|
||||
implicitHeight: childrenRect.height
|
||||
|
||||
ColumnLayout {
|
||||
id: topLayout
|
||||
width: _contentWidth
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: _spacing
|
||||
width: _contentWidth
|
||||
|
||||
RowLayout {
|
||||
// account delegate with action buttons
|
||||
@ -73,83 +67,82 @@ Item {
|
||||
AccountDelegate {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
user: root.user
|
||||
type: AccountDelegate.LargeView
|
||||
enabled: _connected
|
||||
type: AccountDelegate.LargeView
|
||||
user: root.user
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Sign out")
|
||||
secondary: true
|
||||
text: qsTr("Sign out")
|
||||
visible: _connected
|
||||
|
||||
onClicked: {
|
||||
if (!root.user)
|
||||
return;
|
||||
root.user.logout();
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Sign in")
|
||||
secondary: true
|
||||
text: qsTr("Sign in")
|
||||
visible: root.user ? (root.user.state === EUserState.SignedOut) : false
|
||||
|
||||
onClicked: {
|
||||
if (!root.user)
|
||||
return;
|
||||
root.showSignIn();
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
colorScheme: root.colorScheme
|
||||
icon.source: "/qml/icons/ic-trash.svg"
|
||||
secondary: true
|
||||
visible: root.user ? root.user.state !== EUserState.Locked : false
|
||||
|
||||
onClicked: {
|
||||
if (!root.user)
|
||||
return;
|
||||
root.notifications.askDeleteAccount(root.user);
|
||||
}
|
||||
visible: root.user ? root.user.state !== EUserState.Locked : false
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: root._lineThickness
|
||||
color: root.colorScheme.border_weak
|
||||
height: root._lineThickness
|
||||
}
|
||||
|
||||
SettingsItem {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Email clients")
|
||||
Layout.fillWidth: true
|
||||
actionText: qsTr("Configure")
|
||||
colorScheme: root.colorScheme
|
||||
description: qsTr("Using the mailbox details below (re)configure your client.")
|
||||
showSeparator: splitMode.visible
|
||||
text: qsTr("Email clients")
|
||||
type: SettingsItem.Button
|
||||
visible: _connected && (!root.user.splitMode) || (root.user.addresses.length === 1)
|
||||
showSeparator: splitMode.visible
|
||||
|
||||
onClicked: {
|
||||
if (!root.user)
|
||||
return;
|
||||
root.showSetupGuide(root.user, user.addresses[0]);
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SettingsItem {
|
||||
id: splitMode
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Split addresses")
|
||||
description: qsTr("Setup multiple email addresses individually.")
|
||||
type: SettingsItem.Toggle
|
||||
Layout.fillWidth: true
|
||||
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
|
||||
text: qsTr("Split addresses")
|
||||
type: SettingsItem.Toggle
|
||||
visible: _connected && root.user.addresses.length > 1
|
||||
|
||||
onClicked: {
|
||||
if (!splitMode.checked) {
|
||||
root.notifications.askEnableSplitMode(user);
|
||||
@ -158,26 +151,23 @@ Item {
|
||||
root.user.toggleSplitMode(!splitMode.checked);
|
||||
}
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: _spacing
|
||||
Layout.fillWidth: true
|
||||
visible: _connected && root.user.splitMode
|
||||
|
||||
ComboBox {
|
||||
id: addressSelector
|
||||
colorScheme: root.colorScheme
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
model: root.user ? root.user.addresses : null
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Configure")
|
||||
secondary: true
|
||||
text: qsTr("Configure")
|
||||
|
||||
onClicked: {
|
||||
if (!root.user)
|
||||
return;
|
||||
@ -185,25 +175,23 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 0
|
||||
} // just for some extra space before separator
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: bottomArea
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: bottomLayout.implicitHeight
|
||||
color: root.colorScheme.background_weak
|
||||
implicitHeight: bottomLayout.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: bottomLayout
|
||||
width: _contentWidth
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: _spacing
|
||||
visible: _connected
|
||||
width: _contentWidth
|
||||
|
||||
Label {
|
||||
Layout.topMargin: _detailsMargin
|
||||
@ -211,35 +199,34 @@ Item {
|
||||
text: qsTr("Mailbox details")
|
||||
type: Label.Body_semibold
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: configuration
|
||||
spacing: _spacing
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
property string currentAddress: addressSelector.displayText
|
||||
|
||||
Configuration {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
title: qsTr("IMAP")
|
||||
hostname: Backend.hostname
|
||||
port: Backend.imapPort.toString()
|
||||
username: configuration.currentAddress
|
||||
password: root.user ? root.user.password : ""
|
||||
security: Backend.useSSLForIMAP ? "SSL" : "STARTTLS"
|
||||
}
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
spacing: _spacing
|
||||
|
||||
Configuration {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
title: qsTr("SMTP")
|
||||
hostname: Backend.hostname
|
||||
port: Backend.smtpPort.toString()
|
||||
username: configuration.currentAddress
|
||||
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"
|
||||
title: qsTr("SMTP")
|
||||
username: configuration.currentAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,25 +1,19 @@
|
||||
// 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 QtQuick.Controls.impl
|
||||
|
||||
import Proton
|
||||
import Notifications
|
||||
|
||||
@ -27,34 +21,28 @@ Popup {
|
||||
id: root
|
||||
|
||||
property ColorScheme colorScheme
|
||||
property Notification notification
|
||||
property var mainWindow
|
||||
|
||||
topMargin: 37
|
||||
leftMargin: (mainWindow.width - root.implicitWidth)/2
|
||||
property Notification notification
|
||||
|
||||
implicitHeight: contentLayout.implicitHeight + contentLayout.anchors.topMargin + contentLayout.anchors.bottomMargin
|
||||
implicitWidth: 600 // contentLayout.implicitWidth + contentLayout.anchors.leftMargin + contentLayout.anchors.rightMargin
|
||||
|
||||
popupType: ApplicationWindow.PopupType.Banner
|
||||
|
||||
shouldShow: notification ? (notification.active && !notification.dismissed) : false
|
||||
|
||||
leftMargin: (mainWindow.width - root.implicitWidth) / 2
|
||||
modal: false
|
||||
popupType: ApplicationWindow.PopupType.Banner
|
||||
shouldShow: notification ? (notification.active && !notification.dismissed) : false
|
||||
topMargin: 37
|
||||
|
||||
Action {
|
||||
id: defaultDismissAction
|
||||
|
||||
text: qsTr("OK")
|
||||
|
||||
onTriggered: {
|
||||
if (!root.notification) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
root.notification.dismissed = true
|
||||
root.notification.dismissed = true;
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: contentLayout
|
||||
anchors.fill: parent
|
||||
@ -63,170 +51,148 @@ Popup {
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
clip: true
|
||||
implicitHeight: children[1].implicitHeight + children[1].anchors.topMargin + children[1].anchors.bottomMargin
|
||||
implicitWidth: children[1].implicitWidth + children[1].anchors.leftMargin + children[1].anchors.rightMargin
|
||||
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
width: parent.width + 10
|
||||
radius: ProtonStyle.banner_radius
|
||||
anchors.top: parent.top
|
||||
color: {
|
||||
if (!root.notification) {
|
||||
return "transparent"
|
||||
return "transparent";
|
||||
}
|
||||
|
||||
switch (root.notification.type) {
|
||||
case Notification.NotificationType.Info:
|
||||
return root.colorScheme.signal_info
|
||||
case Notification.NotificationType.Success:
|
||||
return root.colorScheme.signal_success
|
||||
case Notification.NotificationType.Warning:
|
||||
return root.colorScheme.signal_warning
|
||||
case Notification.NotificationType.Danger:
|
||||
return root.colorScheme.signal_danger
|
||||
case Notification.NotificationType.Info:
|
||||
return root.colorScheme.signal_info;
|
||||
case Notification.NotificationType.Success:
|
||||
return root.colorScheme.signal_success;
|
||||
case Notification.NotificationType.Warning:
|
||||
return root.colorScheme.signal_warning;
|
||||
case Notification.NotificationType.Danger:
|
||||
return root.colorScheme.signal_danger;
|
||||
}
|
||||
}
|
||||
radius: ProtonStyle.banner_radius
|
||||
width: parent.width + 10
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
anchors.topMargin: 14
|
||||
anchors.bottomMargin: 14
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 16
|
||||
|
||||
anchors.topMargin: 14
|
||||
spacing: 8
|
||||
|
||||
ColorImage {
|
||||
color: root.colorScheme.text_invert
|
||||
width: 24
|
||||
height: 24
|
||||
|
||||
sourceSize.width: 24
|
||||
sourceSize.height: 24
|
||||
|
||||
Layout.preferredHeight: 24
|
||||
Layout.preferredWidth: 24
|
||||
|
||||
color: root.colorScheme.text_invert
|
||||
height: 24
|
||||
source: {
|
||||
if (!root.notification) {
|
||||
return ""
|
||||
return "";
|
||||
}
|
||||
|
||||
switch (root.notification.type) {
|
||||
case Notification.NotificationType.Info:
|
||||
return "/qml/icons/ic-info-circle-filled.svg"
|
||||
case Notification.NotificationType.Success:
|
||||
return "/qml/icons/ic-info-circle-filled.svg"
|
||||
case Notification.NotificationType.Warning:
|
||||
return "/qml/icons/ic-exclamation-circle-filled.svg"
|
||||
case Notification.NotificationType.Danger:
|
||||
return "/qml/icons/ic-exclamation-circle-filled.svg"
|
||||
case Notification.NotificationType.Info:
|
||||
return "/qml/icons/ic-info-circle-filled.svg";
|
||||
case Notification.NotificationType.Success:
|
||||
return "/qml/icons/ic-info-circle-filled.svg";
|
||||
case Notification.NotificationType.Warning:
|
||||
return "/qml/icons/ic-exclamation-circle-filled.svg";
|
||||
case Notification.NotificationType.Danger:
|
||||
return "/qml/icons/ic-exclamation-circle-filled.svg";
|
||||
}
|
||||
}
|
||||
sourceSize.height: 24
|
||||
sourceSize.width: 24
|
||||
width: 24
|
||||
}
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
|
||||
color: root.colorScheme.text_invert
|
||||
colorScheme: root.colorScheme
|
||||
text: root.notification ? root.notification.description : ""
|
||||
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
width: 1
|
||||
color: {
|
||||
if (!root.notification) {
|
||||
return "transparent"
|
||||
return "transparent";
|
||||
}
|
||||
|
||||
switch (root.notification.type) {
|
||||
case Notification.NotificationType.Info:
|
||||
return root.colorScheme.signal_info_active
|
||||
case Notification.NotificationType.Success:
|
||||
return root.colorScheme.signal_success_active
|
||||
case Notification.NotificationType.Warning:
|
||||
return root.colorScheme.signal_warning_active
|
||||
case Notification.NotificationType.Danger:
|
||||
return root.colorScheme.signal_danger_active
|
||||
case Notification.NotificationType.Info:
|
||||
return root.colorScheme.signal_info_active;
|
||||
case Notification.NotificationType.Success:
|
||||
return root.colorScheme.signal_success_active;
|
||||
case Notification.NotificationType.Warning:
|
||||
return root.colorScheme.signal_warning_active;
|
||||
case Notification.NotificationType.Danger:
|
||||
return root.colorScheme.signal_danger_active;
|
||||
}
|
||||
}
|
||||
width: 1
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
Layout.fillHeight: true
|
||||
|
||||
id: actionButton
|
||||
|
||||
Layout.fillHeight: true
|
||||
action: (root.notification && root.notification.action.length > 0) ? root.notification.action[0] : defaultDismissAction
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
background: Item {
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
width: parent.width + 10
|
||||
radius: ProtonStyle.banner_radius
|
||||
anchors.top: parent.top
|
||||
color: {
|
||||
if (!root.notification) {
|
||||
return "transparent"
|
||||
return "transparent";
|
||||
}
|
||||
|
||||
var norm
|
||||
var hover
|
||||
var active
|
||||
|
||||
let norm;
|
||||
let hover;
|
||||
let active;
|
||||
switch (root.notification.type) {
|
||||
case Notification.NotificationType.Info:
|
||||
norm = root.colorScheme.signal_info
|
||||
hover = root.colorScheme.signal_info_hover
|
||||
active = root.colorScheme.signal_info_active
|
||||
case Notification.NotificationType.Info:
|
||||
norm = root.colorScheme.signal_info;
|
||||
hover = root.colorScheme.signal_info_hover;
|
||||
active = root.colorScheme.signal_info_active;
|
||||
break;
|
||||
case Notification.NotificationType.Success:
|
||||
norm = root.colorScheme.signal_success
|
||||
hover = root.colorScheme.signal_success_hover
|
||||
active = root.colorScheme.signal_success_active
|
||||
case Notification.NotificationType.Success:
|
||||
norm = root.colorScheme.signal_success;
|
||||
hover = root.colorScheme.signal_success_hover;
|
||||
active = root.colorScheme.signal_success_active;
|
||||
break;
|
||||
case Notification.NotificationType.Warning:
|
||||
norm = root.colorScheme.signal_warning
|
||||
hover = root.colorScheme.signal_warning_hover
|
||||
active = root.colorScheme.signal_warning_active
|
||||
case Notification.NotificationType.Warning:
|
||||
norm = root.colorScheme.signal_warning;
|
||||
hover = root.colorScheme.signal_warning_hover;
|
||||
active = root.colorScheme.signal_warning_active;
|
||||
break;
|
||||
case Notification.NotificationType.Danger:
|
||||
norm = root.colorScheme.signal_danger
|
||||
hover = root.colorScheme.signal_danger_hover
|
||||
active = root.colorScheme.signal_danger_active
|
||||
case Notification.NotificationType.Danger:
|
||||
norm = root.colorScheme.signal_danger;
|
||||
hover = root.colorScheme.signal_danger_hover;
|
||||
active = root.colorScheme.signal_danger_active;
|
||||
break;
|
||||
}
|
||||
|
||||
if (actionButton.down) {
|
||||
return active
|
||||
return active;
|
||||
}
|
||||
|
||||
if (actionButton.enabled && (actionButton.highlighted || actionButton.hovered || actionButton.checked)) {
|
||||
return hover
|
||||
return hover;
|
||||
}
|
||||
|
||||
if (actionButton.loading) {
|
||||
return hover
|
||||
return hover;
|
||||
}
|
||||
|
||||
return norm
|
||||
return norm;
|
||||
}
|
||||
radius: ProtonStyle.banner_radius
|
||||
width: parent.width + 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,129 +1,112 @@
|
||||
// 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 QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import Qt.labs.platform
|
||||
|
||||
import Proton
|
||||
import Notifications
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
function bound(num, lowerLimit, upperLimit) {
|
||||
return Math.max(lowerLimit, Math.min(upperLimit, num))
|
||||
property MainWindow _mainWindow: MainWindow {
|
||||
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 {
|
||||
id: notifications
|
||||
frontendMain: mainWindow
|
||||
}
|
||||
|
||||
property NotificationFilter _trayNotificationFilter: NotificationFilter {
|
||||
id: trayNotificationFilter
|
||||
source: root._notifications ? root._notifications.all : undefined
|
||||
onTopmostChanged: {
|
||||
if (topmost) {
|
||||
switch (topmost.type) {
|
||||
case Notification.NotificationType.Danger:
|
||||
Backend.setErrorTrayIcon(topmost.brief, topmost.icon)
|
||||
return
|
||||
case Notification.NotificationType.Warning:
|
||||
Backend.setWarnTrayIcon(topmost.brief, topmost.icon)
|
||||
return
|
||||
case Notification.NotificationType.Info:
|
||||
Backend.setUpdateTrayIcon(topmost.brief, topmost.icon)
|
||||
return
|
||||
id: trayNotificationFilter
|
||||
source: root._notifications ? root._notifications.all : undefined
|
||||
|
||||
onTopmostChanged: {
|
||||
if (topmost) {
|
||||
switch (topmost.type) {
|
||||
case Notification.NotificationType.Danger:
|
||||
Backend.setErrorTrayIcon(topmost.brief, topmost.icon);
|
||||
return;
|
||||
case Notification.NotificationType.Warning:
|
||||
Backend.setWarnTrayIcon(topmost.brief, topmost.icon);
|
||||
return;
|
||||
case Notification.NotificationType.Info:
|
||||
Backend.setUpdateTrayIcon(topmost.brief, topmost.icon);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Backend.setNormalTrayIcon()
|
||||
Backend.setNormalTrayIcon();
|
||||
}
|
||||
}
|
||||
property var title: Backend.appname
|
||||
|
||||
|
||||
property MainWindow _mainWindow: MainWindow {
|
||||
id: mainWindow
|
||||
visible: false
|
||||
|
||||
title: root.title
|
||||
notifications: root._notifications
|
||||
|
||||
onVisibleChanged: {
|
||||
Backend.dockIconVisible = visible
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Backend
|
||||
function onDiskCacheUnavailable() {
|
||||
mainWindow.showAndRise()
|
||||
}
|
||||
function onColorSchemeNameChanged(scheme) { root.setColorScheme() }
|
||||
|
||||
function onHideMainWindow() {
|
||||
mainWindow.hide();
|
||||
}
|
||||
}
|
||||
function bound(num, lowerLimit, upperLimit) {
|
||||
return Math.max(lowerLimit, Math.min(upperLimit, num));
|
||||
}
|
||||
function setColorScheme() {
|
||||
if (Backend.colorSchemeName === "light")
|
||||
ProtonStyle.currentStyle = ProtonStyle.lightStyle;
|
||||
if (Backend.colorSchemeName === "dark")
|
||||
ProtonStyle.currentStyle = ProtonStyle.darkStyle;
|
||||
}
|
||||
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!Backend) {
|
||||
console.log("Backend not loaded")
|
||||
console.log("Backend not loaded");
|
||||
}
|
||||
|
||||
root.setColorScheme()
|
||||
|
||||
|
||||
root.setColorScheme();
|
||||
if (!Backend.users) {
|
||||
console.log("users not loaded")
|
||||
console.log("users not loaded");
|
||||
}
|
||||
|
||||
var c = Backend.users.count
|
||||
var u = Backend.users.get(0)
|
||||
const c = Backend.users.count;
|
||||
const u = Backend.users.get(0);
|
||||
// DEBUG
|
||||
if (c !== 0) {
|
||||
console.log("users non zero", c)
|
||||
console.log("first user", u )
|
||||
console.log("users non zero", c);
|
||||
console.log("first user", u);
|
||||
}
|
||||
|
||||
if (c === 0) {
|
||||
mainWindow.showAndRise()
|
||||
mainWindow.showAndRise();
|
||||
}
|
||||
|
||||
if (u) {
|
||||
if (c === 1 && (u.state === EUserState.SignedOut)) {
|
||||
mainWindow.showAndRise()
|
||||
mainWindow.showAndRise();
|
||||
}
|
||||
}
|
||||
|
||||
Backend.guiReady()
|
||||
|
||||
if (Backend.showOnStartup || Backend.showSplashScreen) {
|
||||
mainWindow.showAndRise()
|
||||
Backend.guiReady();
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
130
internal/frontend/bridge-gui/bridge-gui/qml/BugQuestionView.qml
Normal file
130
internal/frontend/bridge-gui/bridge-gui/qml/BugQuestionView.qml
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,202 +1,161 @@
|
||||
// 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
|
||||
|
||||
fillHeight: true
|
||||
|
||||
property var selectedAddress
|
||||
property var categoryId:-1
|
||||
property string category: Backend.getBugCategory(root.categoryId)
|
||||
|
||||
signal bugReportWasSent()
|
||||
signal bugReportWasSent
|
||||
|
||||
Label {
|
||||
text: qsTr("Report a problem")
|
||||
colorScheme: root.colorScheme
|
||||
type: Label.Heading
|
||||
function isValidEmail(text) {
|
||||
const reEmail = /^[^@]+@[^@]+\.[A-Za-z]+\s*$/;
|
||||
return reEmail.test(text);
|
||||
}
|
||||
|
||||
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 {
|
||||
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.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
|
||||
// want TextArea implicitHeight (which is height of all text)
|
||||
// to be considered in SettingsView internal scroll view
|
||||
implicitHeight: height
|
||||
label: "Your answers to: " + qsTr(root.category);
|
||||
readOnly : true
|
||||
}
|
||||
|
||||
|
||||
TextField {
|
||||
id: address
|
||||
|
||||
label: qsTr("Your contact email")
|
||||
colorScheme: root.colorScheme
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
label: qsTr("Your contact email")
|
||||
placeholderText: qsTr("e.g. jane.doe@protonmail.com")
|
||||
|
||||
validator: function(str) {
|
||||
validator: function (str) {
|
||||
if (!isValidEmail(str)) {
|
||||
return qsTr("Enter valid email address")
|
||||
return qsTr("Enter valid email address");
|
||||
}
|
||||
return
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: emailClient
|
||||
|
||||
label: qsTr("Your email client (including version)")
|
||||
colorScheme: root.colorScheme
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
label: qsTr("Your email client (including version)")
|
||||
placeholderText: qsTr("e.g. Apple Mail 14.0")
|
||||
|
||||
validator: function(str) {
|
||||
validator: function (str) {
|
||||
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 {
|
||||
CheckBox {
|
||||
id: includeLogs
|
||||
text: qsTr("Include my recent logs")
|
||||
colorScheme: root.colorScheme
|
||||
checked: true
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Include my recent logs")
|
||||
}
|
||||
Button {
|
||||
Layout.leftMargin: 12
|
||||
text: qsTr("View logs")
|
||||
secondary: true
|
||||
colorScheme: root.colorScheme
|
||||
secondary: true
|
||||
text: qsTr("View logs")
|
||||
|
||||
onClicked: Qt.openUrlExternally(Backend.logsPath)
|
||||
}
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
text: qsTr("Reports are not end-to-end encrypted, please do not send any sensitive information.")
|
||||
|
||||
readOnly: true
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: root.colorScheme.text_weak
|
||||
font.family: ProtonStyle.font_family
|
||||
font.weight: ProtonStyle.fontWeight_400
|
||||
font.pixelSize: ProtonStyle.caption_font_size
|
||||
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
|
||||
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
|
||||
selectByMouse: true
|
||||
}
|
||||
|
||||
Button {
|
||||
id: sendButton
|
||||
text: qsTr("Send")
|
||||
colorScheme: root.colorScheme
|
||||
enabled: !loading
|
||||
text: qsTr("Send")
|
||||
|
||||
onClicked: {
|
||||
description.validate()
|
||||
address.validate()
|
||||
emailClient.validate()
|
||||
|
||||
description.validate();
|
||||
address.validate();
|
||||
emailClient.validate();
|
||||
if (description.error || address.error || emailClient.error) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
submit()
|
||||
submit();
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onBugReportSendSuccess() {
|
||||
root.bugReportWasSent();
|
||||
}
|
||||
function onReportBugFinished() {
|
||||
sendButton.loading = false;
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
113
internal/frontend/bridge-gui/bridge-gui/qml/CategoryItem.qml
Normal file
113
internal/frontend/bridge-gui/bridge-gui/qml/CategoryItem.qml
Normal 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
|
||||
}
|
||||
}
|
||||
@ -1,71 +1,80 @@
|
||||
// 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 QtQuick.Controls.impl
|
||||
|
||||
import Proton
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property int _margin: 24
|
||||
property ColorScheme colorScheme
|
||||
property string title
|
||||
property string hostname
|
||||
property string port
|
||||
property string username
|
||||
property string password
|
||||
property string port
|
||||
property string security
|
||||
|
||||
implicitWidth: 304
|
||||
implicitHeight: content.height + 2*root._margin
|
||||
property string title
|
||||
property string username
|
||||
|
||||
color: root.colorScheme.background_norm
|
||||
implicitHeight: content.height + 2 * root._margin
|
||||
implicitWidth: 304
|
||||
radius: ProtonStyle.card_radius
|
||||
|
||||
property int _margin: 24
|
||||
|
||||
ColumnLayout {
|
||||
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
|
||||
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 {
|
||||
colorScheme: root.colorScheme
|
||||
text: root.title
|
||||
type: Label.Body_semibold
|
||||
}
|
||||
|
||||
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Hostname") ; value: root.hostname }
|
||||
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 }
|
||||
ConfigurationItem {
|
||||
colorScheme: root.colorScheme
|
||||
label: qsTr("Hostname")
|
||||
value: root.hostname
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,35 +1,29 @@
|
||||
// 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 QtQuick.Controls.impl
|
||||
|
||||
import Proton
|
||||
|
||||
Item {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
|
||||
property var colorScheme
|
||||
property string label
|
||||
property string value
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: children[0].implicitHeight
|
||||
implicitWidth: children[0].implicitWidth
|
||||
|
||||
@ -47,45 +41,42 @@ Item {
|
||||
}
|
||||
TextEdit {
|
||||
id: valueText
|
||||
text: root.value
|
||||
Layout.fillWidth: true
|
||||
color: root.colorScheme.text_weak
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
selectByKeyboard: true
|
||||
selectByMouse: true
|
||||
selectionColor: root.colorScheme.text_weak
|
||||
text: root.value
|
||||
wrapMode: Text.WrapAnywhere
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ColorImage {
|
||||
source: "/qml/icons/ic-copy.svg"
|
||||
color: root.colorScheme.text_norm
|
||||
height: root.colorScheme.body_font_size
|
||||
source: "/qml/icons/ic-copy.svg"
|
||||
sourceSize.height: root.colorScheme.body_font_size
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked : {
|
||||
valueText.select(0, valueText.length)
|
||||
valueText.copy()
|
||||
valueText.deselect()
|
||||
|
||||
onClicked: {
|
||||
valueText.select(0, valueText.length);
|
||||
valueText.copy();
|
||||
valueText.deselect();
|
||||
}
|
||||
onPressed: parent.scale = 0.90
|
||||
onReleased: parent.scale = 1
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: root.colorScheme.border_norm
|
||||
height: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,155 +1,138 @@
|
||||
// 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 QtQuick.Controls.impl
|
||||
|
||||
import Proton
|
||||
|
||||
SettingsView {
|
||||
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
|
||||
|
||||
onVisibleChanged: {
|
||||
root.setDefaultValues();
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Connection mode")
|
||||
type: Label.Heading
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
color: root.colorScheme.text_weak
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Change the protocol Bridge and the email client use to connect for IMAP and SMTP.")
|
||||
type: Label.Body
|
||||
color: root.colorScheme.text_weak
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 16
|
||||
|
||||
ButtonGroup{ id: imapProtocolSelection }
|
||||
|
||||
ButtonGroup {
|
||||
id: imapProtocolSelection
|
||||
}
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("IMAP connection")
|
||||
}
|
||||
|
||||
RadioButton {
|
||||
id: imapSSLButton
|
||||
colorScheme: root.colorScheme
|
||||
ButtonGroup.group: imapProtocolSelection
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("SSL")
|
||||
}
|
||||
|
||||
RadioButton {
|
||||
id: imapSTARTTLSButton
|
||||
colorScheme: root.colorScheme
|
||||
ButtonGroup.group: imapProtocolSelection
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("STARTTLS")
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: root.colorScheme.border_weak
|
||||
height: 1
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 16
|
||||
|
||||
ButtonGroup{ id: smtpProtocolSelection }
|
||||
|
||||
ButtonGroup {
|
||||
id: smtpProtocolSelection
|
||||
}
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("SMTP connection")
|
||||
}
|
||||
|
||||
RadioButton {
|
||||
id: smtpSSLButton
|
||||
colorScheme: root.colorScheme
|
||||
ButtonGroup.group: smtpProtocolSelection
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("SSL")
|
||||
}
|
||||
|
||||
RadioButton {
|
||||
id: smtpSTARTTLSButton
|
||||
colorScheme: root.colorScheme
|
||||
ButtonGroup.group: smtpProtocolSelection
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("STARTTLS")
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: root.colorScheme.border_weak
|
||||
height: 1
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 12
|
||||
|
||||
Button {
|
||||
id: submitButton
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Save")
|
||||
onClicked: {
|
||||
submitButton.loading = true
|
||||
root.submit()
|
||||
}
|
||||
|
||||
enabled: (!loading) && ((imapSSLButton.checked !== Backend.useSSLForIMAP) || (smtpSSLButton.checked !== Backend.useSSLForSMTP))
|
||||
}
|
||||
text: qsTr("Save")
|
||||
|
||||
onClicked: {
|
||||
submitButton.loading = true;
|
||||
root.submit();
|
||||
}
|
||||
}
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Cancel")
|
||||
onClicked: root.back()
|
||||
secondary: true
|
||||
}
|
||||
text: qsTr("Cancel")
|
||||
|
||||
onClicked: root.back()
|
||||
}
|
||||
Connections {
|
||||
target: Backend
|
||||
|
||||
function onChangeMailServerSettingsFinished() {
|
||||
submitButton.loading = false
|
||||
root.back()
|
||||
submitButton.loading = false;
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,36 +1,58 @@
|
||||
// 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 ColorScheme colorScheme
|
||||
property var notifications
|
||||
|
||||
signal closeWindow
|
||||
signal quitBridge
|
||||
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 {
|
||||
anchors.fill: parent
|
||||
@ -38,13 +60,13 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
id: leftBar
|
||||
|
||||
property ColorScheme colorScheme: root.colorScheme.prominent
|
||||
|
||||
Layout.minimumWidth: 264
|
||||
Layout.maximumWidth: 320
|
||||
Layout.preferredWidth: 320
|
||||
Layout.fillHeight: true
|
||||
|
||||
Layout.maximumWidth: 320
|
||||
Layout.minimumWidth: 264
|
||||
Layout.preferredWidth: 320
|
||||
color: colorScheme.background_norm
|
||||
|
||||
ColumnLayout {
|
||||
@ -52,24 +74,21 @@ Item {
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
id:topLeftBar
|
||||
|
||||
id: topLeftBar
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: 60
|
||||
Layout.maximumHeight: 60
|
||||
Layout.minimumHeight: 60
|
||||
Layout.preferredHeight: 60
|
||||
spacing: 0
|
||||
|
||||
Status {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: 17
|
||||
Layout.leftMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 17
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
colorScheme: leftBar.colorScheme
|
||||
notifications: root.notifications
|
||||
|
||||
notificationWhitelist: Notifications.Group.Connection | Notifications.Group.ForceUpdate
|
||||
notifications: root.notifications
|
||||
}
|
||||
|
||||
// just a placeholder
|
||||
@ -77,47 +96,38 @@ Item {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
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.maximumHeight: 36
|
||||
Layout.maximumWidth: 36
|
||||
Layout.minimumHeight: 36
|
||||
Layout.minimumWidth: 36
|
||||
Layout.preferredHeight: 36
|
||||
Layout.preferredWidth: 36
|
||||
Layout.rightMargin: 4
|
||||
|
||||
Layout.topMargin: 16
|
||||
colorScheme: leftBar.colorScheme
|
||||
horizontalPadding: 0
|
||||
|
||||
icon.source: "/qml/icons/ic-question-circle.svg"
|
||||
|
||||
onClicked: rightContent.showHelpView()
|
||||
}
|
||||
|
||||
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.maximumHeight: 36
|
||||
Layout.maximumWidth: 36
|
||||
Layout.minimumHeight: 36
|
||||
Layout.minimumWidth: 36
|
||||
Layout.preferredHeight: 36
|
||||
Layout.preferredWidth: 36
|
||||
Layout.rightMargin: 4
|
||||
|
||||
Layout.topMargin: 16
|
||||
colorScheme: leftBar.colorScheme
|
||||
horizontalPadding: 0
|
||||
|
||||
icon.source: "/qml/icons/ic-cog-wheel.svg"
|
||||
|
||||
onClicked: rightContent.showGeneralSettings()
|
||||
}
|
||||
|
||||
Button {
|
||||
id: dotMenuButton
|
||||
Layout.bottomMargin: 9
|
||||
@ -134,7 +144,7 @@ Item {
|
||||
icon.source: "/qml/icons/ic-three-dots-vertical.svg"
|
||||
|
||||
onClicked: {
|
||||
dotMenu.open()
|
||||
dotMenu.open();
|
||||
}
|
||||
|
||||
Menu {
|
||||
@ -143,332 +153,319 @@ Item {
|
||||
modal: true
|
||||
y: dotMenuButton.Layout.preferredHeight + dotMenuButton.Layout.bottomMargin
|
||||
|
||||
onClosed: {
|
||||
parent.checked = false;
|
||||
}
|
||||
onOpened: {
|
||||
parent.checked = true;
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Close window")
|
||||
|
||||
onClicked: {
|
||||
root.closeWindow()
|
||||
root.closeWindow();
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Quit Bridge")
|
||||
onClicked: {
|
||||
root.quitBridge()
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
parent.checked = false
|
||||
}
|
||||
onOpened: {
|
||||
parent.checked = true
|
||||
onClicked: {
|
||||
root.quitBridge();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {implicitHeight:10}
|
||||
Item {
|
||||
implicitHeight: 10
|
||||
}
|
||||
|
||||
// Separator line
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: 1
|
||||
Layout.maximumHeight: 1
|
||||
Layout.minimumHeight: 1
|
||||
color: leftBar.colorScheme.border_weak
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: accounts
|
||||
|
||||
property var _topBottomMargins: 24
|
||||
property var _leftRightMargins: 16
|
||||
property var _topBottomMargins: 24
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: accounts._topBottomMargins
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: accounts._leftRightMargins
|
||||
Layout.rightMargin: accounts._leftRightMargins
|
||||
Layout.topMargin: accounts._topBottomMargins
|
||||
Layout.bottomMargin: accounts._topBottomMargins
|
||||
|
||||
spacing: 12
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
clip: true
|
||||
model: Backend.users
|
||||
spacing: 12
|
||||
|
||||
header: Rectangle {
|
||||
height: headerLabel.height+16
|
||||
// color: ProtonStyle.transparent
|
||||
Label{
|
||||
delegate: Item {
|
||||
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
|
||||
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
|
||||
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
|
||||
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
|
||||
colorScheme: leftBar.colorScheme
|
||||
text: qsTr("Accounts")
|
||||
type: Label.LabelType.Body
|
||||
}
|
||||
}
|
||||
|
||||
highlight: Rectangle {
|
||||
color: leftBar.colorScheme.interaction_default_active
|
||||
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
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: 1
|
||||
Layout.maximumHeight: 1
|
||||
Layout.minimumHeight: 1
|
||||
color: leftBar.colorScheme.border_weak
|
||||
}
|
||||
|
||||
Item {
|
||||
id: bottomLeftBar
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: 52
|
||||
Layout.maximumHeight: 52
|
||||
Layout.minimumHeight: 52
|
||||
Layout.preferredHeight: 52
|
||||
|
||||
Button {
|
||||
colorScheme: leftBar.colorScheme
|
||||
width: 36
|
||||
height: 36
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
|
||||
anchors.leftMargin: 16
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 7
|
||||
|
||||
colorScheme: leftBar.colorScheme
|
||||
height: 36
|
||||
horizontalPadding: 0
|
||||
|
||||
icon.source: "/qml/icons/ic-plus.svg"
|
||||
width: 36
|
||||
|
||||
onClicked: {
|
||||
signIn.username = ""
|
||||
rightContent.showSignIn()
|
||||
signIn.username = "";
|
||||
rightContent.showSignIn();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { // right content background
|
||||
Rectangle {
|
||||
Layout.fillHeight: true // right content background
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
color: colorScheme.background_norm
|
||||
|
||||
StackLayout {
|
||||
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
|
||||
|
||||
AccountView { // 0
|
||||
AccountView {
|
||||
// 0
|
||||
colorScheme: root.colorScheme
|
||||
notifications: root.notifications
|
||||
user: {
|
||||
if (accounts.currentIndex < 0) return undefined
|
||||
if (Backend.users.count == 0) return undefined
|
||||
return Backend.users.get(accounts.currentIndex)
|
||||
if (accounts.currentIndex < 0)
|
||||
return undefined;
|
||||
if (Backend.users.count === 0)
|
||||
return undefined;
|
||||
return Backend.users.get(accounts.currentIndex);
|
||||
}
|
||||
|
||||
onShowSetupGuide: function (user, address) {
|
||||
root.showSetupGuide(user, address);
|
||||
}
|
||||
onShowSignIn: {
|
||||
var user = this.user
|
||||
signIn.username = user ? user.primaryEmailOrUsername() : ""
|
||||
rightContent.showSignIn()
|
||||
}
|
||||
onShowSetupGuide: function(user, address) {
|
||||
root.showSetupGuide(user,address)
|
||||
const user = this.user;
|
||||
signIn.username = user ? user.primaryEmailOrUsername() : "";
|
||||
rightContent.showSignIn();
|
||||
}
|
||||
}
|
||||
|
||||
GridLayout { // 1 Sign In
|
||||
GridLayout {
|
||||
// 1 Sign In
|
||||
columns: 2
|
||||
|
||||
Button {
|
||||
id: backButton
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.leftMargin: 18
|
||||
Layout.topMargin: 10
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
signIn.abort()
|
||||
rightContent.showAccount()
|
||||
}
|
||||
horizontalPadding: 8
|
||||
icon.source: "/qml/icons/ic-arrow-left.svg"
|
||||
secondary: true
|
||||
horizontalPadding: 8
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
signIn.abort();
|
||||
rightContent.showAccount();
|
||||
}
|
||||
}
|
||||
SignIn {
|
||||
id: signIn
|
||||
Layout.topMargin: 68
|
||||
Layout.leftMargin: 80 - backButton.width - 18
|
||||
Layout.rightMargin: 80
|
||||
Layout.bottomMargin: 68
|
||||
Layout.preferredWidth: 320
|
||||
Layout.fillWidth: 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
|
||||
}
|
||||
}
|
||||
|
||||
GeneralSettings { // 2
|
||||
GeneralSettings {
|
||||
// 2
|
||||
colorScheme: root.colorScheme
|
||||
notifications: root.notifications
|
||||
|
||||
onBack: {
|
||||
rightContent.showAccount()
|
||||
rightContent.showAccount();
|
||||
}
|
||||
}
|
||||
|
||||
KeychainSettings { // 3
|
||||
KeychainSettings {
|
||||
// 3
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
onBack: {
|
||||
rightContent.showGeneralSettings()
|
||||
rightContent.showGeneralSettings();
|
||||
}
|
||||
}
|
||||
|
||||
PortSettings { // 4
|
||||
colorScheme: root.colorScheme
|
||||
notifications: root.notifications
|
||||
onBack: {
|
||||
rightContent.showGeneralSettings()
|
||||
}
|
||||
}
|
||||
|
||||
ConnectionModeSettings { // 5
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
onBack: {
|
||||
rightContent.showGeneralSettings()
|
||||
}
|
||||
}
|
||||
|
||||
LocalCacheSettings { // 6
|
||||
PortSettings {
|
||||
// 4
|
||||
colorScheme: root.colorScheme
|
||||
notifications: root.notifications
|
||||
|
||||
onBack: {
|
||||
rightContent.showGeneralSettings()
|
||||
rightContent.showGeneralSettings();
|
||||
}
|
||||
}
|
||||
|
||||
HelpView { // 7
|
||||
ConnectionModeSettings {
|
||||
// 5
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
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
|
||||
colorScheme: root.colorScheme
|
||||
selectedAddress: {
|
||||
if (accounts.currentIndex < 0) return ""
|
||||
if (Backend.users.count == 0) return ""
|
||||
var user = Backend.users.get(accounts.currentIndex)
|
||||
if (!user) return ""
|
||||
return user.addresses[0]
|
||||
if (accounts.currentIndex < 0)
|
||||
return "";
|
||||
if (Backend.users.count === 0)
|
||||
return "";
|
||||
const user = Backend.users.get(accounts.currentIndex);
|
||||
if (!user)
|
||||
return "";
|
||||
return user.addresses[0];
|
||||
}
|
||||
|
||||
onBack: {
|
||||
rightContent.showHelpView()
|
||||
rightContent.showHelpView();
|
||||
}
|
||||
|
||||
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 {
|
||||
target: Backend
|
||||
function onLoginAlreadyLoggedIn(index) {
|
||||
rightContent.showAccount(index);
|
||||
}
|
||||
function onLoginFinished(index) {
|
||||
rightContent.showAccount(index);
|
||||
}
|
||||
|
||||
function onLoginFinished(index) { rightContent.showAccount(index) }
|
||||
function onLoginAlreadyLoggedIn(index) { rightContent.showAccount(index) }
|
||||
target: Backend
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,54 +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/>.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import "."
|
||||
import "./Proton"
|
||||
import "Proton"
|
||||
|
||||
Rectangle {
|
||||
property var target: parent
|
||||
|
||||
x: target.x
|
||||
y: target.y
|
||||
width: target.width
|
||||
height: target.height
|
||||
|
||||
color: "transparent"
|
||||
border.color: "red"
|
||||
border.width: 1
|
||||
color: "transparent"
|
||||
height: target.height
|
||||
width: target.width
|
||||
x: target.x
|
||||
y: target.y
|
||||
//z: parent.z - 1
|
||||
z: 10000000
|
||||
|
||||
Label {
|
||||
text: parent.width + "x" + parent.height
|
||||
anchors.centerIn: parent
|
||||
color: "black"
|
||||
colorScheme: ProtonStyle.currentStyle
|
||||
text: parent.width + "x" + parent.height
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: target.implicitWidth
|
||||
height: target.implicitHeight
|
||||
|
||||
color: "transparent"
|
||||
border.color: "green"
|
||||
border.width: 1
|
||||
color: "transparent"
|
||||
height: target.implicitHeight
|
||||
width: target.implicitWidth
|
||||
//z: parent.z - 1
|
||||
z: 10000000
|
||||
}
|
||||
|
||||
@ -1,25 +1,19 @@
|
||||
// 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 QtQuick.Controls.impl
|
||||
|
||||
import Proton
|
||||
|
||||
SettingsView {
|
||||
@ -31,144 +25,138 @@ SettingsView {
|
||||
fillHeight: false
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Settings")
|
||||
type: Label.Heading
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SettingsItem {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
id: beta
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Beta access")
|
||||
description: qsTr("Be among the first to try new features.")
|
||||
type: SettingsItem.Toggle
|
||||
Layout.fillWidth: true
|
||||
checked: Backend.isBetaEnabled
|
||||
colorScheme: root.colorScheme
|
||||
description: qsTr("Be among the first to try new features.")
|
||||
text: qsTr("Beta access")
|
||||
type: SettingsItem.Toggle
|
||||
|
||||
onClicked: {
|
||||
if (!beta.checked) {
|
||||
root.notifications.askEnableBeta()
|
||||
root.notifications.askEnableBeta();
|
||||
} else {
|
||||
Backend.toggleBeta(false)
|
||||
Backend.toggleBeta(false);
|
||||
}
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
ColorImage {
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-down.svg" : "/qml/icons/ic-chevron-right.svg"
|
||||
color: root.colorScheme.interaction_norm
|
||||
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
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
onClicked: root._isAdvancedShown = !root._isAdvancedShown
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
id: advSettLabel
|
||||
color: root.colorScheme.interaction_norm
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Advanced settings")
|
||||
color: root.colorScheme.interaction_norm
|
||||
type: Label.Body
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
onClicked: root._isAdvancedShown = !root._isAdvancedShown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsItem {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
id: doh
|
||||
visible: root._isAdvancedShown
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Alternative routing")
|
||||
description: qsTr("If Proton’s 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
|
||||
}
|
||||
checked: Backend.isDoHEnabled
|
||||
colorScheme: root.colorScheme
|
||||
description: qsTr("If Proton’s 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 {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
id: telemetry
|
||||
Layout.fillWidth: true
|
||||
@ -181,73 +169,68 @@ SettingsView {
|
||||
|
||||
onClicked: Backend.toggleIsTelemetryDisabled(telemetry.checked)
|
||||
}
|
||||
|
||||
SettingsItem {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
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
|
||||
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 {
|
||||
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
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,126 +1,110 @@
|
||||
// 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
|
||||
|
||||
fillHeight: true
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Help")
|
||||
type: Label.Heading
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SettingsItem {
|
||||
id: setupPage
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Installation and setup")
|
||||
actionText: qsTr("Go to help topics")
|
||||
Layout.fillWidth: true
|
||||
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.")
|
||||
text: qsTr("Installation and setup")
|
||||
type: SettingsItem.PrimaryButton
|
||||
|
||||
onClicked: {
|
||||
Backend.notifyKBArticleClicked("https://proton.me/support/bridge");
|
||||
Qt.openUrlExternally("https://proton.me/support/bridge")}
|
||||
|
||||
Layout.fillWidth: true
|
||||
Qt.openUrlExternally("https://proton.me/support/bridge");
|
||||
}
|
||||
}
|
||||
|
||||
SettingsItem {
|
||||
id: checkUpdates
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Updates")
|
||||
Layout.fillWidth: true
|
||||
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
|
||||
|
||||
onClicked: {
|
||||
checkUpdates.loading = true
|
||||
Backend.checkUpdates()
|
||||
checkUpdates.loading = true;
|
||||
Backend.checkUpdates();
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onCheckUpdatesFinished() {
|
||||
checkUpdates.loading = false;
|
||||
}
|
||||
|
||||
target: Backend
|
||||
function onCheckUpdatesFinished() { checkUpdates.loading = false }
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SettingsItem {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
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
|
||||
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 {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
colorScheme: root.colorScheme
|
||||
type: Label.Caption
|
||||
color: root.colorScheme.text_weak
|
||||
textFormat: Text.StyledText
|
||||
|
||||
colorScheme: root.colorScheme
|
||||
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").
|
||||
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")))
|
||||
|
||||
onLinkActivated: function(link) { Qt.openUrlExternally(link) }
|
||||
onLinkActivated: function (link) {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,116 +1,105 @@
|
||||
// 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 QtQuick.Controls.impl
|
||||
|
||||
import Proton
|
||||
|
||||
SettingsView {
|
||||
id: root
|
||||
|
||||
fillHeight: false
|
||||
property bool _valuesChanged: keychainSelection.checkedButton && keychainSelection.checkedButton.text !== Backend.currentKeychain
|
||||
|
||||
property bool _valuesChanged: keychainSelection.checkedButton && keychainSelection.checkedButton.text != Backend.currentKeychain
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Default keychain")
|
||||
type: Label.Heading
|
||||
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
|
||||
function setDefaultValues() {
|
||||
for (const bi in keychainSelection.buttons) {
|
||||
const button = keychainSelection.buttons[bi];
|
||||
if (button.text === Backend.currentKeychain) {
|
||||
button.checked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fillHeight: false
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: root.colorScheme.border_weak
|
||||
Component.onCompleted: root.setDefaultValues()
|
||||
onBack: {
|
||||
root.setDefaultValues();
|
||||
}
|
||||
|
||||
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 {
|
||||
spacing: 12
|
||||
|
||||
Button {
|
||||
id: submitButton
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Save and restart")
|
||||
enabled: root._valuesChanged
|
||||
text: qsTr("Save and restart")
|
||||
|
||||
onClicked: {
|
||||
Backend.changeKeychain(keychainSelection.checkedButton.text)
|
||||
Backend.changeKeychain(keychainSelection.checkedButton.text);
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Cancel")
|
||||
onClicked: root.back()
|
||||
secondary: true
|
||||
}
|
||||
text: qsTr("Cancel")
|
||||
|
||||
onClicked: root.back()
|
||||
}
|
||||
Connections {
|
||||
target: Backend
|
||||
|
||||
function onChangeKeychainFinished() {
|
||||
submitButton.loading = false
|
||||
root.back()
|
||||
submitButton.loading = false;
|
||||
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()
|
||||
}
|
||||
|
||||
@ -1,81 +1,88 @@
|
||||
// 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 QtQuick.Controls.impl
|
||||
import QtQuick.Dialogs
|
||||
|
||||
import Proton
|
||||
|
||||
SettingsView {
|
||||
id: root
|
||||
|
||||
fillHeight: false
|
||||
|
||||
property var notifications
|
||||
property url diskCachePath: pathDialog.shortcuts.home
|
||||
property var notifications
|
||||
|
||||
function refresh() {
|
||||
diskCacheSetting.description = Backend.nativePath(root.diskCachePath)
|
||||
submitButton.enabled = (!submitButton.loading) && !Backend.areSameFileOrFolder(Backend.diskCachePath, root.diskCachePath)
|
||||
diskCacheSetting.description = Backend.nativePath(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 {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Local cache")
|
||||
type: Label.Heading
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
color: root.colorScheme.text_weak
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Bridge stores your encrypted messages locally to optimize communication with your client.")
|
||||
type: Label.Body
|
||||
color: root.colorScheme.text_weak
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
SettingsItem {
|
||||
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
|
||||
actionText: qsTr("Change location")
|
||||
colorScheme: root.colorScheme
|
||||
descriptionWrap: Text.WrapAnywhere
|
||||
text: qsTr("Current cache location")
|
||||
type: SettingsItem.Button
|
||||
|
||||
onClicked: {
|
||||
pathDialog.open();
|
||||
}
|
||||
|
||||
FolderDialog {
|
||||
id: pathDialog
|
||||
title: qsTr("Select cache location")
|
||||
currentFolder: root.diskCachePath
|
||||
onAccepted: {
|
||||
root.diskCachePath = pathDialog.selectedFolder
|
||||
root.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
title: qsTr("Select cache location")
|
||||
|
||||
onAccepted: {
|
||||
root.diskCachePath = pathDialog.selectedFolder;
|
||||
root.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
spacing: 12
|
||||
|
||||
@ -83,43 +90,25 @@ SettingsView {
|
||||
id: submitButton
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Save")
|
||||
|
||||
onClicked: {
|
||||
root.submit()
|
||||
root.submit();
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Cancel")
|
||||
onClicked: root.back()
|
||||
secondary: true
|
||||
}
|
||||
text: qsTr("Cancel")
|
||||
|
||||
onClicked: root.back()
|
||||
}
|
||||
Connections {
|
||||
target: Backend
|
||||
|
||||
function onDiskCachePathChangeFinished() {
|
||||
submitButton.loading = false
|
||||
root.setDefaultValues()
|
||||
submitButton.loading = false;
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,232 +1,199 @@
|
||||
// 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 QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
import Proton
|
||||
import Notifications
|
||||
|
||||
ApplicationWindow {
|
||||
id: root
|
||||
colorScheme: ProtonStyle.currentStyle
|
||||
visible: true
|
||||
|
||||
|
||||
property int _defaultWidth: 1080
|
||||
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
|
||||
minimumWidth: _defaultWidth
|
||||
|
||||
property var notifications
|
||||
visible: true
|
||||
width: _defaultWidth
|
||||
|
||||
// show Setup Guide on every new user
|
||||
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) {
|
||||
for (var i = first; i <= last; i++ ) {
|
||||
var user = Backend.users.get(i)
|
||||
|
||||
for (let i = first; i <= last; i++) {
|
||||
const user = Backend.users.get(i);
|
||||
if (setupGuide.user === user) {
|
||||
setupGuide.user = null
|
||||
contentLayout._showSetup = false
|
||||
return
|
||||
setupGuide.user = null;
|
||||
contentLayout._showSetup = false;
|
||||
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 {
|
||||
target: Backend
|
||||
|
||||
function onShowMainWindow() {
|
||||
root.showAndRise()
|
||||
}
|
||||
|
||||
function onLoginFinished(index, wasSignedOut) {
|
||||
var user = Backend.users.get(index)
|
||||
const user = Backend.users.get(index);
|
||||
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) {
|
||||
contentWrapper.selectUser(userID)
|
||||
contentWrapper.selectUser(userID);
|
||||
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 {
|
||||
id: contentLayout
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
property bool _showSetup: false
|
||||
|
||||
anchors.fill: parent
|
||||
currentIndex: {
|
||||
// show welcome when there are no users
|
||||
if (Backend.users.count === 0) {
|
||||
return 1
|
||||
return 1;
|
||||
}
|
||||
|
||||
var u = Backend.users.get(0)
|
||||
|
||||
const u = Backend.users.get(0);
|
||||
if (!u) {
|
||||
console.trace()
|
||||
console.log("empty user")
|
||||
return 1
|
||||
console.trace();
|
||||
console.log("empty user");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ((Backend.users.count === 1) && (u.state === EUserState.SignedOut)) {
|
||||
showSignIn(u.primaryEmailOrUsername())
|
||||
return 0
|
||||
showSignIn(u.primaryEmailOrUsername());
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (contentLayout._showSetup) {
|
||||
return 2
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 0
|
||||
return 0;
|
||||
}
|
||||
|
||||
ContentWrapper { // 0
|
||||
ContentWrapper {
|
||||
// 0
|
||||
id: contentWrapper
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
notifications: root.notifications
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
onShowSetupGuide: function(user, address) {
|
||||
root.showSetup(user,address)
|
||||
}
|
||||
|
||||
onCloseWindow: {
|
||||
root.close()
|
||||
root.close();
|
||||
}
|
||||
|
||||
onQuitBridge: {
|
||||
// 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.close()
|
||||
Backend.quit()
|
||||
root.close();
|
||||
Backend.quit();
|
||||
}
|
||||
onShowSetupGuide: function (user, address) {
|
||||
root.showSetup(user, address);
|
||||
}
|
||||
}
|
||||
|
||||
WelcomeGuide { // 1
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
WelcomeGuide {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.fillWidth: true // 1
|
||||
colorScheme: root.colorScheme
|
||||
}
|
||||
|
||||
SetupGuide { // 2
|
||||
SetupGuide {
|
||||
// 2
|
||||
id: setupGuide
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
onDismissed: {
|
||||
root.showSetup(null,"")
|
||||
root.showSetup(null, "");
|
||||
}
|
||||
|
||||
onFinished: {
|
||||
// TODO: Do not close window. Trigger Backend to check that
|
||||
// there is a successfully connected client. Then Backend
|
||||
// should send another signal to close the setup guide.
|
||||
root.showSetup(null,"")
|
||||
root.showSetup(null, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NotificationPopups {
|
||||
colorScheme: root.colorScheme
|
||||
notifications: root.notifications
|
||||
mainWindow: root
|
||||
notifications: root.notifications
|
||||
}
|
||||
|
||||
SplashScreen {
|
||||
id: splashScreen
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,118 +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 QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
import Proton
|
||||
import Notifications
|
||||
|
||||
Dialog {
|
||||
id: root
|
||||
|
||||
default property alias data: additionalChildrenContainer.children
|
||||
property var notification
|
||||
|
||||
shouldShow: notification && notification.active && !notification.dismissed
|
||||
modal: true
|
||||
|
||||
default property alias data: additionalChildrenContainer.children
|
||||
shouldShow: notification && notification.active && !notification.dismissed
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
Image {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
sourceSize.width: 64
|
||||
sourceSize.height: 64
|
||||
|
||||
Layout.bottomMargin: 16
|
||||
Layout.preferredHeight: 64
|
||||
Layout.preferredWidth: 64
|
||||
|
||||
Layout.bottomMargin: 16
|
||||
|
||||
visible: source != ""
|
||||
|
||||
source: {
|
||||
if (!root.notification) {
|
||||
return ""
|
||||
return "";
|
||||
}
|
||||
|
||||
switch (root.notification.type) {
|
||||
case Notification.NotificationType.Info:
|
||||
return "/qml/icons/ic-info.svg"
|
||||
case Notification.NotificationType.Success:
|
||||
return "/qml/icons/ic-success.svg"
|
||||
case Notification.NotificationType.Warning:
|
||||
case Notification.NotificationType.Danger:
|
||||
return "/qml/icons/ic-alert.svg"
|
||||
case Notification.NotificationType.Info:
|
||||
return "/qml/icons/ic-info.svg";
|
||||
case Notification.NotificationType.Success:
|
||||
return "/qml/icons/ic-success.svg";
|
||||
case Notification.NotificationType.Warning:
|
||||
case Notification.NotificationType.Danger:
|
||||
return "/qml/icons/ic-alert.svg";
|
||||
}
|
||||
}
|
||||
sourceSize.height: 64
|
||||
sourceSize.width: 64
|
||||
visible: source != ""
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.bottomMargin: 8
|
||||
colorScheme: root.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: root.notification.title
|
||||
type: Label.LabelType.Title
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.bottomMargin: 16
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 240
|
||||
Layout.bottomMargin: 16
|
||||
|
||||
colorScheme: root.colorScheme
|
||||
text: root.notification.description
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: root.notification.description
|
||||
type: Label.LabelType.Body
|
||||
onLinkActivated: function(link) { Qt.openUrlExternally(link) }
|
||||
}
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
onLinkActivated: function (link) {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
}
|
||||
Item {
|
||||
id: additionalChildrenContainer
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: 16
|
||||
|
||||
visible: children.length > 0
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: additionalChildrenContainer.childrenRect.height
|
||||
implicitWidth: additionalChildrenContainer.childrenRect.width
|
||||
visible: children.length > 0
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 8
|
||||
|
||||
Repeater {
|
||||
model: root.notification.action
|
||||
|
||||
delegate: Button {
|
||||
Layout.fillWidth: true
|
||||
|
||||
colorScheme: root.colorScheme
|
||||
action: modelData
|
||||
|
||||
secondary: index > 0
|
||||
|
||||
colorScheme: root.colorScheme
|
||||
loading: modelData.loading
|
||||
secondary: index > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,25 +1,19 @@
|
||||
// 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 QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
import Proton
|
||||
import Notifications
|
||||
|
||||
@ -27,118 +21,98 @@ Item {
|
||||
id: root
|
||||
|
||||
property ColorScheme colorScheme
|
||||
property var notifications
|
||||
property var mainWindow
|
||||
|
||||
property int notificationWhitelist: NotificationFilter.FilterConsts.All
|
||||
property int notificationBlacklist: NotificationFilter.FilterConsts.None
|
||||
property int notificationWhitelist: NotificationFilter.FilterConsts.All
|
||||
property var notifications
|
||||
|
||||
NotificationFilter {
|
||||
id: bannerNotificationFilter
|
||||
|
||||
source: root.notifications.all
|
||||
blacklist: Notifications.Group.Dialogs
|
||||
source: root.notifications.all
|
||||
}
|
||||
|
||||
Banner {
|
||||
colorScheme: root.colorScheme
|
||||
notification: bannerNotificationFilter.topmost
|
||||
mainWindow: root.mainWindow
|
||||
notification: bannerNotificationFilter.topmost
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.updateManualReady
|
||||
|
||||
Switch {
|
||||
id:autoUpdate
|
||||
id: autoUpdate
|
||||
checked: Backend.isAutomaticUpdateOn
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Update automatically in the future")
|
||||
checked: Backend.isAutomaticUpdateOn
|
||||
|
||||
onClicked: Backend.toggleAutomaticUpdate(autoUpdate.checked)
|
||||
}
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.updateForce
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.updateForceError
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.enableBeta
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.cacheUnavailable
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.cacheCantMove
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.diskFull
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.enableSplitMode
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.resetBridge
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.changeAllMailVisibility
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.deleteAccount
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.noKeychain
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.rebuildKeychain
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.apiCertIssue
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.noActiveKeyForRecipient
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.userBadEvent
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.genericError
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.genericQuestion
|
||||
|
||||
@ -1,54 +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/>.
|
||||
|
||||
import QtQml
|
||||
import QtQuick.Controls
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
default property var children
|
||||
|
||||
enum NotificationType {
|
||||
Info = 0,
|
||||
Success = 1,
|
||||
Warning = 2,
|
||||
Danger = 3
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
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
|
||||
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 group
|
||||
|
||||
property bool dismissed: false
|
||||
property bool active: false
|
||||
readonly property var occurred: active ? new Date() : undefined
|
||||
|
||||
property var data
|
||||
|
||||
onActiveChanged: {
|
||||
dismissed = false
|
||||
dismissed = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,114 +1,95 @@
|
||||
// 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 QtQml
|
||||
import QtQml.Models
|
||||
|
||||
// contains notifications that satisfy black- and whitelist and are sorted in time-occurred order
|
||||
ListModel {
|
||||
id: root
|
||||
|
||||
enum FilterConsts {
|
||||
None = 0,
|
||||
None,
|
||||
All = 255
|
||||
}
|
||||
|
||||
property int whitelist: NotificationFilter.FilterConsts.All
|
||||
property int blacklist: NotificationFilter.FilterConsts.None
|
||||
|
||||
property Notification topmost
|
||||
property var source
|
||||
|
||||
property bool componentCompleted: false
|
||||
Component.onCompleted: {
|
||||
root.componentCompleted = true
|
||||
root.rebuildList()
|
||||
}
|
||||
property var source
|
||||
property Notification topmost
|
||||
property int whitelist: NotificationFilter.FilterConsts.All
|
||||
|
||||
// overriding get method to ignore any role and return directly object itself
|
||||
function get(row) {
|
||||
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() {
|
||||
let i;
|
||||
// avoid evaluation of the list before Component.onCompleted
|
||||
if (!root.componentCompleted) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < root.count; i++) {
|
||||
root.get(i).onActiveChanged.disconnect( root.updateList )
|
||||
for (i = 0; i < root.count; i++) {
|
||||
root.get(i).onActiveChanged.disconnect(root.updateList);
|
||||
}
|
||||
|
||||
root.clear()
|
||||
|
||||
root.clear();
|
||||
if (!root.source) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < root.source.length; i++) {
|
||||
var obj = root.source[i]
|
||||
const obj = root.source[i];
|
||||
if (obj.group & root.blacklist) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(obj.group & root.whitelist)) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
root.append({obj})
|
||||
obj.onActiveChanged.connect( root.updateList )
|
||||
root.append({
|
||||
"obj": obj
|
||||
});
|
||||
obj.onActiveChanged.connect(root.updateList);
|
||||
}
|
||||
}
|
||||
|
||||
function updateList() {
|
||||
var topmost = null
|
||||
|
||||
for (var i = 0; i < root.count; i++) {
|
||||
var obj = root.get(i)
|
||||
|
||||
let topmost = null;
|
||||
for (let i = 0; i < root.count; i++) {
|
||||
const obj = root.get(i);
|
||||
if (!obj.active) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (topmost && (topmost.type > obj.type)) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (topmost && (topmost.type === obj.type) && (topmost.occurred > obj.occurred)) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
topmost = obj
|
||||
topmost = obj;
|
||||
}
|
||||
|
||||
root.topmost = topmost
|
||||
root.topmost = topmost;
|
||||
}
|
||||
|
||||
onWhitelistChanged: {
|
||||
root.rebuildList()
|
||||
Component.onCompleted: {
|
||||
root.componentCompleted = true;
|
||||
root.rebuildList();
|
||||
}
|
||||
onBlacklistChanged: {
|
||||
root.rebuildList()
|
||||
root.rebuildList();
|
||||
}
|
||||
onSourceChanged: {
|
||||
root.rebuildList()
|
||||
root.rebuildList();
|
||||
}
|
||||
onWhitelistChanged: {
|
||||
root.rebuildList();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,178 +1,157 @@
|
||||
// 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 QtQuick.Controls.impl
|
||||
|
||||
import Proton
|
||||
|
||||
SettingsView {
|
||||
id: root
|
||||
|
||||
fillHeight: false
|
||||
|
||||
property bool _valuesChanged: (imapField.text * 1 !== Backend.imapPort || smtpField.text * 1 !== Backend.smtpPort)
|
||||
property var notifications
|
||||
|
||||
property bool _valuesChanged: (
|
||||
imapField.text*1 !== Backend.imapPort ||
|
||||
smtpField.text*1 !== Backend.smtpPort
|
||||
)
|
||||
function isPortFree(field) {
|
||||
const 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;
|
||||
}
|
||||
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 {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Default ports")
|
||||
type: Label.Heading
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
color: root.colorScheme.text_weak
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Changes require reconfiguration of your email client.")
|
||||
type: Label.Body
|
||||
color: root.colorScheme.text_weak
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 16
|
||||
|
||||
TextField {
|
||||
id: imapField
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
|
||||
Layout.preferredWidth: 160
|
||||
colorScheme: root.colorScheme
|
||||
label: qsTr("IMAP port")
|
||||
Layout.preferredWidth: 160
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
|
||||
validator: root.validate
|
||||
}
|
||||
TextField {
|
||||
id: smtpField
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
|
||||
Layout.preferredWidth: 160
|
||||
colorScheme: root.colorScheme
|
||||
label: qsTr("SMTP port")
|
||||
Layout.preferredWidth: 160
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
|
||||
validator: root.validate
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: root.colorScheme.border_weak
|
||||
height: 1
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 12
|
||||
|
||||
Button {
|
||||
id: submitButton
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Save")
|
||||
enabled: (!loading) && root._valuesChanged
|
||||
text: qsTr("Save")
|
||||
|
||||
onClicked: {
|
||||
// removing error here because we may have set it manually (port occupied)
|
||||
imapField.error = false
|
||||
smtpField.error = false
|
||||
imapField.error = false;
|
||||
smtpField.error = false;
|
||||
|
||||
// checking errors separately because we want to display "same port" error only once
|
||||
imapField.validate()
|
||||
imapField.validate();
|
||||
if (imapField.error) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
smtpField.validate()
|
||||
smtpField.validate();
|
||||
if (smtpField.error) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
submitButton.loading = true
|
||||
submitButton.loading = true;
|
||||
|
||||
// check both ports before returning an error
|
||||
var err = false
|
||||
err |= !isPortFree(imapField)
|
||||
err |= !isPortFree(smtpField)
|
||||
let err = false;
|
||||
err |= !isPortFree(imapField);
|
||||
err |= !isPortFree(smtpField);
|
||||
if (err) {
|
||||
submitButton.loading = false
|
||||
return
|
||||
submitButton.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// We turn off all port error notification. They well be restored if problems persist
|
||||
root.notifications.imapPortStartupError.active = false
|
||||
root.notifications.smtpPortStartupError.active = false
|
||||
root.notifications.imapPortChangeError.active = false
|
||||
root.notifications.smtpPortChangeError.active = false
|
||||
|
||||
Backend.setMailServerSettings(imapField.text, smtpField.text, Backend.useSSLForIMAP, Backend.useSSLForSMTP)
|
||||
// We turn off all port error notification. They will be restored if problems persist
|
||||
root.notifications.imapPortStartupError.active = false;
|
||||
root.notifications.smtpPortStartupError.active = false;
|
||||
root.notifications.imapPortChangeError.active = false;
|
||||
root.notifications.smtpPortChangeError.active = false;
|
||||
Backend.setMailServerSettings(imapField.text, smtpField.text, Backend.useSSLForIMAP, Backend.useSSLForSMTP);
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Cancel")
|
||||
onClicked: root.back()
|
||||
secondary: true
|
||||
}
|
||||
text: qsTr("Cancel")
|
||||
|
||||
onClicked: root.back()
|
||||
}
|
||||
Connections {
|
||||
target: Backend
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@ -1,20 +1,15 @@
|
||||
// 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.Templates as T
|
||||
|
||||
|
||||
@ -1,20 +1,15 @@
|
||||
// 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 QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
@ -25,14 +20,14 @@ import QtQuick.Templates as T
|
||||
T.ApplicationWindow {
|
||||
id: root
|
||||
|
||||
property ColorScheme colorScheme
|
||||
|
||||
// popup priority based on types
|
||||
enum PopupType {
|
||||
Banner = 0,
|
||||
Dialog = 1
|
||||
Banner,
|
||||
Dialog
|
||||
}
|
||||
|
||||
property ColorScheme colorScheme
|
||||
|
||||
// contains currently visible popup
|
||||
property var popupVisible: null
|
||||
|
||||
@ -41,85 +36,61 @@ T.ApplicationWindow {
|
||||
// overriding get method to ignore any role and return directly object itself
|
||||
function get(row) {
|
||||
if (row < 0 || row >= count) {
|
||||
return undefined
|
||||
return undefined;
|
||||
}
|
||||
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()
|
||||
return data(index(row, 0), Qt.DisplayRole);
|
||||
}
|
||||
|
||||
onRowsAboutToBeRemoved: function (parent, first, last) {
|
||||
for (var i = first; i <= last; i++ ) {
|
||||
var obj = popups.get(i)
|
||||
obj.onShouldShowChanged.disconnect( root.processPopups )
|
||||
for (let i = first; i <= last; i++) {
|
||||
const obj = popups.get(i);
|
||||
obj.onShouldShowChanged.disconnect(root.processPopups);
|
||||
|
||||
// if currently visible popup was removed
|
||||
if (root.popupVisible === obj) {
|
||||
root.popupVisible.visible = false
|
||||
root.popupVisible = null
|
||||
root.popupVisible.visible = false;
|
||||
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() {
|
||||
if ((root.popupVisible) && (!root.popupVisible.shouldShow)) {
|
||||
root.popupVisible.visible = false
|
||||
root.popupVisible.visible = false;
|
||||
}
|
||||
|
||||
var topmost = null
|
||||
for (var i = 0; i < popups.count; i++) {
|
||||
var obj = popups.get(i)
|
||||
|
||||
let topmost = null;
|
||||
for (let i = 0; i < popups.count; i++) {
|
||||
const obj = popups.get(i);
|
||||
if (obj.shouldShow === false) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (topmost && (topmost.popupType > obj.popupType)) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (topmost && (topmost.popupType === obj.popupType) && (topmost.occurred > obj.occurred)) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
topmost = obj
|
||||
topmost = obj;
|
||||
}
|
||||
|
||||
if (root.popupVisible !== topmost) {
|
||||
if (root.popupVisible) {
|
||||
root.popupVisible.visible = false
|
||||
root.popupVisible.visible = false;
|
||||
}
|
||||
root.popupVisible = topmost
|
||||
root.popupVisible = topmost;
|
||||
}
|
||||
|
||||
if (!root.popupVisible) {
|
||||
return
|
||||
}
|
||||
|
||||
root.popupVisible.visible = true
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.popupVisible
|
||||
|
||||
function onVisibleChanged() {
|
||||
if (root.popupVisible.visible) {
|
||||
return
|
||||
}
|
||||
|
||||
root.popupVisible = null
|
||||
root.processPopups()
|
||||
return;
|
||||
}
|
||||
root.popupVisible.visible = true;
|
||||
}
|
||||
|
||||
color: root.colorScheme.background_norm
|
||||
@ -127,8 +98,19 @@ T.ApplicationWindow {
|
||||
Overlay.modal: Rectangle {
|
||||
color: root.colorScheme.backdrop_norm
|
||||
}
|
||||
|
||||
Overlay.modeless: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onVisibleChanged() {
|
||||
if (root.popupVisible.visible) {
|
||||
return;
|
||||
}
|
||||
root.popupVisible = null;
|
||||
root.processPopups();
|
||||
}
|
||||
|
||||
target: root.popupVisible
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,15 @@
|
||||
// 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.Controls
|
||||
import QtQuick.Controls.impl
|
||||
@ -23,212 +18,169 @@ import QtQuick.Layouts
|
||||
import "." as Proton
|
||||
|
||||
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
|
||||
|
||||
implicitWidth: Math.max(
|
||||
implicitBackgroundWidth + leftInset + rightInset,
|
||||
implicitContentWidth + leftPadding + rightPadding
|
||||
)
|
||||
implicitHeight: Math.max(
|
||||
implicitBackgroundHeight + topInset + bottomInset,
|
||||
implicitContentHeight + topPadding + bottomPadding
|
||||
)
|
||||
|
||||
padding: 8
|
||||
horizontalPadding: 16
|
||||
spacing: 10
|
||||
property bool borderless: false
|
||||
property ColorScheme colorScheme
|
||||
readonly property bool hasTextAndIcon: (control.text !== "") && (iconImage.source.toString().length > 0)
|
||||
readonly property bool isIcon: control.text === ""
|
||||
property int labelType: Proton.Label.LabelType.Body
|
||||
property bool loading: false
|
||||
readonly property bool primary: !secondary
|
||||
property alias secondary: control.flat
|
||||
property alias textHorizontalAlignment: label.horizontalAlignment
|
||||
property alias textVerticalAlignment: label.verticalAlignment
|
||||
|
||||
font: label.font
|
||||
|
||||
icon.width: 16
|
||||
icon.height: 16
|
||||
horizontalPadding: 16
|
||||
icon.color: {
|
||||
if (primary && !isIcon) {
|
||||
return "#FFFFFF"
|
||||
return "#FFFFFF";
|
||||
} 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 {
|
||||
id: _contentItem
|
||||
spacing: control.hasTextAndIcon ? control.spacing : 0
|
||||
|
||||
Proton.Label {
|
||||
colorScheme: root.colorScheme
|
||||
id: label
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
|
||||
visible: !control.isIcon
|
||||
text: control.text
|
||||
Layout.fillWidth: true
|
||||
color: {
|
||||
if (primary && !isIcon) {
|
||||
return "#FFFFFF"
|
||||
return "#FFFFFF";
|
||||
} 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
|
||||
|
||||
text: control.text
|
||||
type: labelType
|
||||
visible: !control.isIcon
|
||||
}
|
||||
|
||||
ColorImage {
|
||||
id: iconImage
|
||||
|
||||
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: {
|
||||
// special case for loading since we want icon to be square for rotation animation
|
||||
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 {
|
||||
target: iconImage
|
||||
loops: Animation.Infinite
|
||||
direction: RotationAnimation.Clockwise
|
||||
duration: 1000
|
||||
from: 0
|
||||
to: 360
|
||||
direction: RotationAnimation.Clockwise
|
||||
loops: Animation.Infinite
|
||||
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: {
|
||||
if (!control.colorScheme) {
|
||||
console.trace()
|
||||
var next = root
|
||||
for (var i = 0; i<1000; i++) {
|
||||
console.log(i, next, "colorscheme", next.colorScheme)
|
||||
next = next.parent
|
||||
if (!next) break
|
||||
console.trace();
|
||||
let next = root;
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
console.log(i, next, "colorscheme", next.colorScheme);
|
||||
next = next.parent;
|
||||
if (!next)
|
||||
break;
|
||||
}
|
||||
console.error("ColorScheme not defined")
|
||||
console.error("ColorScheme not defined");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,97 +1,96 @@
|
||||
// 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.Controls
|
||||
import QtQuick.Controls.impl
|
||||
import QtQuick.Templates as T
|
||||
|
||||
T.CheckBox {
|
||||
property ColorScheme colorScheme
|
||||
|
||||
property bool error: false
|
||||
|
||||
id: control
|
||||
|
||||
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
|
||||
implicitContentWidth + leftPadding + rightPadding)
|
||||
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
|
||||
implicitContentHeight + topPadding + bottomPadding,
|
||||
implicitIndicatorHeight + topPadding + bottomPadding)
|
||||
property ColorScheme colorScheme
|
||||
property bool error: 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: 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 {
|
||||
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
|
||||
implicitWidth: 20
|
||||
radius: ProtonStyle.checkbox_radius
|
||||
|
||||
x: text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 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 {
|
||||
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
|
||||
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?
|
||||
@ -105,30 +104,4 @@ T.CheckBox {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,93 +1,88 @@
|
||||
// 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 QtQml
|
||||
|
||||
QtObject {
|
||||
// should be a pointer to ColorScheme object
|
||||
property var prominent
|
||||
|
||||
// Primary
|
||||
property color primary_norm
|
||||
// Backdrop
|
||||
property color backdrop_norm
|
||||
property color background_avatar
|
||||
|
||||
// Interaction-norm
|
||||
property color interaction_norm
|
||||
property color interaction_norm_hover
|
||||
property color interaction_norm_active
|
||||
|
||||
// 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
|
||||
// Background
|
||||
property color background_norm
|
||||
property color background_strong
|
||||
property color background_weak
|
||||
|
||||
// Border
|
||||
property color border_norm
|
||||
property color border_weak
|
||||
property color field_disabled
|
||||
property color field_hover
|
||||
|
||||
// Background
|
||||
property color background_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
|
||||
// Field
|
||||
property color field_norm
|
||||
|
||||
// Interaction-default
|
||||
property color interaction_default
|
||||
property color interaction_default_hover
|
||||
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
|
||||
property color scrollbar_norm
|
||||
property color scrollbar_hover
|
||||
|
||||
// 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
|
||||
property color shadow_lifted
|
||||
|
||||
// Shadows
|
||||
property color shadow_norm
|
||||
property color shadow_lifted
|
||||
|
||||
// Backdrop
|
||||
property color backdrop_norm
|
||||
// Signal
|
||||
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
|
||||
property string welcome_img
|
||||
property string logo_img
|
||||
}
|
||||
|
||||
@ -1,20 +1,15 @@
|
||||
// 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.Controls
|
||||
@ -26,148 +21,124 @@ T.ComboBox {
|
||||
|
||||
property ColorScheme colorScheme
|
||||
|
||||
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
|
||||
implicitContentWidth + leftPadding + rightPadding)
|
||||
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
|
||||
implicitContentHeight + topPadding + bottomPadding,
|
||||
implicitIndicatorHeight + topPadding + bottomPadding)
|
||||
|
||||
bottomPadding: 5
|
||||
font.family: ProtonStyle.font_family
|
||||
font.letterSpacing: ProtonStyle.body_letter_spacing
|
||||
font.pixelSize: ProtonStyle.body_font_size
|
||||
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)
|
||||
rightPadding: 12 + (root.mirrored || !indicator || !indicator.visible ? 0 : indicator.width + spacing)
|
||||
|
||||
topPadding: 5
|
||||
bottomPadding: 5
|
||||
|
||||
spacing: 8
|
||||
topPadding: 5
|
||||
|
||||
font.family: ProtonStyle.font_family
|
||||
font.weight: ProtonStyle.fontWeight_400
|
||||
font.pixelSize: ProtonStyle.body_font_size
|
||||
font.letterSpacing: ProtonStyle.body_letter_spacing
|
||||
|
||||
background: Rectangle {
|
||||
border.color: root.colorScheme.border_norm
|
||||
border.width: 1
|
||||
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 {
|
||||
padding: 5
|
||||
|
||||
text: root.editable ? root.editText : root.displayText
|
||||
font: root.font
|
||||
|
||||
enabled: 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
|
||||
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
|
||||
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 {
|
||||
radius: ProtonStyle.context_item_radius
|
||||
visible: root.enabled && root.editable && !root.flat
|
||||
border.color: {
|
||||
if (root.activeFocus) {
|
||||
return root.colorScheme.interaction_norm
|
||||
return root.colorScheme.interaction_norm;
|
||||
}
|
||||
|
||||
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
|
||||
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 {
|
||||
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
|
||||
|
||||
font: root.font
|
||||
highlighted: root.highlightedIndex === index
|
||||
hoverEnabled: root.hoverEnabled
|
||||
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 {
|
||||
radius: ProtonStyle.context_item_radius
|
||||
color: {
|
||||
if (parent.down) {
|
||||
return root.colorScheme.interaction_default_active
|
||||
return root.colorScheme.interaction_default_active;
|
||||
}
|
||||
|
||||
if (parent.selected) {
|
||||
return root.colorScheme.interaction_norm
|
||||
return root.colorScheme.interaction_norm;
|
||||
}
|
||||
|
||||
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 {
|
||||
y: root.height
|
||||
width: root.width
|
||||
bottomMargin: 8
|
||||
height: Math.min(contentItem.implicitHeight, root.Window.height - topMargin - bottomMargin)
|
||||
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 {
|
||||
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
|
||||
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
|
||||
@ -175,21 +146,14 @@ T.ComboBox {
|
||||
ListView {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
|
||||
currentIndex: root.highlightedIndex
|
||||
implicitHeight: contentHeight
|
||||
model: root.delegateModel
|
||||
currentIndex: root.highlightedIndex
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,15 @@
|
||||
// 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 QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Templates as T
|
||||
@ -23,58 +18,46 @@ import QtQuick.Controls.impl
|
||||
|
||||
T.Dialog {
|
||||
id: root
|
||||
|
||||
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
|
||||
function open() {
|
||||
root.shouldShow = true
|
||||
}
|
||||
readonly property int popupType: ApplicationWindow.PopupType.Dialog
|
||||
property bool shouldShow: false
|
||||
|
||||
function close() {
|
||||
root.shouldShow = false
|
||||
root.shouldShow = false;
|
||||
}
|
||||
function open() {
|
||||
root.shouldShow = true;
|
||||
}
|
||||
|
||||
anchors.centerIn: Overlay.overlay
|
||||
|
||||
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
|
||||
contentWidth + leftPadding + rightPadding,
|
||||
implicitHeaderWidth,
|
||||
implicitFooterWidth)
|
||||
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
|
||||
contentHeight + topPadding + bottomPadding
|
||||
+ (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0)
|
||||
+ (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0))
|
||||
|
||||
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, contentWidth + leftPadding + rightPadding, implicitHeaderWidth, implicitFooterWidth)
|
||||
padding: 24
|
||||
|
||||
// TODO: Add DropShadow here
|
||||
T.Overlay.modal: Rectangle {
|
||||
color: root.colorScheme.backdrop_norm
|
||||
}
|
||||
T.Overlay.modeless: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
background: Rectangle {
|
||||
color: root.colorScheme.background_norm
|
||||
radius: ProtonStyle.dialog_radius
|
||||
}
|
||||
|
||||
// TODO: Add DropShadow here
|
||||
|
||||
T.Overlay.modal: Rectangle {
|
||||
color: root.colorScheme.backdrop_norm
|
||||
}
|
||||
|
||||
T.Overlay.modeless: Rectangle {
|
||||
color: "transparent"
|
||||
Component.onCompleted: {
|
||||
if (!ApplicationWindow.window) {
|
||||
return;
|
||||
}
|
||||
if (ApplicationWindow.window.popups === undefined) {
|
||||
return;
|
||||
}
|
||||
const obj = this;
|
||||
ApplicationWindow.window.popups.append({
|
||||
"obj": obj
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,32 +1,23 @@
|
||||
// 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.Controls
|
||||
import QtQuick.Controls.impl
|
||||
import QtQuick.Templates as T
|
||||
|
||||
import "." as Proton
|
||||
|
||||
T.Label {
|
||||
id: root
|
||||
|
||||
property ColorScheme colorScheme
|
||||
|
||||
enum LabelType {
|
||||
// weight 700, size 28, height 36
|
||||
Heading,
|
||||
@ -47,96 +38,92 @@ T.Label {
|
||||
// weight 700, size 12, height 16, spacing 0.4
|
||||
Caption_bold
|
||||
}
|
||||
|
||||
property ColorScheme colorScheme
|
||||
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
|
||||
linkColor: root.colorScheme.interaction_norm
|
||||
palette.link: linkColor
|
||||
|
||||
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: {
|
||||
switch (root.type) {
|
||||
case Proton.Label.LabelType.Heading:
|
||||
case Proton.Label.LabelType.Title:
|
||||
case Proton.Label.LabelType.Lead:
|
||||
return 0
|
||||
return 0;
|
||||
case Proton.Label.LabelType.Body:
|
||||
case Proton.Label.LabelType.Body_semibold:
|
||||
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_semibold:
|
||||
case Proton.Label.LabelType.Caption_bold:
|
||||
return ProtonStyle.caption_letter_spacing
|
||||
return ProtonStyle.caption_letter_spacing;
|
||||
}
|
||||
}
|
||||
|
||||
verticalAlignment: Text.AlignBottom
|
||||
|
||||
function link(url, text) {
|
||||
return `<a href="${url}">${text}</a>`
|
||||
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;
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -1,20 +1,15 @@
|
||||
// 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.Controls
|
||||
import QtQuick.Controls.impl
|
||||
@ -27,22 +22,19 @@ T.Menu {
|
||||
|
||||
property ColorScheme colorScheme
|
||||
|
||||
implicitWidth: Math.max(
|
||||
implicitBackgroundWidth + leftInset + rightInset,
|
||||
contentWidth + leftPadding + rightPadding
|
||||
)
|
||||
implicitHeight: Math.max(
|
||||
implicitBackgroundHeight + topInset + bottomInset,
|
||||
contentHeight + topPadding + bottomPadding
|
||||
)
|
||||
|
||||
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding)
|
||||
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding)
|
||||
margins: 0
|
||||
overlap: 1
|
||||
|
||||
delegate: MenuItem {
|
||||
colorScheme: control.colorScheme
|
||||
background: Rectangle {
|
||||
border.color: colorScheme.border_weak
|
||||
border.width: 1
|
||||
color: colorScheme.background_norm
|
||||
implicitHeight: 40
|
||||
implicitWidth: 200
|
||||
radius: ProtonStyle.account_row_radius
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
|
||||
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
|
||||
@ -50,23 +42,17 @@ T.Menu {
|
||||
ListView {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
|
||||
implicitHeight: contentHeight
|
||||
model: control.contentModel
|
||||
interactive: Window.window ? contentHeight > Window.window.height : false
|
||||
clip: true
|
||||
currentIndex: control.currentIndex
|
||||
implicitHeight: contentHeight
|
||||
interactive: Window.window ? contentHeight > Window.window.height : false
|
||||
model: control.contentModel
|
||||
|
||||
ScrollIndicator.vertical: ScrollIndicator {}
|
||||
ScrollIndicator.vertical: ScrollIndicator {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
implicitWidth: 200
|
||||
implicitHeight: 40
|
||||
color: colorScheme.background_norm
|
||||
border.width: 1
|
||||
border.color: colorScheme.border_weak
|
||||
radius: ProtonStyle.account_row_radius
|
||||
delegate: MenuItem {
|
||||
colorScheme: control.colorScheme
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,15 @@
|
||||
// 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.Controls
|
||||
import QtQuick.Controls.impl
|
||||
@ -26,46 +21,39 @@ T.MenuItem {
|
||||
|
||||
property ColorScheme colorScheme
|
||||
|
||||
width: parent.width // required. Other item overflows to the right of the menu and get clipped.
|
||||
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
|
||||
implicitContentHeight + topPadding + bottomPadding,
|
||||
implicitIndicatorHeight + topPadding + bottomPadding)
|
||||
|
||||
font.family: ProtonStyle.font_family
|
||||
font.letterSpacing: ProtonStyle.body_letter_spacing
|
||||
font.pixelSize: ProtonStyle.body_font_size
|
||||
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
|
||||
spacing: 6
|
||||
|
||||
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
|
||||
}
|
||||
width: parent.width // required. Other item overflows to the right of the menu and get clipped.
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,15 @@
|
||||
// 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 QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
@ -23,45 +18,40 @@ import QtQuick.Templates as T
|
||||
|
||||
T.Popup {
|
||||
id: root
|
||||
|
||||
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
|
||||
function open() {
|
||||
root.shouldShow = true
|
||||
}
|
||||
property int popupType: ApplicationWindow.PopupType.Banner
|
||||
property bool shouldShow: false
|
||||
|
||||
function close() {
|
||||
root.shouldShow = false
|
||||
root.shouldShow = false;
|
||||
}
|
||||
function open() {
|
||||
root.shouldShow = true;
|
||||
}
|
||||
|
||||
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
|
||||
contentWidth + leftPadding + rightPadding)
|
||||
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
|
||||
contentHeight + topPadding + bottomPadding)
|
||||
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding)
|
||||
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding)
|
||||
|
||||
// TODO: Add DropShadow here
|
||||
|
||||
T.Overlay.modal: Rectangle {
|
||||
color: root.colorScheme.backdrop_norm
|
||||
}
|
||||
|
||||
T.Overlay.modeless: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!ApplicationWindow.window) {
|
||||
return;
|
||||
}
|
||||
if (ApplicationWindow.window.popups === undefined) {
|
||||
return;
|
||||
}
|
||||
const obj = this;
|
||||
ApplicationWindow.window.popups.append({
|
||||
"obj": obj
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,115 +1,91 @@
|
||||
// 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.Controls
|
||||
import QtQuick.Controls.impl
|
||||
import QtQuick.Templates as T
|
||||
|
||||
T.RadioButton {
|
||||
property ColorScheme colorScheme
|
||||
|
||||
property bool error: false
|
||||
|
||||
id: control
|
||||
|
||||
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
|
||||
implicitContentWidth + leftPadding + rightPadding)
|
||||
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
|
||||
implicitContentHeight + topPadding + bottomPadding,
|
||||
implicitIndicatorHeight + topPadding + bottomPadding)
|
||||
property ColorScheme colorScheme
|
||||
property bool error: 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: 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 {
|
||||
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
|
||||
implicitWidth: 20
|
||||
radius: 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
|
||||
|
||||
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 {
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
width: 8
|
||||
height: 8
|
||||
radius: width / 2
|
||||
color: {
|
||||
if (!control.enabled) {
|
||||
return control.colorScheme.field_disabled
|
||||
return control.colorScheme.field_disabled;
|
||||
}
|
||||
|
||||
if (control.error) {
|
||||
return control.colorScheme.signal_danger
|
||||
return control.colorScheme.signal_danger;
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,387 +1,188 @@
|
||||
// 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/>.
|
||||
|
||||
pragma Singleton
|
||||
import QtQml
|
||||
import QtQuick
|
||||
|
||||
import "./"
|
||||
import "."
|
||||
|
||||
// https://wiki.qt.io/Qml_Styling
|
||||
// http://imaginativethinking.ca/make-qml-component-singleton/
|
||||
|
||||
QtObject {
|
||||
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 {
|
||||
id: _darkProminentStyle
|
||||
|
||||
prominent: this
|
||||
// Backdrop
|
||||
backdrop_norm: Qt.rgba(0, 0, 0, 0.32)
|
||||
background_avatar: "#6D4AFF"
|
||||
|
||||
// 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"
|
||||
// Background
|
||||
background_norm: "#16141c"
|
||||
background_strong: "#3F3B4C"
|
||||
background_weak: "#292733"
|
||||
|
||||
// Border
|
||||
border_norm: "#4A4658"
|
||||
border_weak: "#343140"
|
||||
field_disabled: "#3F3B4C"
|
||||
field_hover: "#6D697D"
|
||||
|
||||
// Background
|
||||
background_norm: "#16141c"
|
||||
background_weak: "#292733"
|
||||
background_strong: "#3F3B4C"
|
||||
background_avatar: "#6D4AFF"
|
||||
|
||||
// Interaction-weak
|
||||
interaction_weak: "#4A4658"
|
||||
interaction_weak_hover: "#5B576B"
|
||||
interaction_weak_active: "#6D697D"
|
||||
// Field
|
||||
field_norm: "#5B576B"
|
||||
|
||||
// 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)
|
||||
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_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_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_danger_hover: "#FF5473"
|
||||
signal_info: "#239ECE"
|
||||
signal_info_hover: "#27B1E8"
|
||||
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
|
||||
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)
|
||||
// Text
|
||||
text_norm: "#FFFFFF"
|
||||
text_weak: "#A7A4B5"
|
||||
|
||||
// Images
|
||||
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: {
|
||||
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")
|
||||
}
|
||||
// Background
|
||||
background_norm: "#1C1B24"
|
||||
background_strong: "#3F3B4C"
|
||||
background_weak: "#292733"
|
||||
|
||||
// Border
|
||||
border_norm: "#4A4658"
|
||||
border_weak: "#343140"
|
||||
field_disabled: "#3F3B4C"
|
||||
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 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 real dialog_radius: 12 * root.px // px
|
||||
property int fontWeight_100: Font.Thin
|
||||
property int fontWeight_200: Font.Light
|
||||
property int fontWeight_300: Font.ExtraLight
|
||||
@ -391,4 +192,179 @@ QtObject {
|
||||
property int fontWeight_700: Font.Bold
|
||||
property int fontWeight_800: Font.ExtraBold
|
||||
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
|
||||
}
|
||||
|
||||
@ -1,150 +1,124 @@
|
||||
// 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.Templates as T
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.impl
|
||||
|
||||
T.Switch {
|
||||
property ColorScheme colorScheme
|
||||
id: control
|
||||
|
||||
property ColorScheme colorScheme
|
||||
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?
|
||||
// For now assuming that only enabled buttons could have loading state
|
||||
onLoadingChanged: {
|
||||
if (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
|
||||
enabled = !loading;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,54 +1,37 @@
|
||||
// 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 QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.impl
|
||||
import QtQuick.Templates as T
|
||||
import QtQuick.Layouts
|
||||
|
||||
import "." as Proton
|
||||
|
||||
FocusScope {
|
||||
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 string assistiveText
|
||||
property alias background: control.background
|
||||
property alias baseUrl: control.baseUrl
|
||||
property alias bottomInset: control.bottomInset
|
||||
property alias bottomPadding: control.bottomPadding
|
||||
property alias canPaste: control.canPaste
|
||||
property alias canRedo: control.canRedo
|
||||
property alias canUndo: control.canUndo
|
||||
property alias color: control.color
|
||||
property ColorScheme colorScheme
|
||||
property alias contentHeight: control.contentHeight
|
||||
property alias contentWidth: control.contentWidth
|
||||
property alias cursorDelegate: control.cursorDelegate
|
||||
@ -56,21 +39,36 @@ FocusScope {
|
||||
property alias cursorRectangle: control.cursorRectangle
|
||||
property alias cursorVisible: control.cursorVisible
|
||||
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 hint: hint.text
|
||||
property alias horizontalAlignment: control.horizontalAlignment
|
||||
property alias hoverEnabled: control.hoverEnabled
|
||||
property alias hovered: control.hovered
|
||||
property alias hoveredLink: control.hoveredLink
|
||||
property alias implicitBackgroundHeight: control.implicitBackgroundHeight
|
||||
property alias implicitBackgroundWidth: control.implicitBackgroundWidth
|
||||
property alias inputMethodComposing: control.inputMethodComposing
|
||||
property alias inputMethodHints: control.inputMethodHints
|
||||
property alias label: label.text
|
||||
property alias leftInset: control.leftInset
|
||||
property alias leftPadding: control.leftPadding
|
||||
property alias length: control.length
|
||||
property alias lineCount: control.lineCount
|
||||
property alias mouseSelectionMode: control.mouseSelectionMode
|
||||
property alias overwriteMode: control.overwriteMode
|
||||
property alias padding: control.padding
|
||||
property alias palette: control.palette
|
||||
property alias persistentSelection: control.persistentSelection
|
||||
property alias placeholderText: control.placeholderText
|
||||
property alias placeholderTextColor: control.placeholderTextColor
|
||||
property alias preeditText: control.preeditText
|
||||
property alias readOnly: control.readOnly
|
||||
property alias renderType: control.renderType
|
||||
property alias rightInset: control.rightInset
|
||||
property alias rightPadding: control.rightPadding
|
||||
property alias selectByKeyboard: control.selectByKeyboard
|
||||
property alias selectByMouse: control.selectByMouse
|
||||
@ -84,61 +82,119 @@ FocusScope {
|
||||
property alias textDocument: control.textDocument
|
||||
property alias textFormat: control.textFormat
|
||||
property alias textMargin: control.textMargin
|
||||
property alias topInset: control.topInset
|
||||
property alias topPadding: control.topPadding
|
||||
property bool validateOnEditingFinished: true
|
||||
// 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
|
||||
property var validator
|
||||
property alias verticalAlignment: control.verticalAlignment
|
||||
property alias wrapMode: control.wrapMode
|
||||
|
||||
implicitWidth: children[0].implicitWidth
|
||||
implicitHeight: children[0].implicitHeight
|
||||
signal editingFinished
|
||||
|
||||
property alias label: label.text
|
||||
property alias hint: hint.text
|
||||
property string assistiveText
|
||||
property string errorString
|
||||
|
||||
property bool error: false
|
||||
|
||||
signal editingFinished()
|
||||
|
||||
function append(text) { return control.append(text) }
|
||||
function clear() { return control.clear() }
|
||||
function copy() { return control.copy() }
|
||||
function cut() { return control.cut() }
|
||||
function deselect() { return control.deselect() }
|
||||
function getFormattedText(start, end) { return control.getFormattedText(start, end) }
|
||||
function getText(start, end) { return control.getText(start, end) }
|
||||
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 append(text) {
|
||||
return control.append(text);
|
||||
}
|
||||
function clear() {
|
||||
return control.clear();
|
||||
}
|
||||
function copy() {
|
||||
return control.copy();
|
||||
}
|
||||
function cut() {
|
||||
return control.cut();
|
||||
}
|
||||
function deselect() {
|
||||
return control.deselect();
|
||||
}
|
||||
function getFormattedText(start, end) {
|
||||
return control.getFormattedText(start, end);
|
||||
}
|
||||
function getText(start, end) {
|
||||
return control.getText(start, end);
|
||||
}
|
||||
|
||||
// Calculates the height of the component to make exactly lineNum visible in edit area
|
||||
function heightForLinesVisible(lineNum) {
|
||||
var totalHeight = 0
|
||||
totalHeight += headerLayout.height
|
||||
totalHeight += footerLayout.height
|
||||
totalHeight += control.topPadding + control.bottomPadding
|
||||
totalHeight += lineNum * fontMetrics.height
|
||||
return totalHeight
|
||||
let totalHeight = 0;
|
||||
totalHeight += headerLayout.height;
|
||||
totalHeight += footerLayout.height;
|
||||
totalHeight += control.topPadding + control.bottomPadding;
|
||||
totalHeight += lineNum * fontMetrics.height;
|
||||
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 {
|
||||
id: fontMetrics
|
||||
font: control.font
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
@ -149,154 +205,131 @@ FocusScope {
|
||||
spacing: 0
|
||||
|
||||
Proton.Label {
|
||||
colorScheme: root.colorScheme
|
||||
id: label
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
|
||||
|
||||
colorScheme: root.colorScheme
|
||||
type: Proton.Label.LabelType.Body_semibold
|
||||
}
|
||||
|
||||
Proton.Label {
|
||||
colorScheme: root.colorScheme
|
||||
id: hint
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: root.enabled ? root.colorScheme.text_weak : root.colorScheme.text_disabled
|
||||
colorScheme: root.colorScheme
|
||||
horizontalAlignment: Text.AlignRight
|
||||
type: Proton.Label.LabelType.Caption
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: controlView
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
clip: true
|
||||
|
||||
T.TextArea {
|
||||
id: control
|
||||
|
||||
implicitWidth: Math.max(
|
||||
contentWidth + leftPadding + rightPadding,
|
||||
implicitBackgroundWidth + leftInset + rightInset,
|
||||
placeholder.implicitWidth + leftPadding + rightPadding
|
||||
)
|
||||
implicitHeight: Math.max(
|
||||
contentHeight + topPadding + bottomPadding,
|
||||
implicitBackgroundHeight + topInset + bottomInset,
|
||||
placeholder.implicitHeight + topPadding + bottomPadding
|
||||
)
|
||||
|
||||
topPadding: 8
|
||||
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
|
||||
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
|
||||
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
|
||||
color: {
|
||||
if (!control.enabled) {
|
||||
return root.colorScheme.text_disabled
|
||||
}
|
||||
if (control.readOnly) {
|
||||
return root.colorScheme.text_hint
|
||||
}
|
||||
return root.colorScheme.text_norm
|
||||
}
|
||||
|
||||
// enforcing default focus here within component
|
||||
focus: root.focus
|
||||
|
||||
KeyNavigation.priority: root.KeyNavigation.priority
|
||||
KeyNavigation.backtab: root.KeyNavigation.backtab
|
||||
KeyNavigation.tab: root.KeyNavigation.tab
|
||||
KeyNavigation.up: root.KeyNavigation.up
|
||||
KeyNavigation.down: root.KeyNavigation.down
|
||||
KeyNavigation.left: root.KeyNavigation.left
|
||||
KeyNavigation.right: root.KeyNavigation.right
|
||||
|
||||
font.family: ProtonStyle.font_family
|
||||
font.letterSpacing: ProtonStyle.body_letter_spacing
|
||||
font.pixelSize: ProtonStyle.body_font_size
|
||||
font.weight: ProtonStyle.fontWeight_400
|
||||
implicitHeight: Math.max(contentHeight + topPadding + bottomPadding, implicitBackgroundHeight + topInset + bottomInset, placeholder.implicitHeight + topPadding + bottomPadding)
|
||||
implicitWidth: Math.max(contentWidth + leftPadding + rightPadding, implicitBackgroundWidth + leftInset + rightInset, placeholder.implicitWidth + leftPadding + rightPadding)
|
||||
leftPadding: 12
|
||||
placeholderTextColor: control.enabled ? root.colorScheme.text_hint : root.colorScheme.text_disabled
|
||||
rightPadding: 12
|
||||
selectByMouse: true
|
||||
|
||||
cursorDelegate: Rectangle {
|
||||
id: cursor
|
||||
width: 1
|
||||
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
|
||||
}
|
||||
selectedTextColor: control.palette.highlightedText
|
||||
selectionColor: control.palette.highlight
|
||||
topPadding: 8
|
||||
wrapMode: TextInput.Wrap
|
||||
|
||||
background: Rectangle {
|
||||
anchors.fill: parent
|
||||
|
||||
radius: ProtonStyle.input_radius
|
||||
visible: true
|
||||
color: root.colorScheme.background_norm
|
||||
border.color: {
|
||||
if (!control.enabled) {
|
||||
return root.colorScheme.field_disabled
|
||||
if (!control.enabled || control.readOnly) {
|
||||
return root.colorScheme.field_disabled;
|
||||
}
|
||||
|
||||
if (control.activeFocus) {
|
||||
return root.colorScheme.interaction_norm
|
||||
return root.colorScheme.interaction_norm;
|
||||
}
|
||||
|
||||
if (root.error) {
|
||||
return root.colorScheme.signal_danger
|
||||
return root.colorScheme.signal_danger;
|
||||
}
|
||||
|
||||
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
|
||||
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 {
|
||||
id: footerLayout
|
||||
Layout.fillWidth: true
|
||||
@ -304,67 +337,29 @@ FocusScope {
|
||||
|
||||
ColorImage {
|
||||
id: errorIcon
|
||||
|
||||
Layout.rightMargin: 4
|
||||
|
||||
visible: root.error && (assistiveText.text.length > 0)
|
||||
source: "/qml/icons/ic-exclamation-circle-filled.svg"
|
||||
color: root.colorScheme.signal_danger
|
||||
height: assistiveText.height
|
||||
source: "/qml/icons/ic-exclamation-circle-filled.svg"
|
||||
sourceSize.height: assistiveText.height
|
||||
visible: root.error && (assistiveText.text.length > 0)
|
||||
}
|
||||
|
||||
Proton.Label {
|
||||
colorScheme: root.colorScheme
|
||||
id: assistiveText
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: root.error ? root.errorString : root.assistiveText
|
||||
|
||||
color: {
|
||||
if (!root.enabled) {
|
||||
return root.colorScheme.text_disabled
|
||||
return root.colorScheme.text_disabled;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = ""
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,54 +1,38 @@
|
||||
// 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 QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.impl
|
||||
import QtQuick.Templates as T
|
||||
import QtQuick.Layouts
|
||||
|
||||
import "." as Proton
|
||||
|
||||
FocusScope {
|
||||
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 activeFocusOnPress: control.activeFocusOnPress
|
||||
property string assistiveText
|
||||
property alias autoScroll: control.autoScroll
|
||||
property alias background: control.background
|
||||
property alias bottomInset: control.bottomInset
|
||||
property alias bottomPadding: control.bottomPadding
|
||||
property alias canPaste: control.canPaste
|
||||
property alias canRedo: control.canRedo
|
||||
property alias canUndo: control.canUndo
|
||||
property alias color: control.color
|
||||
property ColorScheme colorScheme
|
||||
//property alias contentHeight: control.contentHeight
|
||||
//property alias contentWidth: control.contentWidth
|
||||
property alias cursorDelegate: control.cursorDelegate
|
||||
@ -56,24 +40,39 @@ FocusScope {
|
||||
property alias cursorRectangle: control.cursorRectangle
|
||||
property alias cursorVisible: control.cursorVisible
|
||||
property alias displayText: control.displayText
|
||||
property int echoMode: TextInput.Normal
|
||||
property alias effectiveHorizontalAlignment: control.effectiveHorizontalAlignment
|
||||
property bool error: false
|
||||
property string errorString
|
||||
property alias focusReason: control.focusReason
|
||||
property alias font: control.font
|
||||
property alias hint: hint.text
|
||||
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 inputMethodComposing: control.inputMethodComposing
|
||||
property alias inputMethodHints: control.inputMethodHints
|
||||
property alias label: label.text
|
||||
property alias leftInset: control.leftInset
|
||||
property alias leftPadding: control.leftPadding
|
||||
property alias length: control.length
|
||||
property alias maximumLength: control.maximumLength
|
||||
property alias mouseSelectionMode: control.mouseSelectionMode
|
||||
property alias overwriteMode: control.overwriteMode
|
||||
property alias padding: control.padding
|
||||
property alias palette: control.palette
|
||||
property alias passwordCharacter: control.passwordCharacter
|
||||
property alias passwordMaskDelay: control.passwordMaskDelay
|
||||
property alias persistentSelection: control.persistentSelection
|
||||
property alias placeholderText: control.placeholderText
|
||||
property alias placeholderTextColor: control.placeholderTextColor
|
||||
property alias preeditText: control.preeditText
|
||||
property alias readOnly: control.readOnly
|
||||
property alias renderType: control.renderType
|
||||
property alias rightInset: control.rightInset
|
||||
property alias rightPadding: control.rightPadding
|
||||
property alias selectByMouse: control.selectByMouse
|
||||
property alias selectedText: control.selectedText
|
||||
@ -82,47 +81,102 @@ FocusScope {
|
||||
property alias selectionEnd: control.selectionEnd
|
||||
property alias selectionStart: control.selectionStart
|
||||
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
|
||||
// returning an error string in case of error and undefined if no error
|
||||
property var validator
|
||||
property alias verticalAlignment: control.verticalAlignment
|
||||
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
|
||||
implicitWidth: children[0].implicitWidth
|
||||
|
||||
property alias label: label.text
|
||||
property alias hint: hint.text
|
||||
property string assistiveText
|
||||
property string errorString
|
||||
|
||||
property int echoMode: TextInput.Normal
|
||||
|
||||
property bool error: false
|
||||
|
||||
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() }
|
||||
onEditingFinished: {
|
||||
if (!validateOnEditingFinished) {
|
||||
return;
|
||||
}
|
||||
validate();
|
||||
}
|
||||
onTextChanged: {
|
||||
root.error = false;
|
||||
root.errorString = "";
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
@ -133,19 +187,18 @@ FocusScope {
|
||||
spacing: 0
|
||||
|
||||
Proton.Label {
|
||||
colorScheme: root.colorScheme
|
||||
id: label
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
type: Proton.Label.LabelType.Body_semibold
|
||||
}
|
||||
|
||||
Proton.Label {
|
||||
colorScheme: root.colorScheme
|
||||
id: hint
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
color: root.enabled ? root.colorScheme.text_weak : root.colorScheme.text_disabled
|
||||
colorScheme: root.colorScheme
|
||||
horizontalAlignment: Text.AlignRight
|
||||
type: Proton.Label.LabelType.Caption
|
||||
}
|
||||
@ -156,36 +209,29 @@ FocusScope {
|
||||
// will be adjusted to background's width making text field and eye button overlap
|
||||
Rectangle {
|
||||
id: background
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
radius: ProtonStyle.input_radius
|
||||
visible: true
|
||||
color: root.colorScheme.background_norm
|
||||
border.color: {
|
||||
if (!control.enabled) {
|
||||
return root.colorScheme.field_disabled
|
||||
return root.colorScheme.field_disabled;
|
||||
}
|
||||
|
||||
if (control.activeFocus) {
|
||||
return root.colorScheme.interaction_norm
|
||||
return root.colorScheme.interaction_norm;
|
||||
}
|
||||
|
||||
if (root.error) {
|
||||
return root.colorScheme.signal_danger
|
||||
return root.colorScheme.signal_danger;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
implicitWidth: children[0].implicitWidth
|
||||
color: root.colorScheme.background_norm
|
||||
implicitHeight: children[0].implicitHeight
|
||||
implicitWidth: children[0].implicitWidth
|
||||
radius: ProtonStyle.input_radius
|
||||
visible: true
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
@ -193,190 +239,135 @@ FocusScope {
|
||||
|
||||
T.TextField {
|
||||
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.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
|
||||
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
|
||||
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
|
||||
|
||||
// enforcing default focus here within component
|
||||
focus: true
|
||||
|
||||
KeyNavigation.priority: root.KeyNavigation.priority
|
||||
KeyNavigation.backtab: root.KeyNavigation.backtab
|
||||
KeyNavigation.tab: root.KeyNavigation.tab
|
||||
KeyNavigation.up: root.KeyNavigation.up
|
||||
KeyNavigation.down: root.KeyNavigation.down
|
||||
KeyNavigation.left: root.KeyNavigation.left
|
||||
KeyNavigation.right: root.KeyNavigation.right
|
||||
|
||||
font.family: ProtonStyle.font_family
|
||||
font.letterSpacing: ProtonStyle.body_letter_spacing
|
||||
font.pixelSize: ProtonStyle.body_font_size
|
||||
font.weight: ProtonStyle.fontWeight_400
|
||||
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding, placeholder.implicitHeight + topPadding + bottomPadding)
|
||||
implicitWidth: implicitBackgroundWidth + leftInset + rightInset || Math.max(contentWidth, placeholder.implicitWidth) + leftPadding + rightPadding
|
||||
leftPadding: 12
|
||||
placeholderTextColor: control.enabled ? root.colorScheme.text_hint : root.colorScheme.text_disabled
|
||||
rightPadding: 12
|
||||
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 {
|
||||
id: cursor
|
||||
width: 1
|
||||
color: root.colorScheme.interaction_norm
|
||||
visible: control.activeFocus && !control.readOnly && control.selectionStart === control.selectionEnd
|
||||
width: 1
|
||||
|
||||
Connections {
|
||||
target: control
|
||||
function onCursorPositionChanged() {
|
||||
// keep a moving cursor visible
|
||||
cursor.opacity = 1
|
||||
timer.restart()
|
||||
cursor.opacity = 1;
|
||||
timer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
target: control
|
||||
}
|
||||
Timer {
|
||||
id: timer
|
||||
running: control.activeFocus && !control.readOnly
|
||||
repeat: true
|
||||
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
|
||||
onRunningChanged: cursor.opacity = 1
|
||||
onTriggered: cursor.opacity = !cursor.opacity ? 1 : 0
|
||||
}
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
root.accepted();
|
||||
}
|
||||
onEditingFinished: {
|
||||
root.editingFinished();
|
||||
}
|
||||
onTextEdited: {
|
||||
root.textEdited();
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
elide: Text.ElideRight
|
||||
renderType: control.renderType
|
||||
}
|
||||
|
||||
background: Item {
|
||||
implicitWidth: 80
|
||||
implicitHeight: 36
|
||||
visible: false
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
root.accepted()
|
||||
}
|
||||
onEditingFinished: {
|
||||
root.editingFinished()
|
||||
}
|
||||
onTextEdited: {
|
||||
root.textEdited()
|
||||
width: control.width - (control.leftPadding + control.rightPadding)
|
||||
x: control.leftPadding
|
||||
y: control.topPadding
|
||||
}
|
||||
}
|
||||
|
||||
Proton.Button {
|
||||
colorScheme: root.colorScheme
|
||||
id: eyeButton
|
||||
|
||||
Layout.fillHeight: true
|
||||
|
||||
visible: root.echoMode === TextInput.Password
|
||||
icon.color: control.color
|
||||
checkable: true
|
||||
colorScheme: root.colorScheme
|
||||
icon.color: control.color
|
||||
icon.source: checked ? "../icons/ic-eye-slash.svg" : "../icons/ic-eye.svg"
|
||||
visible: root.echoMode === TextInput.Password
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
ColorImage {
|
||||
id: errorIcon
|
||||
|
||||
Layout.rightMargin: 4
|
||||
|
||||
visible: root.error && (assistiveText.text.length > 0)
|
||||
source: "../icons/ic-exclamation-circle-filled.svg"
|
||||
color: root.colorScheme.signal_danger
|
||||
height: assistiveText.lineHeight
|
||||
source: "../icons/ic-exclamation-circle-filled.svg"
|
||||
sourceSize.height: assistiveText.lineHeight
|
||||
visible: root.error && (assistiveText.text.length > 0)
|
||||
}
|
||||
|
||||
Proton.Label {
|
||||
colorScheme: root.colorScheme
|
||||
id: assistiveText
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
text: root.error ? root.errorString : root.assistiveText
|
||||
|
||||
color: {
|
||||
if (!root.enabled) {
|
||||
return root.colorScheme.text_disabled
|
||||
return root.colorScheme.text_disabled;
|
||||
}
|
||||
|
||||
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
|
||||
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 = ""
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,15 @@
|
||||
// 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
|
||||
@ -22,92 +17,106 @@ import QtQuick.Controls.impl
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property var colorScheme
|
||||
|
||||
property bool _disabled: !enabled
|
||||
property bool checked
|
||||
property var colorScheme
|
||||
property bool hovered
|
||||
property bool loading
|
||||
|
||||
signal clicked
|
||||
|
||||
property bool _disabled: !enabled
|
||||
|
||||
implicitHeight: children[0].implicitHeight
|
||||
implicitWidth: children[0].implicitWidth
|
||||
|
||||
Rectangle {
|
||||
id: indicator
|
||||
implicitWidth: 40
|
||||
implicitHeight: 24
|
||||
|
||||
radius: width/2
|
||||
color: {
|
||||
if (root.loading) return "transparent"
|
||||
if (root._disabled) return root.colorScheme.background_strong
|
||||
return root.colorScheme.background_norm
|
||||
}
|
||||
border {
|
||||
width: 1
|
||||
color: (root._disabled || root.loading) ? "transparent" : colorScheme.field_norm
|
||||
if (root.loading)
|
||||
return "transparent";
|
||||
if (root._disabled)
|
||||
return root.colorScheme.background_strong;
|
||||
return root.colorScheme.background_norm;
|
||||
}
|
||||
implicitHeight: 24
|
||||
implicitWidth: 40
|
||||
radius: width / 2
|
||||
|
||||
border {
|
||||
color: (root._disabled || root.loading) ? "transparent" : colorScheme.field_norm
|
||||
width: 1
|
||||
}
|
||||
Rectangle {
|
||||
anchors.verticalCenter: indicator.verticalCenter
|
||||
anchors.left: indicator.left
|
||||
anchors.leftMargin: root.checked ? 16 : 0
|
||||
width: 24
|
||||
height: 24
|
||||
radius: width/2
|
||||
anchors.verticalCenter: indicator.verticalCenter
|
||||
color: {
|
||||
if (root.loading) return "transparent"
|
||||
if (root._disabled) return root.colorScheme.field_disabled
|
||||
|
||||
if (root.loading)
|
||||
return "transparent";
|
||||
if (root._disabled)
|
||||
return root.colorScheme.field_disabled;
|
||||
if (root.checked) {
|
||||
if (root.hovered) return root.colorScheme.interaction_norm_hover
|
||||
return root.colorScheme.interaction_norm
|
||||
if (root.hovered)
|
||||
return root.colorScheme.interaction_norm_hover;
|
||||
return root.colorScheme.interaction_norm;
|
||||
} else {
|
||||
if (root.hovered) return root.colorScheme.field_hover
|
||||
return root.colorScheme.field_norm
|
||||
if (root.hovered)
|
||||
return root.colorScheme.field_hover;
|
||||
return root.colorScheme.field_norm;
|
||||
}
|
||||
}
|
||||
height: 24
|
||||
radius: width / 2
|
||||
width: 24
|
||||
|
||||
ColorImage {
|
||||
anchors.centerIn: parent
|
||||
source: "/qml/icons/ic-check.svg"
|
||||
color: root.colorScheme.background_norm
|
||||
height: root.colorScheme.body_font_size
|
||||
source: "/qml/icons/ic-check.svg"
|
||||
sourceSize.height: root.colorScheme.body_font_size
|
||||
visible: root.checked
|
||||
}
|
||||
}
|
||||
|
||||
ColorImage {
|
||||
id: loader
|
||||
anchors.centerIn: parent
|
||||
source: "/qml/icons/Loader_16.svg"
|
||||
color: root.colorScheme.text_norm
|
||||
height: root.colorScheme.body_font_size
|
||||
source: "/qml/icons/Loader_16.svg"
|
||||
sourceSize.height: root.colorScheme.body_font_size
|
||||
visible: root.loading
|
||||
|
||||
RotationAnimation {
|
||||
target: loader
|
||||
loops: Animation.Infinite
|
||||
direction: RotationAnimation.Clockwise
|
||||
duration: 1000
|
||||
from: 0
|
||||
to: 360
|
||||
direction: RotationAnimation.Clockwise
|
||||
loops: Animation.Infinite
|
||||
running: root.loading
|
||||
target: loader
|
||||
to: 360
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: indicator
|
||||
hoverEnabled: true
|
||||
onEntered: {root.hovered = true }
|
||||
onExited: {root.hovered = false }
|
||||
onClicked: { if (root.enabled) root.clicked();}
|
||||
onPressed: {root.hovered = true }
|
||||
onReleased: { root.hovered = containsMouse }
|
||||
|
||||
onClicked: {
|
||||
if (root.enabled)
|
||||
root.clicked();
|
||||
}
|
||||
onEntered: {
|
||||
root.hovered = true;
|
||||
}
|
||||
onExited: {
|
||||
root.hovered = false;
|
||||
}
|
||||
onPressed: {
|
||||
root.hovered = true;
|
||||
}
|
||||
onReleased: {
|
||||
root.hovered = containsMouse;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
254
internal/frontend/bridge-gui/bridge-gui/qml/QuestionItem.qml
Normal file
254
internal/frontend/bridge-gui/bridge-gui/qml/QuestionItem.qml
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -1,51 +1,44 @@
|
||||
// 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 colorScheme
|
||||
|
||||
property string text: "Text"
|
||||
property string actionText: "Action"
|
||||
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
|
||||
enum ActionType {
|
||||
Toggle = 1,
|
||||
Button,
|
||||
PrimaryButton
|
||||
}
|
||||
|
||||
property var _bottomMargin: 20
|
||||
property var _lineWidth: 1
|
||||
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
|
||||
|
||||
enum ActionType {
|
||||
Toggle = 1, Button = 2, PrimaryButton = 3
|
||||
}
|
||||
|
||||
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
|
||||
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
|
||||
|
||||
@ -54,10 +47,9 @@ Item {
|
||||
spacing: 16
|
||||
|
||||
ColumnLayout {
|
||||
Layout.bottomMargin: root._bottomMargin
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: root._bottomMargin
|
||||
|
||||
spacing: 4
|
||||
|
||||
Label {
|
||||
@ -66,51 +58,51 @@ Item {
|
||||
text: root.text
|
||||
type: Label.Body_semibold
|
||||
}
|
||||
|
||||
Label {
|
||||
id: descriptionLabel
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
Layout.preferredWidth: parent.width
|
||||
|
||||
wrapMode: Text.WordWrap
|
||||
color: root.colorScheme.text_weak
|
||||
colorScheme: root.colorScheme
|
||||
text: root.description
|
||||
color: root.colorScheme.text_weak
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
Toggle {
|
||||
id: toggle
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.topMargin: root._toggleTopMargin
|
||||
id: toggle
|
||||
checked: root.checked
|
||||
colorScheme: root.colorScheme
|
||||
loading: root.loading
|
||||
visible: root.type === SettingsItem.ActionType.Toggle
|
||||
|
||||
checked: root.checked
|
||||
loading: root.loading
|
||||
onClicked: { if (!root.loading) root.clicked() }
|
||||
onClicked: {
|
||||
if (!root.loading)
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
id: button
|
||||
Layout.alignment: Qt.AlignTop
|
||||
colorScheme: root.colorScheme
|
||||
visible: root.type === SettingsItem.Button || root.type === SettingsItem.PrimaryButton
|
||||
text: root.actionText + (root.actionIcon != "" ? " " : "")
|
||||
loading: root.loading
|
||||
icon.source: root.actionIcon
|
||||
onClicked: { if (!root.loading) root.clicked() }
|
||||
loading: root.loading
|
||||
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 {
|
||||
anchors.bottom: root.bottom
|
||||
anchors.left: root.left
|
||||
anchors.right: root.right
|
||||
anchors.bottom: root.bottom
|
||||
color: colorScheme.border_weak
|
||||
height: root._lineWidth
|
||||
visible: root.showSeparator
|
||||
|
||||
@ -1,61 +1,57 @@
|
||||
// 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 QtQuick.Controls.impl
|
||||
|
||||
import Proton
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var colorScheme
|
||||
default property alias items: content.children
|
||||
|
||||
signal back()
|
||||
|
||||
property int _bottomMargin: 32
|
||||
property int _leftMargin: 64
|
||||
property int _rightMargin: 64
|
||||
property int _topMargin: 32
|
||||
property int _bottomMargin: 32
|
||||
property int _spacing: 20
|
||||
property int _topMargin: 32
|
||||
property var colorScheme
|
||||
|
||||
// fillHeight indicates whether the SettingsView should fill all available explicit height set
|
||||
property bool fillHeight: false
|
||||
default property alias items: content.children
|
||||
|
||||
function positionViewAtBegining() {
|
||||
scrollView.ScrollBar.vertical.position = 0
|
||||
}
|
||||
signal back
|
||||
|
||||
ScrollView {
|
||||
id: scrollView
|
||||
anchors.fill: parent
|
||||
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.
|
||||
|
||||
Item {
|
||||
// can't use parent here because parent is not ScrollView (Flickable inside contentItem inside ScrollView)
|
||||
width: scrollView.availableWidth
|
||||
height: scrollView.availableHeight
|
||||
|
||||
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
|
||||
// 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
|
||||
implicitWidth: width
|
||||
// can't use parent here because parent is not ScrollView (Flickable inside contentItem inside ScrollView)
|
||||
width: scrollView.availableWidth
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
@ -63,16 +59,13 @@ Item {
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
spacing: root._spacing
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
Layout.topMargin: root._topMargin
|
||||
Layout.bottomMargin: root._bottomMargin
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: root._leftMargin
|
||||
Layout.rightMargin: root._rightMargin
|
||||
Layout.topMargin: root._topMargin
|
||||
spacing: root._spacing
|
||||
}
|
||||
|
||||
Item {
|
||||
id: filler
|
||||
Layout.fillHeight: true
|
||||
@ -81,19 +74,20 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: backButton
|
||||
anchors {
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
topMargin: root._topMargin
|
||||
leftMargin: (root._leftMargin-backButton.width) / 2
|
||||
}
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: root.back()
|
||||
horizontalPadding: 8
|
||||
icon.source: "/qml/icons/ic-arrow-left.svg"
|
||||
secondary: true
|
||||
horizontalPadding: 8
|
||||
|
||||
onClicked: root.back()
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: (root._leftMargin - backButton.width) / 2
|
||||
top: parent.top
|
||||
topMargin: root._topMargin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,110 +1,139 @@
|
||||
// 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 QtQuick.Controls.impl
|
||||
|
||||
import Proton
|
||||
|
||||
Item {
|
||||
id:root
|
||||
id: root
|
||||
|
||||
property string address
|
||||
property ColorScheme colorScheme
|
||||
property var user
|
||||
property string address
|
||||
|
||||
signal dismissed()
|
||||
signal finished()
|
||||
signal dismissed
|
||||
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
|
||||
implicitWidth: children[0].implicitWidth
|
||||
|
||||
|
||||
ListModel {
|
||||
id: clients
|
||||
property string name : "Apple Mail"
|
||||
property string iconSource : "/qml/icons/ic-apple-mail.svg"
|
||||
|
||||
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 name: "Apple Mail"
|
||||
|
||||
Component.onCompleted : {
|
||||
if (Backend.goos == "darwin") {
|
||||
Component.onCompleted: {
|
||||
if (Backend.goos === "darwin") {
|
||||
append({
|
||||
"name" : "Apple Mail",
|
||||
"iconSource" : "/qml/icons/ic-apple-mail.svg",
|
||||
"haveAutoSetup" : true,
|
||||
"link" : "https://proton.me/support/protonmail-bridge-clients-apple-mail"
|
||||
})
|
||||
"name": "Apple Mail",
|
||||
"iconSource": "/qml/icons/ic-apple-mail.svg",
|
||||
"haveAutoSetup": true,
|
||||
"link": "https://proton.me/support/protonmail-bridge-clients-apple-mail"
|
||||
});
|
||||
append({
|
||||
"name" : "Microsoft Outlook",
|
||||
"iconSource" : "/qml/icons/ic-microsoft-outlook.svg",
|
||||
"haveAutoSetup" : false,
|
||||
"link" : "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2019"
|
||||
})
|
||||
"name": "Microsoft Outlook",
|
||||
"iconSource": "/qml/icons/ic-microsoft-outlook.svg",
|
||||
"haveAutoSetup": false,
|
||||
"link": "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2019"
|
||||
});
|
||||
}
|
||||
if (Backend.goos == "windows") {
|
||||
if (Backend.goos === "windows") {
|
||||
append({
|
||||
"name" : "Microsoft Outlook",
|
||||
"iconSource" : "/qml/icons/ic-microsoft-outlook.svg",
|
||||
"haveAutoSetup" : false,
|
||||
"link" : "https://proton.me/support/protonmail-bridge-clients-windows-outlook-2019"
|
||||
})
|
||||
"name": "Microsoft Outlook",
|
||||
"iconSource": "/qml/icons/ic-microsoft-outlook.svg",
|
||||
"haveAutoSetup": false,
|
||||
"link": "https://proton.me/support/protonmail-bridge-clients-windows-outlook-2019"
|
||||
});
|
||||
}
|
||||
|
||||
append({
|
||||
"name" : "Mozilla Thunderbird",
|
||||
"iconSource" : "/qml/icons/ic-mozilla-thunderbird.svg",
|
||||
"haveAutoSetup" : false,
|
||||
"link" : "https://proton.me/support/protonmail-bridge-clients-windows-thunderbird"
|
||||
})
|
||||
|
||||
"name": "Mozilla Thunderbird",
|
||||
"iconSource": "/qml/icons/ic-mozilla-thunderbird.svg",
|
||||
"haveAutoSetup": false,
|
||||
"link": "https://proton.me/support/protonmail-bridge-clients-windows-thunderbird"
|
||||
});
|
||||
append({
|
||||
"name" : "Other",
|
||||
"iconSource" : "/qml/icons/ic-other-mail-clients.svg",
|
||||
"haveAutoSetup" : false,
|
||||
"link" : "https://proton.me/support/protonmail-bridge-configure-client"
|
||||
})
|
||||
|
||||
"name": "Other",
|
||||
"iconSource": "/qml/icons/ic-other-mail-clients.svg",
|
||||
"haveAutoSetup": false,
|
||||
"link": "https://proton.me/support/protonmail-bridge-configure-client"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: root
|
||||
color: root.colorScheme.background_norm
|
||||
}
|
||||
|
||||
StackLayout {
|
||||
id: guidePages
|
||||
anchors.bottomMargin: 70
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 80
|
||||
anchors.rightMargin: 80
|
||||
anchors.topMargin: 30
|
||||
anchors.bottomMargin: 70
|
||||
|
||||
|
||||
ColumnLayout { // 0: Client selection
|
||||
ColumnLayout {
|
||||
// 0: Client selection
|
||||
id: clientView
|
||||
Layout.fillHeight: true
|
||||
|
||||
property int columnWidth: 268
|
||||
|
||||
Layout.fillHeight: true
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
@ -112,16 +141,14 @@ Item {
|
||||
text: qsTr("Setting up email client")
|
||||
type: Label.LabelType.Heading
|
||||
}
|
||||
|
||||
Label {
|
||||
color: root.colorScheme.text_weak
|
||||
colorScheme: root.colorScheme
|
||||
text: address
|
||||
color: root.colorScheme.text_weak
|
||||
type: Label.LabelType.Lead
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: 32-clientView.spacing
|
||||
Layout.topMargin: 32 - clientView.spacing
|
||||
spacing: 24
|
||||
|
||||
ColumnLayout {
|
||||
@ -134,185 +161,133 @@ Item {
|
||||
text: qsTr("Choose an email client")
|
||||
type: Label.LabelType.Body_semibold
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: clientList
|
||||
Layout.fillHeight: true
|
||||
model: clients
|
||||
width: clientView.columnWidth
|
||||
|
||||
model: clients
|
||||
|
||||
highlight: Rectangle {
|
||||
color: root.colorScheme.interaction_default_active
|
||||
radius: ProtonStyle.context_item_radius
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
implicitWidth: clientRow.width
|
||||
implicitHeight: clientRow.height
|
||||
implicitWidth: clientRow.width
|
||||
|
||||
ColumnLayout {
|
||||
id: clientRow
|
||||
width: clientList.width
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: 12
|
||||
Layout.bottomMargin: 12
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 12
|
||||
|
||||
ColorImage {
|
||||
source: model.iconSource
|
||||
height: 36
|
||||
source: model.iconSource
|
||||
sourceSize.height: 36
|
||||
}
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
Layout.leftMargin: 12
|
||||
colorScheme: root.colorScheme
|
||||
text: model.name
|
||||
type: Label.LabelType.Body
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: root.colorScheme.border_weak
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
clientList.currentIndex = index
|
||||
clientList.currentIndex = index;
|
||||
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 {
|
||||
id: actionColumn
|
||||
visible: clientList.currentIndex >= 0 && clients.get(clientList.currentIndex).haveAutoSetup
|
||||
Layout.alignment: Qt.AlignTop
|
||||
visible: clientList.currentIndex >= 0 && clients.get(clientList.currentIndex).haveAutoSetup
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Choose configuration mode")
|
||||
type: Label.LabelType.Body_semibold
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: actionList
|
||||
Layout.fillHeight: true
|
||||
model: [qsTr("Configure automatically"), qsTr("Configure manually")]
|
||||
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 {
|
||||
implicitWidth: children[0].width
|
||||
implicitHeight: children[0].height
|
||||
implicitWidth: children[0].width
|
||||
|
||||
ColumnLayout {
|
||||
width: actionList.width
|
||||
|
||||
Label {
|
||||
Layout.topMargin: 20
|
||||
Layout.bottomMargin: 20
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 20
|
||||
colorScheme: root.colorScheme
|
||||
text: modelData
|
||||
type: Label.LabelType.Body
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: root.colorScheme.border_weak
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
actionList.currentIndex = index
|
||||
root.setupAction(index,clientList.currentIndex)
|
||||
actionList.currentIndex = index;
|
||||
root.setupAction(index, clientList.currentIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
highlight: Rectangle {
|
||||
color: root.colorScheme.interaction_default_active
|
||||
radius: ProtonStyle.context_item_radius
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Set up later")
|
||||
flat: true
|
||||
text: qsTr("Set up later")
|
||||
|
||||
onClicked: {
|
||||
root.setupAction(-1,-1)
|
||||
root.setupAction(-1, -1);
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,473 +1,413 @@
|
||||
// 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 QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.impl
|
||||
|
||||
import Proton
|
||||
|
||||
FocusScope {
|
||||
id: root
|
||||
|
||||
property ColorScheme colorScheme
|
||||
|
||||
function reset() {
|
||||
stackLayout.currentIndex = 0
|
||||
loginNormalLayout.reset()
|
||||
login2FALayout.reset()
|
||||
login2PasswordLayout.reset()
|
||||
|
||||
}
|
||||
property alias currentIndex: stackLayout.currentIndex
|
||||
property alias username: usernameTextField.text
|
||||
|
||||
function abort() {
|
||||
root.reset()
|
||||
Backend.loginAbort(usernameTextField.text)
|
||||
root.reset();
|
||||
Backend.loginAbort(usernameTextField.text);
|
||||
}
|
||||
function reset() {
|
||||
stackLayout.currentIndex = 0;
|
||||
loginNormalLayout.reset();
|
||||
login2FALayout.reset();
|
||||
login2PasswordLayout.reset();
|
||||
}
|
||||
|
||||
implicitHeight: children[0].implicitHeight
|
||||
implicitWidth: children[0].implicitWidth
|
||||
|
||||
property alias username: usernameTextField.text
|
||||
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 {
|
||||
id: stackLayout
|
||||
function loginFailed() {
|
||||
signInButton.loading = false;
|
||||
usernameTextField.enabled = true;
|
||||
usernameTextField.error = true;
|
||||
passwordTextField.enabled = true;
|
||||
passwordTextField.error = true;
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
function loginFailed() {
|
||||
signInButton.loading = false
|
||||
|
||||
usernameTextField.enabled = true
|
||||
usernameTextField.error = true
|
||||
|
||||
passwordTextField.enabled = true
|
||||
passwordTextField.error = true
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Backend
|
||||
|
||||
function onLoginUsernamePasswordError(errorMsg) {
|
||||
console.assert(stackLayout.currentIndex == 0, "Unexpected loginUsernamePasswordError")
|
||||
console.assert(signInButton.loading == true, "Unexpected loginUsernamePasswordError")
|
||||
|
||||
stackLayout.loginFailed()
|
||||
if (errorMsg!="") errorLabel.text = errorMsg
|
||||
else errorLabel.text = qsTr("Incorrect login credentials")
|
||||
function onLogin2FAError(_) {
|
||||
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 onLoginFreeUserError() {
|
||||
console.assert(stackLayout.currentIndex == 0, "Unexpected loginFreeUserError")
|
||||
stackLayout.loginFailed()
|
||||
function onLogin2FAErrorAbort(_) {
|
||||
console.assert(stackLayout.currentIndex === 1, "Unexpected login2FAErrorAbort");
|
||||
root.reset();
|
||||
errorLabel.text = qsTr("Incorrect login credentials. Please try again.");
|
||||
}
|
||||
|
||||
function onLoginConnectionError(errorMsg) {
|
||||
if (stackLayout.currentIndex == 0 ) {
|
||||
stackLayout.loginFailed()
|
||||
function onLogin2FARequested(username) {
|
||||
console.assert(stackLayout.currentIndex === 0, "Unexpected login2FARequested");
|
||||
twoFactorUsernameLabel.text = username;
|
||||
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 onLogin2FARequested(username) {
|
||||
console.assert(stackLayout.currentIndex == 0, "Unexpected login2FARequested")
|
||||
twoFactorUsernameLabel.text = username
|
||||
stackLayout.currentIndex = 1
|
||||
twoFactorPasswordTextField.focus = true
|
||||
function onLoginFinished(_) {
|
||||
stackLayout.currentIndex = 0;
|
||||
root.reset();
|
||||
}
|
||||
function onLoginFreeUserError() {
|
||||
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) {
|
||||
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()
|
||||
}
|
||||
target: Backend
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: loginNormalLayout
|
||||
|
||||
function reset() {
|
||||
signInButton.loading = false
|
||||
|
||||
errorLabel.text = ""
|
||||
|
||||
usernameTextField.enabled = true
|
||||
usernameTextField.error = false
|
||||
usernameTextField.errorString = ""
|
||||
usernameTextField.focus = true
|
||||
|
||||
passwordTextField.enabled = true
|
||||
passwordTextField.error = false
|
||||
passwordTextField.errorString = ""
|
||||
passwordTextField.text = ""
|
||||
signInButton.loading = false;
|
||||
errorLabel.text = "";
|
||||
usernameTextField.enabled = true;
|
||||
usernameTextField.error = false;
|
||||
usernameTextField.errorString = "";
|
||||
usernameTextField.focus = true;
|
||||
passwordTextField.enabled = true;
|
||||
passwordTextField.error = false;
|
||||
passwordTextField.errorString = "";
|
||||
passwordTextField.text = "";
|
||||
}
|
||||
|
||||
spacing: 0
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Sign in")
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: 16
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Sign in")
|
||||
type: Label.LabelType.Title
|
||||
}
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
id: subTitle
|
||||
text: qsTr("Enter your Proton Account details.")
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: 8
|
||||
color: root.colorScheme.text_weak
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Enter your Proton Account details.")
|
||||
type: Label.LabelType.Body
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 36
|
||||
|
||||
spacing: 0
|
||||
visible: errorLabel.text.length > 0
|
||||
|
||||
ColorImage {
|
||||
color: root.colorScheme.signal_danger
|
||||
source: "/qml/icons/ic-exclamation-circle-filled.svg"
|
||||
height: errorLabel.lineHeight
|
||||
source: "/qml/icons/ic-exclamation-circle-filled.svg"
|
||||
sourceSize.height: errorLabel.lineHeight
|
||||
}
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
id: errorLabel
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true;
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 4
|
||||
color: root.colorScheme.signal_danger
|
||||
|
||||
colorScheme: root.colorScheme
|
||||
type: root.error ? Label.LabelType.Caption_semibold : Label.LabelType.Caption
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
TextField {
|
||||
colorScheme: root.colorScheme
|
||||
id: usernameTextField
|
||||
label: qsTr("Email or username")
|
||||
focus: true
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
colorScheme: root.colorScheme
|
||||
focus: true
|
||||
label: qsTr("Email or username")
|
||||
validateOnEditingFinished: false
|
||||
|
||||
onTextChanged: {
|
||||
// remove "invalid username / password error"
|
||||
if (error || errorLabel.text.length > 0) {
|
||||
errorLabel.text = ""
|
||||
usernameTextField.error = false
|
||||
passwordTextField.error = false
|
||||
}
|
||||
}
|
||||
|
||||
validator: function(str) {
|
||||
validator: function (str) {
|
||||
if (str.length === 0) {
|
||||
return qsTr("Enter email or username")
|
||||
return qsTr("Enter email or username");
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
onAccepted: passwordTextField.forceActiveFocus()
|
||||
}
|
||||
|
||||
TextField {
|
||||
colorScheme: root.colorScheme
|
||||
id: passwordTextField
|
||||
label: qsTr("Password")
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
echoMode: TextInput.Password
|
||||
validateOnEditingFinished: false
|
||||
|
||||
onTextChanged: {
|
||||
// remove "invalid username / password error"
|
||||
if (error || errorLabel.text.length > 0) {
|
||||
errorLabel.text = ""
|
||||
usernameTextField.error = false
|
||||
passwordTextField.error = false
|
||||
errorLabel.text = "";
|
||||
usernameTextField.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) {
|
||||
return qsTr("Enter password")
|
||||
return qsTr("Enter password");
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
onAccepted: signInButton.checkAndSignIn()
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
id: signInButton
|
||||
text: loading ? qsTr("Signing in") : qsTr("Sign in")
|
||||
enabled: !loading
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
|
||||
|
||||
onClicked: checkAndSignIn()
|
||||
|
||||
function checkAndSignIn() {
|
||||
usernameTextField.validate()
|
||||
passwordTextField.validate()
|
||||
|
||||
if (usernameTextField.error || passwordTextField.error) {
|
||||
return
|
||||
onTextChanged: {
|
||||
// remove "invalid username / password error"
|
||||
if (error || errorLabel.text.length > 0) {
|
||||
errorLabel.text = "";
|
||||
usernameTextField.error = false;
|
||||
passwordTextField.error = false;
|
||||
}
|
||||
|
||||
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
|
||||
textFormat: Text.StyledText
|
||||
text: link("https://proton.me/mail/pricing", qsTr("Create or upgrade your account"))
|
||||
enabled: !loading
|
||||
text: loading ? qsTr("Signing in") : qsTr("Sign in")
|
||||
|
||||
onClicked: {
|
||||
checkAndSignIn();
|
||||
}
|
||||
}
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
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
|
||||
|
||||
onLinkActivated: {
|
||||
Qt.openUrlExternally(link)
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: login2FALayout
|
||||
|
||||
function reset() {
|
||||
twoFAButton.loading = false
|
||||
|
||||
twoFactorPasswordTextField.enabled = true
|
||||
twoFactorPasswordTextField.error = false
|
||||
twoFactorPasswordTextField.errorString = ""
|
||||
twoFactorPasswordTextField.text = ""
|
||||
twoFAButton.loading = false;
|
||||
twoFactorPasswordTextField.enabled = true;
|
||||
twoFactorPasswordTextField.error = false;
|
||||
twoFactorPasswordTextField.errorString = "";
|
||||
twoFactorPasswordTextField.text = "";
|
||||
}
|
||||
|
||||
spacing: 0
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.topMargin: 16
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Two-factor authentication")
|
||||
Layout.topMargin: 16
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
type: Label.LabelType.Heading
|
||||
}
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
id: twoFactorUsernameLabel
|
||||
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.topMargin: 8
|
||||
type: Label.LabelType.Lead
|
||||
color: root.colorScheme.text_weak
|
||||
}
|
||||
|
||||
TextField {
|
||||
colorScheme: root.colorScheme
|
||||
type: Label.LabelType.Lead
|
||||
}
|
||||
TextField {
|
||||
id: twoFactorPasswordTextField
|
||||
label: qsTr("Two-factor code")
|
||||
assistiveText: qsTr("Enter the 6-digit code")
|
||||
validateOnEditingFinished: false
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 32
|
||||
|
||||
validator: function(str) {
|
||||
assistiveText: qsTr("Enter the 6-digit code")
|
||||
colorScheme: root.colorScheme
|
||||
label: qsTr("Two-factor code")
|
||||
validateOnEditingFinished: false
|
||||
validator: function (str) {
|
||||
if (str.length === 0) {
|
||||
return qsTr("Enter the 6-digit code")
|
||||
}
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
if (text.length >= 6) {
|
||||
twoFAButton.onClicked()
|
||||
return qsTr("Enter the 6-digit code");
|
||||
}
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
twoFAButton.onClicked()
|
||||
twoFAButton.onClicked();
|
||||
}
|
||||
onTextChanged: {
|
||||
if (text.length >= 6) {
|
||||
twoFAButton.onClicked();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
id: twoFAButton
|
||||
text: loading ? qsTr("Authenticating") : qsTr("Authenticate")
|
||||
enabled: !loading
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
colorScheme: root.colorScheme
|
||||
enabled: !loading
|
||||
text: loading ? qsTr("Authenticating") : qsTr("Authenticate")
|
||||
|
||||
onClicked: {
|
||||
twoFactorPasswordTextField.validate()
|
||||
|
||||
twoFactorPasswordTextField.validate();
|
||||
if (twoFactorPasswordTextField.error) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
twoFactorPasswordTextField.enabled = false
|
||||
loading = true
|
||||
Backend.login2FA(usernameTextField.text, Qt.btoa(twoFactorPasswordTextField.text))
|
||||
twoFactorPasswordTextField.enabled = false;
|
||||
loading = true;
|
||||
Backend.login2FA(usernameTextField.text, Qt.btoa(twoFactorPasswordTextField.text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: login2PasswordLayout
|
||||
|
||||
function reset() {
|
||||
secondPasswordButton.loading = false
|
||||
|
||||
secondPasswordTextField.enabled = true
|
||||
secondPasswordTextField.error = false
|
||||
secondPasswordTextField.errorString = ""
|
||||
secondPasswordTextField.text = ""
|
||||
secondPasswordButton.loading = false;
|
||||
secondPasswordTextField.enabled = true;
|
||||
secondPasswordTextField.error = false;
|
||||
secondPasswordTextField.errorString = "";
|
||||
secondPasswordTextField.text = "";
|
||||
}
|
||||
|
||||
spacing: 0
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.topMargin: 16
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Unlock your mailbox")
|
||||
Layout.topMargin: 16
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
type: Label.LabelType.Heading
|
||||
}
|
||||
|
||||
TextField {
|
||||
colorScheme: root.colorScheme
|
||||
id: secondPasswordTextField
|
||||
label: qsTr("Mailbox password")
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8 + implicitHeight + 24 + subTitle.implicitHeight
|
||||
colorScheme: root.colorScheme
|
||||
echoMode: TextInput.Password
|
||||
label: qsTr("Mailbox password")
|
||||
validateOnEditingFinished: false
|
||||
|
||||
validator: function(str) {
|
||||
validator: function (str) {
|
||||
if (str.length === 0) {
|
||||
return qsTr("Enter password")
|
||||
return qsTr("Enter password");
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
secondPasswordButton.onClicked()
|
||||
secondPasswordButton.onClicked();
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
id: secondPasswordButton
|
||||
text: loading ? qsTr("Unlocking") : qsTr("Unlock")
|
||||
enabled: !loading
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
colorScheme: root.colorScheme
|
||||
enabled: !loading
|
||||
text: loading ? qsTr("Unlocking") : qsTr("Unlock")
|
||||
|
||||
onClicked: {
|
||||
secondPasswordTextField.validate()
|
||||
|
||||
secondPasswordTextField.validate();
|
||||
if (secondPasswordTextField.error) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
secondPasswordTextField.enabled = false
|
||||
loading = true
|
||||
Backend.login2Password(usernameTextField.text, Qt.btoa(secondPasswordTextField.text))
|
||||
secondPasswordTextField.enabled = false;
|
||||
loading = true;
|
||||
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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,194 +1,169 @@
|
||||
// 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 QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.impl
|
||||
|
||||
import Proton
|
||||
|
||||
Dialog {
|
||||
id: root
|
||||
|
||||
shouldShow: Backend.showSplashScreen
|
||||
leftPadding: 0
|
||||
modal: true
|
||||
|
||||
topPadding : 0
|
||||
leftPadding : 0
|
||||
rightPadding : 0
|
||||
rightPadding: 0
|
||||
shouldShow: Backend.showSplashScreen
|
||||
topPadding: 0
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 20
|
||||
|
||||
Image {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
sourceSize.width: 384
|
||||
sourceSize.height: 144
|
||||
|
||||
Layout.preferredWidth: 384
|
||||
Layout.preferredHeight: 144
|
||||
|
||||
Layout.preferredWidth: 384
|
||||
source: "./icons/img-splash.png"
|
||||
sourceSize.height: 144
|
||||
sourceSize.width: 384
|
||||
}
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme;
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter;
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.leftMargin: 24
|
||||
Layout.rightMargin: 24
|
||||
Layout.preferredWidth: 336
|
||||
|
||||
type: Label.Title
|
||||
Layout.rightMargin: 24
|
||||
colorScheme: root.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("What's new in Bridge")
|
||||
type: Label.Title
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
width: root.width
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
width: 24
|
||||
Layout.leftMargin: 32
|
||||
Layout.rightMargin: 16
|
||||
width: 24
|
||||
|
||||
Image {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
sourceSize.width: 24
|
||||
sourceSize.height: 24
|
||||
source: "./icons/ic-splash-check.svg"
|
||||
sourceSize.height: 24
|
||||
sourceSize.width: 24
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter;
|
||||
Layout.preferredWidth: 264
|
||||
Layout.leftMargin: 0
|
||||
Layout.preferredWidth: 264
|
||||
Layout.rightMargin: 24
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
type: Label.Body
|
||||
colorScheme: root.colorScheme
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
textFormat: Text.StyledText
|
||||
text: qsTr("<b>New IMAP engine</b><br/>For improved stability and performance.")
|
||||
textFormat: Text.StyledText
|
||||
type: Label.Body
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
width: root.width
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
width: 24
|
||||
Layout.leftMargin: 32
|
||||
Layout.rightMargin: 16
|
||||
width: 24
|
||||
|
||||
Image {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
sourceSize.width: 24
|
||||
sourceSize.height: 24
|
||||
source: "./icons/ic-splash-check.svg"
|
||||
sourceSize.height: 24
|
||||
sourceSize.width: 24
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter;
|
||||
Layout.preferredWidth: 264
|
||||
Layout.leftMargin: 0
|
||||
Layout.preferredWidth: 264
|
||||
Layout.rightMargin: 24
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
type: Label.Body
|
||||
colorScheme: root.colorScheme
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
textFormat: Text.StyledText
|
||||
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 {
|
||||
width: root.width
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
width: 24
|
||||
Layout.leftMargin: 32
|
||||
Layout.rightMargin: 16
|
||||
width: 24
|
||||
|
||||
Image {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
sourceSize.width: 24
|
||||
sourceSize.height: 24
|
||||
source: "./icons/ic-splash-check.svg"
|
||||
sourceSize.height: 24
|
||||
sourceSize.width: 24
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter;
|
||||
Layout.preferredWidth: 264
|
||||
Layout.leftMargin: 0
|
||||
Layout.preferredWidth: 264
|
||||
Layout.rightMargin: 24
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
type: Label.Body
|
||||
colorScheme: root.colorScheme
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
textFormat: Text.StyledText
|
||||
text: qsTr("<b>Extra security</b><br/>New, encrypted local database and keychain improvements.")
|
||||
textFormat: Text.StyledText
|
||||
type: Label.Body
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 24
|
||||
Layout.rightMargin: 24
|
||||
colorScheme: root.colorScheme
|
||||
text: "Got it"
|
||||
|
||||
onClicked: Backend.showSplashScreen = false
|
||||
}
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter;
|
||||
Layout.preferredWidth: 336
|
||||
Layout.leftMargin: 24
|
||||
Layout.preferredWidth: 336
|
||||
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
|
||||
|
||||
type: Label.Body
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
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) }
|
||||
onLinkActivated: function (link) {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,111 +1,93 @@
|
||||
|
||||
// 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 QtQuick.Controls.impl
|
||||
|
||||
import Proton
|
||||
import Notifications
|
||||
|
||||
Item {
|
||||
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
|
||||
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
|
||||
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
|
||||
|
||||
NotificationFilter {
|
||||
id: notificationFilter
|
||||
|
||||
blacklist: root.notificationBlacklist
|
||||
source: root.notifications ? root.notifications.all : undefined
|
||||
whitelist: root.notificationWhitelist
|
||||
blacklist: root.notificationBlacklist
|
||||
|
||||
onTopmostChanged: {
|
||||
if (!topmost) {
|
||||
image.source = "/qml/icons/ic-connected.svg"
|
||||
image.color = root.colorScheme.signal_success
|
||||
label.text = qsTr("Connected")
|
||||
label.color = root.colorScheme.signal_success
|
||||
image.source = "/qml/icons/ic-connected.svg";
|
||||
image.color = root.colorScheme.signal_success;
|
||||
label.text = qsTr("Connected");
|
||||
label.color = root.colorScheme.signal_success;
|
||||
return;
|
||||
}
|
||||
|
||||
image.source = topmost.icon
|
||||
label.text = topmost.brief
|
||||
|
||||
image.source = topmost.icon;
|
||||
label.text = topmost.brief;
|
||||
switch (topmost.type) {
|
||||
case Notification.NotificationType.Danger:
|
||||
image.color = root.colorScheme.signal_danger
|
||||
label.color = root.colorScheme.signal_danger
|
||||
case Notification.NotificationType.Danger:
|
||||
image.color = root.colorScheme.signal_danger;
|
||||
label.color = root.colorScheme.signal_danger;
|
||||
break;
|
||||
case Notification.NotificationType.Warning:
|
||||
image.color = root.colorScheme.signal_warning
|
||||
label.color = root.colorScheme.signal_warning
|
||||
case Notification.NotificationType.Warning:
|
||||
image.color = root.colorScheme.signal_warning;
|
||||
label.color = root.colorScheme.signal_warning;
|
||||
break;
|
||||
case Notification.NotificationType.Success:
|
||||
image.color = root.colorScheme.signal_success
|
||||
label.color = root.colorScheme.signal_success
|
||||
case Notification.NotificationType.Success:
|
||||
image.color = root.colorScheme.signal_success;
|
||||
label.color = root.colorScheme.signal_success;
|
||||
break;
|
||||
case Notification.NotificationType.Info:
|
||||
image.color = root.colorScheme.signal_info
|
||||
label.color = root.colorScheme.signal_info
|
||||
case Notification.NotificationType.Info:
|
||||
image.color = root.colorScheme.signal_info;
|
||||
label.color = root.colorScheme.signal_info;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 8
|
||||
|
||||
ColorImage {
|
||||
id: image
|
||||
width: 16
|
||||
height: 16
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
source: "/qml/icons/ic-connected.svg"
|
||||
color: root.colorScheme.signal_success
|
||||
height: 16
|
||||
source: "/qml/icons/ic-connected.svg"
|
||||
sourceSize.height: height
|
||||
sourceSize.width: width
|
||||
width: 16
|
||||
}
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
id: label
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
text: qsTr("Connected")
|
||||
color: root.colorScheme.signal_success
|
||||
colorScheme: root.colorScheme
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: qsTr("Connected")
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,25 +1,19 @@
|
||||
// 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 QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
import Proton
|
||||
|
||||
Item {
|
||||
@ -34,24 +28,46 @@ Item {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
Rectangle {
|
||||
color: root.colorScheme.background_norm
|
||||
states: [
|
||||
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.fillWidth: true
|
||||
|
||||
color: root.colorScheme.background_norm
|
||||
implicitHeight: children[0].implicitHeight
|
||||
implicitWidth: children[0].implicitWidth
|
||||
|
||||
visible: signInItem.currentIndex == 0
|
||||
visible: signInItem.currentIndex === 0
|
||||
|
||||
GridLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
columnSpacing: 0
|
||||
rowSpacing: 0
|
||||
|
||||
columns: 3
|
||||
rowSpacing: 0
|
||||
|
||||
// top margin
|
||||
Item {
|
||||
@ -59,141 +75,123 @@ Item {
|
||||
Layout.fillWidth: true
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// left margin
|
||||
Item {
|
||||
Layout.minimumWidth: 48
|
||||
Layout.maximumWidth: 80
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: 80
|
||||
Layout.minimumWidth: 48
|
||||
Layout.preferredHeight: welcomeContentItem.height
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: welcomeContentItem
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
Image {
|
||||
source: colorScheme.welcome_img
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: 16
|
||||
source: colorScheme.welcome_img
|
||||
sourceSize.height: 148
|
||||
sourceSize.width: 264
|
||||
}
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Welcome to\nProton Mail Bridge")
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
|
||||
colorScheme: root.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
|
||||
text: qsTr("Welcome to\nProton Mail Bridge")
|
||||
type: Label.LabelType.Heading
|
||||
}
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
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.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.preferredWidth: 320
|
||||
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
Layout.topMargin: 16
|
||||
colorScheme: root.colorScheme
|
||||
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
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
// Right margin
|
||||
Item {
|
||||
Layout.minimumWidth: 48
|
||||
Layout.maximumWidth: 80
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: 80
|
||||
Layout.minimumWidth: 48
|
||||
Layout.preferredHeight: welcomeContentItem.height
|
||||
}
|
||||
|
||||
// bottom margin
|
||||
Item {
|
||||
Layout.columnSpan: 3
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: children[0].implicitHeight + children[0].anchors.bottomMargin + children[0].anchors.topMargin
|
||||
implicitWidth: children[0].implicitWidth
|
||||
|
||||
Image {
|
||||
id: logoImage
|
||||
source: colorScheme.logo_img
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.topMargin: 48
|
||||
anchors.bottomMargin: 48
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: 48
|
||||
source: colorScheme.logo_img
|
||||
sourceSize.height: 25
|
||||
sourceSize.width: 200
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: (signInItem.currentIndex == 0) ? root.colorScheme.background_weak : root.colorScheme.background_norm
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: (signInItem.currentIndex == 0) ? root.colorScheme.background_weak : root.colorScheme.background_norm
|
||||
implicitHeight: children[0].implicitHeight
|
||||
implicitWidth: children[0].implicitWidth
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: signInItem.currentIndex == 0 ? 0 : parent.width / 4
|
||||
|
||||
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
|
||||
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
anchors.left: parent.left
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
anchors.bottomMargin: 80
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 80
|
||||
anchors.rightMargin: 80
|
||||
anchors.topMargin: 80
|
||||
anchors.bottomMargin: 80
|
||||
|
||||
visible: signInItem.currentIndex != 0
|
||||
|
||||
colorScheme: root.colorScheme
|
||||
secondary: true
|
||||
text: qsTr("Back")
|
||||
visible: signInItem.currentIndex != 0
|
||||
|
||||
onClicked: {
|
||||
signInItem.abort()
|
||||
signInItem.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
columnSpacing: 0
|
||||
rowSpacing: 0
|
||||
|
||||
columns: 3
|
||||
rowSpacing: 0
|
||||
|
||||
// top margin
|
||||
Item {
|
||||
@ -201,76 +199,47 @@ Item {
|
||||
Layout.fillWidth: true
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// left margin
|
||||
Item {
|
||||
Layout.minimumWidth: 48
|
||||
Layout.maximumWidth: 80
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: 80
|
||||
Layout.minimumWidth: 48
|
||||
Layout.preferredHeight: signInItem.height
|
||||
}
|
||||
|
||||
|
||||
SignIn {
|
||||
id: signInItem
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
Layout.preferredWidth: 320
|
||||
Layout.fillWidth: true
|
||||
|
||||
Layout.preferredWidth: 320
|
||||
colorScheme: root.colorScheme
|
||||
focus: true
|
||||
username: Backend.users.count === 1 && Backend.users.get(0) && (Backend.users.get(0).state === EUserState.SignedOut) ? Backend.users.get(0).username : ""
|
||||
}
|
||||
|
||||
// Right margin
|
||||
Item {
|
||||
Layout.minimumWidth: 48
|
||||
Layout.maximumWidth: 80
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: 80
|
||||
Layout.minimumWidth: 48
|
||||
Layout.preferredHeight: signInItem.height
|
||||
}
|
||||
|
||||
// bottom margin
|
||||
Item {
|
||||
Layout.columnSpan: 3
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 |
@ -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"
|
||||
}
|
||||
}
|
||||
@ -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.Layouts
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import "../Proton"
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
property ColorScheme colorScheme
|
||||
|
||||
property string textNormal: "Button"
|
||||
property string iconNormal: ""
|
||||
property string textDisabled: "Disabled"
|
||||
property string iconDisabled: ""
|
||||
property string textLoading: "Loading"
|
||||
property string iconLoading: ""
|
||||
property bool secondary: false
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
Layout.fillWidth: true
|
||||
|
||||
Layout.minimumHeight: implicitHeight
|
||||
Layout.minimumWidth: implicitWidth
|
||||
|
||||
text: root.textNormal
|
||||
icon.source: iconNormal
|
||||
secondary: root.secondary
|
||||
}
|
||||
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
Layout.fillWidth: true
|
||||
|
||||
Layout.minimumHeight: implicitHeight
|
||||
Layout.minimumWidth: implicitWidth
|
||||
|
||||
text: root.textDisabled
|
||||
icon.source: iconDisabled
|
||||
secondary: root.secondary
|
||||
|
||||
enabled: false
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
Layout.fillWidth: true
|
||||
|
||||
Layout.minimumHeight: implicitHeight
|
||||
Layout.minimumWidth: implicitWidth
|
||||
|
||||
text: root.textLoading
|
||||
icon.source: iconLoading
|
||||
secondary: root.secondary
|
||||
|
||||
loading: true
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user