mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 12:46:46 +00:00
Compare commits
258 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b95ef4d82 | |||
| 951c7c27fb | |||
| e7423a9519 | |||
| 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 | |||
| 84adbbc461 | |||
| 75811d22e8 | |||
| e26c7683d2 | |||
| f0e2688a8e | |||
| 06639ff6cd | |||
| 716de7f45a | |||
| 750de0cd31 | |||
| 823ca4d207 | |||
| a187747c7c | |||
| 11ebb16933 | |||
| 0048767022 | |||
| c4b75c6f34 | |||
| 32448063dc | |||
| 86bde91958 | |||
| 90c34406ba | |||
| d7b71aceda | |||
| 25a787529b | |||
| f82965b825 | |||
| f1cf4ee194 | |||
| 5136919c36 | |||
| 334a256638 | |||
| da528f2d9b | |||
| 1b0f930471 | |||
| 09c523e2d2 | |||
| 0b35f41ffd | |||
| 7be1a8ae8a | |||
| bdbf1bdd76 | |||
| 776976a8a2 | |||
| 040ddadb7a | |||
| 11f6f84dd6 | |||
| 82efa16d65 | |||
| 110286b81c | |||
| bc66841cdc | |||
| 8d028966c7 | |||
| d120bbeffc | |||
| eda49483e2 | |||
| 3ef526333a | |||
| f454f1a74f | |||
| e97a4d8847 | |||
| cb8174dbfd | |||
| cfca429067 | |||
| 1ffb9089ba | |||
| c0fc23d7cd | |||
| 4f8ecd598f | |||
| 65365281eb | |||
| 42178806d1 | |||
| e34050282e | |||
| b2830b39e0 | |||
| efb6ba0f1b | |||
| 80194ad797 | |||
| cda6b2a728 | |||
| 44bde86fde | |||
| 6cb52cacc9 | |||
| 8248491833 | |||
| fbdede28f0 | |||
| 66bc911652 | |||
| c860741ffa | |||
| 80d743afec | |||
| ac9857a965 | |||
| 0d57e3645a | |||
| 0afdc31f96 | |||
| 91de6e001e | |||
| 908ed3e723 | |||
| 7411073c08 | |||
| 7d838375bb | |||
| f545f30ec0 | |||
| 40c48ba804 | |||
| 1c2cb4f439 | |||
| 650dac37a7 | |||
| 55275b23ee | |||
| bce69e1a1b | |||
| f1917ad0de | |||
| 300d243331 | |||
| 6ea6d54af6 | |||
| c1b486a7eb | |||
| eaa673c4e4 | |||
| cc17366c1c | |||
| 62f6db35db | |||
| b2eb35592f | |||
| a3b26431ce | |||
| e96713a998 | |||
| 6c9d5ccd4a | |||
| 234554b459 | |||
| 6df5a82364 | |||
| 238929c3ec | |||
| 1f79e3b0a7 | |||
| f5af2afce5 | |||
| 9482bea8af | |||
| a55572e5b3 | |||
| 098eb7cb7a | |||
| 68334e3bb8 | |||
| 124231c3c7 | |||
| f591af2cbd | |||
| ff11d20d9c | |||
| 4e080b59d3 | |||
| a91d9762db | |||
| 6fb11d69f9 | |||
| 1eab3296d1 | |||
| 4352154b84 | |||
| 650158ea8a | |||
| e9488d12ee | |||
| 9a87b155ba | |||
| f6a1cd9b64 | |||
| 7b7c9093ce | |||
| c267168cb7 | |||
| 58b45d8458 | |||
| bdc6542970 | |||
| c942a44f6a | |||
| 55081fa59b | |||
| cc1d0e803b | |||
| b7a2371220 | |||
| ae65385c38 | |||
| ab70e85f1c | |||
| ff57eb2b43 | |||
| e78cb8089b | |||
| 09eef64514 | |||
| a2c2710760 | |||
| c4abb14ae6 | |||
| 38a0cdb4ab | |||
| c587dfc0dc | |||
| 7a090ffcc9 | |||
| 6ab49367ba | |||
| a06fd31f49 | |||
| 016319784e | |||
| ac00ef1b64 | |||
| 1e9a77c7b2 | |||
| a8dd52800e | |||
| fab063f194 | |||
| 4902898880 | |||
| c46b3245b8 | |||
| 3afd94c61d | |||
| 89632b7acd | |||
| b8cf193911 | |||
| fb4d34ef5b | |||
| 7f7e360cd7 | |||
| fc06665d2b | |||
| bfaf9765ae | |||
| a702e19dff | |||
| 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 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -41,3 +41,7 @@ cmake-build-*/
|
||||
|
||||
# Doxygen doc files
|
||||
_doc/
|
||||
|
||||
# gRPC auto-generated C++ source files
|
||||
*.pb.cc
|
||||
*.pb.h
|
||||
|
||||
396
.gitlab-ci.yml
396
.gitlab-ci.yml
@ -30,13 +30,6 @@ stages:
|
||||
- test
|
||||
- build
|
||||
|
||||
.rules-branch-and-MR-always:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- when: never
|
||||
|
||||
.rules-branch-and-MR-manual:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
@ -44,16 +37,6 @@ stages:
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.rules-branch-manual-MR-always:
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.rules-branch-manual-MR-and-devel-always:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "devel" || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
@ -64,173 +47,23 @@ stages:
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.after-script-code-coverage:
|
||||
after_script:
|
||||
- go get github.com/boumenot/gocover-cobertura
|
||||
- go run github.com/boumenot/gocover-cobertura < /tmp/coverage.out > coverage.xml
|
||||
- "go tool cover -func=/tmp/coverage.out | grep total:"
|
||||
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
|
||||
artifacts:
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
|
||||
# Stage: TEST
|
||||
|
||||
lint:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-and-MR-always
|
||||
script:
|
||||
- make lint
|
||||
tags:
|
||||
- medium
|
||||
|
||||
|
||||
.test-base:
|
||||
stage: test
|
||||
script:
|
||||
- make test
|
||||
|
||||
test-linux:
|
||||
extends:
|
||||
- .test-base
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
- .after-script-code-coverage
|
||||
tags:
|
||||
- large
|
||||
|
||||
test-linux-race:
|
||||
extends:
|
||||
- test-linux
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make test-race
|
||||
|
||||
test-integration:
|
||||
extends:
|
||||
- test-linux
|
||||
script:
|
||||
- make test-integration
|
||||
tags:
|
||||
- large
|
||||
|
||||
test-integration-race:
|
||||
extends:
|
||||
- test-integration
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make test-integration-race
|
||||
|
||||
|
||||
.windows-base:
|
||||
before_script:
|
||||
- export GOROOT=/c/Go1.20
|
||||
- export PATH=$GOROOT/bin:$PATH
|
||||
- export GOARCH=amd64
|
||||
- export GOPATH=~/go1.20
|
||||
- export GO111MODULE=on
|
||||
- export PATH=$GOPATH/bin:$PATH
|
||||
- export MSYSTEM=
|
||||
tags:
|
||||
- windows-bridge
|
||||
|
||||
#test-windows:
|
||||
# extends:
|
||||
# - .rules-branch-manual-MR-always
|
||||
# - .windows-base
|
||||
# stage: test
|
||||
# script:
|
||||
# - make test
|
||||
|
||||
# Stage: BUILD
|
||||
|
||||
.build-base:
|
||||
stage: build
|
||||
needs: ["lint"]
|
||||
.rules-branch-manual-scheduled-and-test-branch-always:
|
||||
rules:
|
||||
# GODT-1833: use `=~ /qa/` after mac and windows runners are fixed
|
||||
- if: $CI_JOB_NAME =~ /build-linux-qa/ && $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME=~ /^test/
|
||||
when: always
|
||||
allow_failure: false
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
script:
|
||||
- make build
|
||||
- git diff && git diff-index --quiet HEAD
|
||||
- make vault-editor
|
||||
artifacts:
|
||||
expire_in: 1 day
|
||||
when: always
|
||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
- vault-editor
|
||||
|
||||
|
||||
.linux-build-setup:
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.3.2
|
||||
variables:
|
||||
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||
cache:
|
||||
key: linux-vcpkg
|
||||
paths:
|
||||
- .cache
|
||||
when: 'always'
|
||||
before_script:
|
||||
- mkdir -p .cache/bin
|
||||
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||
- export PATH=$PATH:$QT6DIR/bin
|
||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
tags:
|
||||
- large
|
||||
|
||||
build-linux:
|
||||
extends:
|
||||
- .build-base
|
||||
- .linux-build-setup
|
||||
|
||||
build-linux-qa:
|
||||
extends:
|
||||
- build-linux
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
|
||||
.darwin-build-setup:
|
||||
before_script:
|
||||
- export PATH=/usr/local/bin:$PATH
|
||||
- export PATH=/usr/local/opt/git/bin:$PATH
|
||||
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
|
||||
- export PATH=/usr/local/opt/go@1.13/bin:$PATH
|
||||
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
|
||||
- export GOPATH=~/go1.20
|
||||
- export PATH=$GOPATH/bin:$PATH
|
||||
- export CGO_CPPFLAGS='-Wno-error -Wno-nullability-completeness -Wno-expansion-to-defined -Wno-builtin-requires-header'
|
||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
cache: {}
|
||||
tags:
|
||||
- macOS
|
||||
|
||||
build-darwin:
|
||||
extends:
|
||||
- .build-base
|
||||
- .darwin-build-setup
|
||||
|
||||
build-darwin-qa:
|
||||
extends:
|
||||
- build-darwin
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
.windows-build-setup:
|
||||
# ENV
|
||||
.env-windows:
|
||||
before_script:
|
||||
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
|
||||
- export GOROOT=/c/Go1.20/
|
||||
- export PATH=$GOROOT/bin:$PATH
|
||||
- export GOARCH=amd64
|
||||
@ -249,15 +82,204 @@ build-darwin-qa:
|
||||
tags:
|
||||
- windows-bridge
|
||||
|
||||
#build-windows:
|
||||
# extends:
|
||||
# - .build-base
|
||||
# - .windows-build-setup
|
||||
#
|
||||
##build-windows-qa:
|
||||
# extends:
|
||||
# - build-windows
|
||||
# variables:
|
||||
# BUILD_TAGS: "build_qa"
|
||||
#
|
||||
.env-darwin:
|
||||
before_script:
|
||||
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
|
||||
- export PATH=/usr/local/bin:$PATH
|
||||
- export PATH=/usr/local/opt/git/bin:$PATH
|
||||
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
|
||||
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
|
||||
- export GOROOT=~/local/opt/go@1.20
|
||||
- export PATH="${GOROOT}/bin:$PATH"
|
||||
- export GOPATH=~/go1.20
|
||||
- export PATH="${GOPATH}/bin:$PATH"
|
||||
- export QT6DIR=/opt/Qt/6.3.2/macos
|
||||
- export PATH="${QT6DIR}/bin:$PATH"
|
||||
- uname -a
|
||||
cache: {}
|
||||
tags:
|
||||
- macos-m1-bridge
|
||||
|
||||
.env-linux-build:
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.3.2
|
||||
variables:
|
||||
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||
cache:
|
||||
key: linux-vcpkg
|
||||
paths:
|
||||
- .cache
|
||||
when: 'always'
|
||||
before_script:
|
||||
- mkdir -p .cache/bin
|
||||
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
|
||||
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||
- export PATH=$PATH:$QT6DIR/bin
|
||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
tags:
|
||||
- large
|
||||
|
||||
# Stage: TEST
|
||||
|
||||
lint:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
script:
|
||||
- make 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:
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
script:
|
||||
- make test
|
||||
artifacts:
|
||||
paths:
|
||||
- coverage/**
|
||||
|
||||
test-linux:
|
||||
extends:
|
||||
- .script-test
|
||||
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
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make test-race
|
||||
|
||||
test-integration:
|
||||
extends:
|
||||
- test-linux
|
||||
script:
|
||||
- make test-integration
|
||||
|
||||
test-integration-race:
|
||||
extends:
|
||||
- test-integration
|
||||
- .rules-branch-and-MR-manual
|
||||
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
|
||||
|
||||
test-darwin:
|
||||
extends:
|
||||
- .env-darwin
|
||||
- .script-test
|
||||
|
||||
test-coverage:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-scheduled-and-test-branch-always
|
||||
script:
|
||||
- ./utils/coverage.sh
|
||||
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
|
||||
needs:
|
||||
- test-linux
|
||||
- test-windows
|
||||
- test-darwin
|
||||
- test-integration
|
||||
- test-integration-nightly
|
||||
tags:
|
||||
- small
|
||||
artifacts:
|
||||
paths:
|
||||
- coverage*
|
||||
- coverage/**
|
||||
when: 'always'
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
|
||||
# Stage: BUILD
|
||||
|
||||
.script-build:
|
||||
stage: build
|
||||
needs: ["lint"]
|
||||
extends:
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make build
|
||||
- git diff && git diff-index --quiet HEAD
|
||||
- make vault-editor
|
||||
artifacts:
|
||||
expire_in: 1 day
|
||||
when: always
|
||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
- vault-editor
|
||||
|
||||
build-linux:
|
||||
extends:
|
||||
- .script-build
|
||||
- .env-linux-build
|
||||
|
||||
build-linux-qa:
|
||||
extends:
|
||||
- build-linux
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
build-darwin:
|
||||
extends:
|
||||
- .script-build
|
||||
- .env-darwin
|
||||
|
||||
build-darwin-qa:
|
||||
extends:
|
||||
- build-darwin
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
build-windows:
|
||||
extends:
|
||||
- .script-build
|
||||
- .env-windows
|
||||
|
||||
build-windows-qa:
|
||||
extends:
|
||||
- build-windows
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
# TODO: PUT BACK ALL THE JOBS! JUST DID THIS FOR NOW TO GET CI WORKING AGAIN...
|
||||
|
||||
@ -36,6 +36,14 @@ issues:
|
||||
- gosec
|
||||
- goconst
|
||||
- dogsled
|
||||
- path: utils/smtp-send
|
||||
linters:
|
||||
- dupl
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gosec
|
||||
- goconst
|
||||
- dogsled
|
||||
|
||||
linters-settings:
|
||||
godox:
|
||||
|
||||
@ -58,7 +58,7 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [testify](https://github.com/stretchr/testify) available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
|
||||
* [cli](https://github.com/urfave/cli/v2) available under [license](https://github.com/urfave/cli/v2/blob/master/LICENSE)
|
||||
* [msgpack](https://github.com/vmihailenco/msgpack/v5) available under [license](https://github.com/vmihailenco/msgpack/v5/blob/master/LICENSE)
|
||||
* [goleak](https://go.uber.org/goleak)
|
||||
* [goleak](https://go.uber.org/goleak) available under [license](https://pkg.go.dev/go.uber.org/goleak?tab=licenses)
|
||||
* [exp](https://golang.org/x/exp) available under [license](https://cs.opensource.google/go/x/exp/+/master:LICENSE)
|
||||
* [net](https://golang.org/x/net) available under [license](https://cs.opensource.google/go/x/net/+/master:LICENSE)
|
||||
* [sys](https://golang.org/x/sys) available under [license](https://cs.opensource.google/go/x/sys/+/master:LICENSE)
|
||||
@ -66,16 +66,12 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [grpc](https://google.golang.org/grpc) available under [license](https://github.com/grpc/grpc-go/blob/master/LICENSE)
|
||||
* [protobuf](https://google.golang.org/protobuf) available under [license](https://github.com/protocolbuffers/protobuf/blob/main/LICENSE)
|
||||
* [plist](https://howett.net/plist) available under [license](https://github.com/DHowett/go-plist/blob/main/LICENSE)
|
||||
* [atlas](https://ariga.io/atlas)
|
||||
* [ent](https://entgo.io/ent)
|
||||
* [bcrypt](https://github.com/ProtonMail/bcrypt) available under [license](https://github.com/ProtonMail/bcrypt/blob/master/LICENSE)
|
||||
* [go-crypto](https://github.com/ProtonMail/go-crypto) available under [license](https://github.com/ProtonMail/go-crypto/blob/master/LICENSE)
|
||||
* [go-mime](https://github.com/ProtonMail/go-mime) available under [license](https://github.com/ProtonMail/go-mime/blob/master/LICENSE)
|
||||
* [go-srp](https://github.com/ProtonMail/go-srp) available under [license](https://github.com/ProtonMail/go-srp/blob/master/LICENSE)
|
||||
* [readline](https://github.com/abiosoft/readline) available under [license](https://github.com/abiosoft/readline/blob/master/LICENSE)
|
||||
* [levenshtein](https://github.com/agext/levenshtein) available under [license](https://github.com/agext/levenshtein/blob/master/LICENSE)
|
||||
* [cascadia](https://github.com/andybalholm/cascadia) available under [license](https://github.com/andybalholm/cascadia/blob/master/LICENSE)
|
||||
* [go-textseg](https://github.com/apparentlymart/go-textseg/v13) available under [license](https://github.com/apparentlymart/go-textseg/v13/blob/master/LICENSE)
|
||||
* [sonic](https://github.com/bytedance/sonic) available under [license](https://github.com/bytedance/sonic/blob/master/LICENSE)
|
||||
* [base64x](https://github.com/chenzhuoyu/base64x) available under [license](https://github.com/chenzhuoyu/base64x/blob/master/LICENSE)
|
||||
* [test](https://github.com/chzyer/test) available under [license](https://github.com/chzyer/test/blob/master/LICENSE)
|
||||
@ -93,7 +89,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [mimetype](https://github.com/gabriel-vasile/mimetype) available under [license](https://github.com/gabriel-vasile/mimetype/blob/master/LICENSE)
|
||||
* [sse](https://github.com/gin-contrib/sse) available under [license](https://github.com/gin-contrib/sse/blob/master/LICENSE)
|
||||
* [gin](https://github.com/gin-gonic/gin) available under [license](https://github.com/gin-gonic/gin/blob/master/LICENSE)
|
||||
* [inflect](https://github.com/go-openapi/inflect) available under [license](https://github.com/go-openapi/inflect/blob/master/LICENSE)
|
||||
* [locales](https://github.com/go-playground/locales) available under [license](https://github.com/go-playground/locales/blob/master/LICENSE)
|
||||
* [universal-translator](https://github.com/go-playground/universal-translator) available under [license](https://github.com/go-playground/universal-translator/blob/master/LICENSE)
|
||||
* [validator](https://github.com/go-playground/validator/v10) available under [license](https://github.com/go-playground/validator/v10/blob/master/LICENSE)
|
||||
@ -105,7 +100,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [go-immutable-radix](https://github.com/hashicorp/go-immutable-radix) available under [license](https://github.com/hashicorp/go-immutable-radix/blob/master/LICENSE)
|
||||
* [go-memdb](https://github.com/hashicorp/go-memdb) available under [license](https://github.com/hashicorp/go-memdb/blob/master/LICENSE)
|
||||
* [golang-lru](https://github.com/hashicorp/golang-lru) available under [license](https://github.com/hashicorp/golang-lru/blob/master/LICENSE)
|
||||
* [hcl](https://github.com/hashicorp/hcl/v2) available under [license](https://github.com/hashicorp/hcl/v2/blob/master/LICENSE)
|
||||
* [multierror](https://github.com/joeshaw/multierror) available under [license](https://github.com/joeshaw/multierror/blob/master/LICENSE)
|
||||
* [go](https://github.com/json-iterator/go) available under [license](https://github.com/json-iterator/go/blob/master/LICENSE)
|
||||
* [cpuid](https://github.com/klauspost/cpuid/v2) available under [license](https://github.com/klauspost/cpuid/v2/blob/master/LICENSE)
|
||||
@ -114,7 +108,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [go-isatty](https://github.com/mattn/go-isatty) available under [license](https://github.com/mattn/go-isatty/blob/master/LICENSE)
|
||||
* [go-runewidth](https://github.com/mattn/go-runewidth) available under [license](https://github.com/mattn/go-runewidth/blob/master/LICENSE)
|
||||
* [go-sqlite3](https://github.com/mattn/go-sqlite3) available under [license](https://github.com/mattn/go-sqlite3/blob/master/LICENSE)
|
||||
* [go-wordwrap](https://github.com/mitchellh/go-wordwrap) available under [license](https://github.com/mitchellh/go-wordwrap/blob/master/LICENSE)
|
||||
* [concurrent](https://github.com/modern-go/concurrent) available under [license](https://github.com/modern-go/concurrent/blob/master/LICENSE)
|
||||
* [reflect2](https://github.com/modern-go/reflect2) available under [license](https://github.com/modern-go/reflect2/blob/master/LICENSE)
|
||||
* [tablewriter](https://github.com/olekukonko/tablewriter) available under [license](https://github.com/olekukonko/tablewriter/blob/master/LICENSE)
|
||||
@ -130,14 +123,13 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [codec](https://github.com/ugorji/go/codec) available under [license](https://github.com/ugorji/go/codec/blob/master/LICENSE)
|
||||
* [tagparser](https://github.com/vmihailenco/tagparser/v2) available under [license](https://github.com/vmihailenco/tagparser/v2/blob/master/LICENSE)
|
||||
* [smetrics](https://github.com/xrash/smetrics) available under [license](https://github.com/xrash/smetrics/blob/master/LICENSE)
|
||||
* [go-cty](https://github.com/zclconf/go-cty) available under [license](https://github.com/zclconf/go-cty/blob/master/LICENSE)
|
||||
* [arch](https://golang.org/x/arch) available under [license](https://cs.opensource.google/go/x/arch/+/master:LICENSE)
|
||||
* [crypto](https://golang.org/x/crypto) available under [license](https://cs.opensource.google/go/x/crypto/+/master:LICENSE)
|
||||
* [mod](https://golang.org/x/mod) available under [license](https://cs.opensource.google/go/x/mod/+/master:LICENSE)
|
||||
* [sync](https://golang.org/x/sync) available under [license](https://cs.opensource.google/go/x/sync/+/master:LICENSE)
|
||||
* [tools](https://golang.org/x/tools) available under [license](https://cs.opensource.google/go/x/tools/+/master:LICENSE)
|
||||
* [genproto](https://google.golang.org/genproto)
|
||||
gopkg.in/yaml.v3
|
||||
* [genproto](https://google.golang.org/genproto) available under [license](https://pkg.go.dev/google.golang.org/genproto?tab=licenses)
|
||||
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
|
||||
* [docker-credential-helpers](https://github.com/ProtonMail/docker-credential-helpers) available under [license](https://github.com/ProtonMail/docker-credential-helpers/blob/master/LICENSE)
|
||||
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
|
||||
* [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE)
|
||||
|
||||
198
Changelog.md
198
Changelog.md
@ -3,6 +3,150 @@
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
|
||||
## Umshiang Bridge 3.5.2
|
||||
|
||||
### Fixed
|
||||
* GODT-3003: Ensure IMAP State is reset after vault corruption.
|
||||
* GODT-3001: Only create system labels during system label sync.
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
### Changed
|
||||
* Test: Add require.Eventually to TestBridge_UserAgentFromSMTPClient.
|
||||
* Test: Add smtp-send utility.
|
||||
* GODT-2759: Check for oprhan messages.
|
||||
* GODT-2759: Add prompt to download missing messages for analysis.
|
||||
* GODT-2759: CLI debug commands.
|
||||
* Remove gRPC auto-generated C++ source files.
|
||||
* Test: Force all unit test to use minimum sync spec.
|
||||
* Test: Force sync limits to minimum with env variable.
|
||||
* GODT-2691: Close logrus output file on exit.
|
||||
* GODT-2522: New Gluon database layout.
|
||||
* GODT-2678: When internet is off, do not display status dot icon for the user in the context menu.
|
||||
* GODT-2686: Change the orientation of the expand/collapse arrow for Advanced settings.
|
||||
* Test(GODT-2636): Add step for sending from EML.
|
||||
* Log failed message ids during sync.
|
||||
* GODT-2510: Remove Ent.
|
||||
* Test(GODT-2600): Changing state (read/unread, starred/unstarred) of a message in integration tests.
|
||||
* GODT-2703: Got rid of account details dialog with Apple Mail autoconf.
|
||||
* GODT-2685: Update to bug report log attachment logic.
|
||||
* GODT-2690: Update sentry reporting in GUI for new log file naming.
|
||||
* GODT-2668: Implemented new log retention policy.
|
||||
* Test(GODT-2683): Save Draft without "Date" & "From" in headers.
|
||||
* GODT-2666: Feat(GODT-2667): introduce sessionID in bridge.
|
||||
* GODT-2660: Calculate bridge coverage and refactor CI yaml file.
|
||||
* Fix dependency_license script to handle dot formated version.
|
||||
|
||||
### Fixed
|
||||
* GODT-2812: Fix rare sync deadlock.
|
||||
* GODT-2822: Better handling 429 during sync and event loop.
|
||||
* GODT-2763: Missing Answered flag on Sync and Message Create.
|
||||
* GODT-2758: Fix panic in SetFlagsOnMessages.
|
||||
* 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-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.
|
||||
|
||||
|
||||
## Stone Bridge 3.3.2
|
||||
|
||||
### Fixed
|
||||
@ -82,7 +226,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.
|
||||
@ -142,7 +286,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
|
||||
|
||||
@ -283,7 +427,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.
|
||||
@ -300,7 +444,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.
|
||||
|
||||
@ -365,7 +509,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.
|
||||
@ -645,7 +789,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.
|
||||
@ -799,7 +943,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.
|
||||
@ -896,7 +1040,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.
|
||||
@ -938,7 +1082,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.
|
||||
@ -1189,7 +1333,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.
|
||||
|
||||
@ -1279,7 +1423,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.
|
||||
@ -1603,7 +1747,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.
|
||||
@ -1768,13 +1912,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.
|
||||
@ -1900,7 +2044,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.
|
||||
@ -1908,7 +2052,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.
|
||||
@ -1956,7 +2100,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.
|
||||
@ -2030,7 +2174,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.
|
||||
@ -2052,7 +2196,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.
|
||||
@ -2069,7 +2213,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.
|
||||
|
||||
@ -2093,7 +2237,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.
|
||||
@ -2112,7 +2256,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).
|
||||
@ -2184,7 +2328,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)
|
||||
|
||||
@ -2250,7 +2394,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
|
||||
@ -2282,12 +2426,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).
|
||||
@ -2312,7 +2456,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).
|
||||
|
||||
59
Makefile
59
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.3.2+git
|
||||
BRIDGE_APP_VERSION?=3.5.2+git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
APP_FULL_NAME:=Proton Mail Bridge
|
||||
APP_VENDOR:=Proton AG
|
||||
@ -229,14 +229,28 @@ add-license:
|
||||
change-copyright-year:
|
||||
./utils/missing_license.sh change-year
|
||||
|
||||
GOCOVERAGE=-covermode=count -coverpkg=github.com/ProtonMail/proton-bridge/v3/internal/...,github.com/ProtonMail/proton-bridge/v3/pkg/...,
|
||||
GOCOVERDIR=-args -test.gocoverdir=$$PWD/coverage
|
||||
|
||||
test: gofiles
|
||||
go test -v -timeout=20m -p=1 -count=1 -coverprofile=/tmp/coverage.out -run=${TESTRUN} ./internal/... ./pkg/...
|
||||
mkdir -p coverage/unit-${GOOS}
|
||||
go test \
|
||||
-v -timeout=20m -p=1 -count=1 \
|
||||
${GOCOVERAGE} \
|
||||
-run=${TESTRUN} ./internal/... ./pkg/... \
|
||||
${GOCOVERDIR}/unit-${GOOS}
|
||||
|
||||
test-race: gofiles
|
||||
go test -v -timeout=40m -p=1 -count=1 -race -failfast -run=${TESTRUN} ./internal/... ./pkg/...
|
||||
|
||||
test-integration: gofiles
|
||||
go test -v -timeout=60m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests
|
||||
mkdir -p coverage/integration
|
||||
go test \
|
||||
-v -timeout=60m -p=1 -count=1 -tags=test_integration \
|
||||
${GOCOVERAGE} \
|
||||
github.com/ProtonMail/proton-bridge/v3/tests \
|
||||
${GOCOVERDIR}/integration
|
||||
|
||||
|
||||
test-integration-debug: gofiles
|
||||
dlv test github.com/ProtonMail/proton-bridge/v3/tests -- -test.v -test.timeout=10m -test.parallel=1 -test.count=1
|
||||
@ -244,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
|
||||
@ -260,8 +290,23 @@ 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/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
|
||||
mockgen --package mocks github.com/ProtonMail/gluon/connector IMAPStateWrite > internal/services/imapservice/mocks/mocks.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
|
||||
@ -277,6 +322,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}
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@ -43,9 +44,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
appName = "Proton Mail Launcher"
|
||||
exeName = "bridge"
|
||||
guiName = "bridge-gui"
|
||||
appName = "Proton Mail Launcher"
|
||||
exeName = "bridge"
|
||||
guiName = "bridge-gui"
|
||||
launcherName = "launcher"
|
||||
|
||||
FlagCLI = "cli"
|
||||
FlagCLIShort = "c"
|
||||
@ -53,6 +55,7 @@ const (
|
||||
FlagNonInteractiveShort = "n"
|
||||
FlagLauncher = "--launcher"
|
||||
FlagWait = "--wait"
|
||||
FlagSessionID = "--session-id"
|
||||
)
|
||||
|
||||
func main() { //nolint:funlen
|
||||
@ -75,12 +78,26 @@ func main() { //nolint:funlen
|
||||
if err != nil {
|
||||
l.WithError(err).Fatal("Failed to get logs path")
|
||||
}
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
||||
|
||||
if err := logging.Init(logsPath, os.Getenv("VERBOSITY")); err != nil {
|
||||
sessionID := logging.NewSessionID()
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath, sessionID, launcherName))
|
||||
|
||||
var closer io.Closer
|
||||
if closer, err = logging.Init(
|
||||
logsPath,
|
||||
sessionID,
|
||||
logging.LauncherShortAppName,
|
||||
logging.DefaultMaxLogFileSize,
|
||||
logging.NoPruning,
|
||||
os.Getenv("VERBOSITY"),
|
||||
); err != nil {
|
||||
l.WithError(err).Fatal("Failed to setup logging")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = logging.Close(closer)
|
||||
}()
|
||||
|
||||
updatesPath, err := locations.ProvideUpdatesPath()
|
||||
if err != nil {
|
||||
l.WithError(err).Fatal("Failed to get updates path")
|
||||
@ -134,7 +151,7 @@ func main() { //nolint:funlen
|
||||
}
|
||||
}
|
||||
|
||||
cmd := execabs.Command(exe, appendLauncherPath(launcher, args)...) //nolint:gosec
|
||||
cmd := execabs.Command(exe, appendLauncherPath(launcher, append(args, FlagSessionID, string(sessionID)))...) //nolint:gosec
|
||||
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
|
||||
@ -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
|
||||
|
||||
14
go.mod
14
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.20230706112359-3146d8312d12
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20231009084701-3af0474b0b3c
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230628092916-81cb3f87f184
|
||||
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
|
||||
@ -51,16 +51,12 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb // indirect
|
||||
entgo.io/ent v0.11.8 // indirect
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
|
||||
github.com/ProtonMail/go-srp v0.0.7 // indirect
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/chzyer/test v1.0.0 // indirect
|
||||
@ -78,7 +74,6 @@ require (
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.9.1 // indirect
|
||||
github.com/go-openapi/inflect v0.19.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
@ -90,7 +85,6 @@ require (
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-memdb v1.3.3 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.16.1 // indirect
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
@ -98,8 +92,7 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.16 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
@ -115,7 +108,6 @@ require (
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
github.com/zclconf/go-cty v1.12.1 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.9.0 // indirect
|
||||
golang.org/x/mod v0.8.0 // indirect
|
||||
|
||||
42
go.sum
42
go.sum
@ -1,5 +1,3 @@
|
||||
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb h1:mbsFtavDqGdYwdDpP50LGOOZ2hgyGoJcZeOpbgKMyu4=
|
||||
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb/go.mod h1:T230JFcENj4ZZzMkZrXFDSkv+2kXkUgpJ5FQQ5hMcKU=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
@ -13,13 +11,10 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
entgo.io/ent v0.11.8 h1:M/M0QL1CYCUSdqGRXUrXhFYSDRJPsOOrr+RLEej/gyQ=
|
||||
entgo.io/ent v0.11.8/go.mod h1:ericBi6Q8l3wBH1wEIDfKxw7rcQEuRPyBfbIzjtxJ18=
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA1qmKJ+hQn3UjytosdoG27WGjrDlVs=
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
@ -28,10 +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.20230607122549-dbdb8e1cc0c3 h1:VMbbJD3dcGPPIgbdQTS5Z4nX0QU/SsVZWdmsMVVBBsI=
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230607122549-dbdb8e1cc0c3/go.mod h1:ERZikuN+2i/oTeSwS5fq7J0Fms76uUcBlTAwT4KaEAk=
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230706112359-3146d8312d12 h1:a4mVvmGGojclWgbQ6g4eW/XquioHJ/iYF4OFk70265Q=
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230706112359-3146d8312d12/go.mod h1:ERZikuN+2i/oTeSwS5fq7J0Fms76uUcBlTAwT4KaEAk=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20231009084701-3af0474b0b3c h1:gUDu4pOswgbou0QczfreNiXQFrmvVlpSh8Q+vft/JvI=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20231009084701-3af0474b0b3c/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=
|
||||
@ -42,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.20230628092916-81cb3f87f184 h1:gw8sgQMCIDS/lw5xbF2iqlTfvY0HhuafjlGsKcN3VsE=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230628092916-81cb3f87f184/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=
|
||||
@ -54,8 +47,6 @@ github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
|
||||
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA=
|
||||
@ -63,9 +54,6 @@ github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.m
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
@ -160,8 +148,6 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
|
||||
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
@ -172,8 +158,6 @@ github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QX
|
||||
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
|
||||
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
|
||||
@ -250,8 +234,6 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/hcl/v2 v2.16.1 h1:BwuxEMD/tsYgbhIW7UuI3crjovf3MzuFWiVgiv57iHg=
|
||||
github.com/hashicorp/hcl/v2 v2.16.1/go.mod h1:JRmR89jycNkrrqnMmvPDMd56n1rQJ2Q6KocSLCMCXng=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
@ -284,9 +266,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
|
||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
@ -303,8 +282,8 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
||||
@ -313,8 +292,6 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
@ -369,8 +346,6 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
|
||||
@ -415,10 +390,8 @@ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4d
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/urfave/cli/v2 v2.24.4 h1:0gyJJEBYtCV87zI/x2nZCPyDxD51K6xM8SkwjHFCNEU=
|
||||
github.com/urfave/cli/v2 v2.24.4/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
|
||||
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
@ -426,9 +399,6 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRT
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zclconf/go-cty v1.12.1 h1:PcupnljUm9EIvbgSHQnHhUr3fO6oFmkOrvs2BAFNXXY=
|
||||
github.com/zclconf/go-cty v1.12.1/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA=
|
||||
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
|
||||
@ -19,6 +19,7 @@ package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
@ -51,6 +52,9 @@ const (
|
||||
flagCPUProfile = "cpu-prof"
|
||||
flagCPUProfileShort = "p"
|
||||
|
||||
flagTraceProfile = "trace-prof"
|
||||
flagTraceProfileShort = "t"
|
||||
|
||||
flagMemProfile = "mem-prof"
|
||||
flagMemProfileShort = "m"
|
||||
|
||||
@ -76,10 +80,12 @@ const (
|
||||
flagNoWindow = "no-window"
|
||||
flagParentPID = "parent-pid"
|
||||
flagSoftwareRenderer = "software-renderer"
|
||||
flagSessionID = "session-id"
|
||||
)
|
||||
|
||||
const (
|
||||
appUsage = "Proton Mail IMAP and SMTP Bridge"
|
||||
appUsage = "Proton Mail IMAP and SMTP Bridge"
|
||||
appShortName = "bridge"
|
||||
)
|
||||
|
||||
func New() *cli.App {
|
||||
@ -93,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},
|
||||
@ -150,6 +161,10 @@ func New() *cli.App {
|
||||
Hidden: true,
|
||||
Value: false,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flagSessionID,
|
||||
Hidden: true,
|
||||
},
|
||||
}
|
||||
|
||||
app.Action = run
|
||||
@ -183,6 +198,11 @@ func run(c *cli.Context) error {
|
||||
exe = os.Args[0]
|
||||
}
|
||||
|
||||
var logCloser io.Closer
|
||||
defer func() {
|
||||
_ = logging.Close(logCloser)
|
||||
}()
|
||||
|
||||
// Restart the app if requested.
|
||||
return withRestarter(exe, func(restarter *restarter.Restarter) error {
|
||||
// Handle crashes with various actions.
|
||||
@ -199,7 +219,9 @@ func run(c *cli.Context) error {
|
||||
}
|
||||
|
||||
// Initialize logging.
|
||||
return withLogging(c, crashHandler, locations, func() error {
|
||||
return withLogging(c, crashHandler, locations, func(closer io.Closer) error {
|
||||
logCloser = closer
|
||||
|
||||
// If there was an error during migration, log it now.
|
||||
if migrationErr != nil {
|
||||
logrus.WithError(migrationErr).Error("Failed to migrate old app data")
|
||||
@ -298,7 +320,7 @@ func withSingleInstance(settingPath, lockFile string, version *semver.Version, f
|
||||
}
|
||||
|
||||
// Initialize our logging system.
|
||||
func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locations.Locations, fn func() error) error {
|
||||
func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locations.Locations, fn func(closer io.Closer) error) error {
|
||||
logrus.Debug("Initializing logging")
|
||||
defer logrus.Debug("Logging stopped")
|
||||
|
||||
@ -311,12 +333,21 @@ func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locatio
|
||||
logrus.WithField("path", logsPath).Debug("Received logs path")
|
||||
|
||||
// Initialize logging.
|
||||
if err := logging.Init(logsPath, c.String(flagLogLevel)); err != nil {
|
||||
sessionID := logging.NewSessionIDFromString(c.String(flagSessionID))
|
||||
var closer io.Closer
|
||||
if closer, err = logging.Init(
|
||||
logsPath,
|
||||
sessionID,
|
||||
logging.BridgeShortAppName,
|
||||
logging.DefaultMaxLogFileSize,
|
||||
logging.DefaultPruningSize,
|
||||
c.String(flagLogLevel),
|
||||
); err != nil {
|
||||
return fmt.Errorf("could not initialize logging: %w", err)
|
||||
}
|
||||
|
||||
// Ensure we dump a stack trace if we crash.
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath, sessionID, appShortName))
|
||||
|
||||
logrus.
|
||||
WithField("appName", constants.FullAppName).
|
||||
@ -329,7 +360,7 @@ func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locatio
|
||||
WithField("SentryID", sentry.GetProtectedHostname()).
|
||||
Info("Run app")
|
||||
|
||||
return fn()
|
||||
return fn(closer)
|
||||
}
|
||||
|
||||
// WithLocations provides access to locations where we store our files.
|
||||
@ -356,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()
|
||||
|
||||
@ -44,7 +44,7 @@ import (
|
||||
// deleteOldGoIMAPFiles Set with `-ldflags -X app.deleteOldGoIMAPFiles=true` to enable cleanup of old imap cache data.
|
||||
var deleteOldGoIMAPFiles bool //nolint:gochecknoglobals
|
||||
|
||||
// withBridge creates creates and tears down the bridge.
|
||||
// withBridge creates and tears down the bridge.
|
||||
func withBridge(
|
||||
c *cli.Context,
|
||||
exe string,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -25,6 +25,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@ -43,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"
|
||||
@ -53,6 +55,7 @@ import (
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -300,8 +303,11 @@ func TestBridge_UserAgentFromSMTPClient(t *testing.T) {
|
||||
string(info.BridgePass)),
|
||||
))
|
||||
|
||||
currentUserAgent = b.GetCurrentUserAgent()
|
||||
require.Contains(t, currentUserAgent, "UnknownClient/0.0.1")
|
||||
require.Eventually(t, func() bool {
|
||||
currentUserAgent = b.GetCurrentUserAgent()
|
||||
|
||||
return strings.Contains(currentUserAgent, "UnknownClient/0.0.1")
|
||||
}, time.Minute, 5*time.Second)
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -579,7 +585,7 @@ func TestBridge_MissingGluonStore(t *testing.T) {
|
||||
require.NoError(t, os.RemoveAll(gluonDir))
|
||||
|
||||
// Bridge starts but can't find the gluon store dir; there should be no error.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// ...
|
||||
})
|
||||
})
|
||||
@ -617,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)
|
||||
@ -637,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)
|
||||
@ -696,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))
|
||||
})
|
||||
})
|
||||
@ -772,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.
|
||||
@ -869,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()
|
||||
|
||||
@ -1053,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)
|
||||
|
||||
@ -1062,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 {
|
||||
|
||||
@ -18,13 +18,8 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
@ -34,11 +29,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
MaxTotalAttachmentSize = 7 * (1 << 20)
|
||||
MaxCompressedFilesCount = 6
|
||||
DefaultMaxBugReportZipSize = 7 * 1024 * 1024
|
||||
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 {
|
||||
@ -51,54 +46,25 @@ func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, descript
|
||||
}
|
||||
}
|
||||
|
||||
var atts []proton.ReportBugAttachment
|
||||
var attachment []proton.ReportBugAttachment
|
||||
|
||||
if attachLogs {
|
||||
logs, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
||||
return logging.MatchLogName(filename) && !logging.MatchStackTraceName(filename)
|
||||
})
|
||||
logsPath, err := bridge.locator.ProvideLogsPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
crashes, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
||||
return logging.MatchLogName(filename) && logging.MatchStackTraceName(filename)
|
||||
})
|
||||
buffer, err := logging.ZipLogsForBugReport(logsPath, DefaultMaxSessionCountForBugReport, DefaultMaxBugReportZipSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
guiLogs, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
||||
return logging.MatchGUILogName(filename) && !logging.MatchStackTraceName(filename)
|
||||
})
|
||||
body, err := io.ReadAll(buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var matchFiles []string
|
||||
|
||||
// Include bridge logs, up to a maximum amount.
|
||||
matchFiles = append(matchFiles, logs[max(0, len(logs)-(MaxCompressedFilesCount/2)):]...)
|
||||
|
||||
// Include crash logs, up to a maximum amount.
|
||||
matchFiles = append(matchFiles, crashes[max(0, len(crashes)-(MaxCompressedFilesCount/2)):]...)
|
||||
|
||||
// bridge-gui keeps just one small (~ 1kb) log file; we always include it.
|
||||
if len(guiLogs) > 0 {
|
||||
matchFiles = append(matchFiles, guiLogs[len(guiLogs)-1])
|
||||
}
|
||||
|
||||
archive, err := zipFiles(matchFiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(archive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
atts = append(atts, proton.ReportBugAttachment{
|
||||
attachment = append(attachment, proton.ReportBugAttachment{
|
||||
Name: "logs.zip",
|
||||
Filename: "logs.zip",
|
||||
MIMEType: "application/zip",
|
||||
@ -116,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,
|
||||
@ -125,116 +91,5 @@ func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, descript
|
||||
|
||||
Username: account,
|
||||
Email: email,
|
||||
}, atts...)
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func getMatchingLogs(locator Locator, filenameMatchFunc func(string) bool) (filenames []string, err error) {
|
||||
logsPath, err := locator.ProvideLogsPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(logsPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var matchFiles []string
|
||||
|
||||
for _, file := range files {
|
||||
if filenameMatchFunc(file.Name()) {
|
||||
matchFiles = append(matchFiles, filepath.Join(logsPath, file.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(matchFiles) // Sorted by timestamp: oldest first.
|
||||
|
||||
return matchFiles, nil
|
||||
}
|
||||
|
||||
type limitedBuffer struct {
|
||||
capacity int
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
func newLimitedBuffer(capacity int) *limitedBuffer {
|
||||
return &limitedBuffer{
|
||||
capacity: capacity,
|
||||
buf: bytes.NewBuffer(make([]byte, 0, capacity)),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *limitedBuffer) Write(p []byte) (n int, err error) {
|
||||
if len(p)+b.buf.Len() > b.capacity {
|
||||
return 0, ErrSizeTooLarge
|
||||
}
|
||||
|
||||
return b.buf.Write(p)
|
||||
}
|
||||
|
||||
func (b *limitedBuffer) Read(p []byte) (n int, err error) {
|
||||
return b.buf.Read(p)
|
||||
}
|
||||
|
||||
func zipFiles(filenames []string) (io.Reader, error) {
|
||||
if len(filenames) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
buf := newLimitedBuffer(MaxTotalAttachmentSize)
|
||||
|
||||
w := zip.NewWriter(buf)
|
||||
defer w.Close() //nolint:errcheck
|
||||
|
||||
for _, file := range filenames {
|
||||
if err := addFileToZip(file, w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func addFileToZip(filename string, writer *zip.Writer) error {
|
||||
fileReader, err := os.Open(filepath.Clean(filename))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fileReader.Close() //nolint:errcheck,gosec
|
||||
|
||||
fileInfo, err := fileReader.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header, err := zip.FileInfoHeader(fileInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header.Method = zip.Deflate
|
||||
header.Name = filepath.Base(filename)
|
||||
|
||||
fileWriter, err := writer.CreateHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(fileWriter, fileReader); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fileReader.Close()
|
||||
}, attachment...)
|
||||
}
|
||||
|
||||
297
internal/bridge/debug.go
Normal file
297
internal/bridge/debug.go
Normal file
@ -0,0 +1,297 @@
|
||||
// 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"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/gluon/rfc822"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/bradenaw/juniper/iterator"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
goimap "github.com/emersion/go-imap"
|
||||
goimapclient "github.com/emersion/go-imap/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
type CheckClientStateResult struct {
|
||||
MissingMessages map[string]map[string]user.DiagMailboxMessage
|
||||
}
|
||||
|
||||
func (c *CheckClientStateResult) AddMissingMessage(userID string, message user.DiagMailboxMessage) {
|
||||
v, ok := c.MissingMessages[userID]
|
||||
if !ok {
|
||||
c.MissingMessages[userID] = map[string]user.DiagMailboxMessage{message.ID: message}
|
||||
} else {
|
||||
v[message.ID] = message
|
||||
}
|
||||
}
|
||||
|
||||
// CheckClientState checks the current IMAP client reported state against the proton server state and reports
|
||||
// anything that is out of place.
|
||||
func (bridge *Bridge) CheckClientState(ctx context.Context, checkFlags bool, progressCB func(string)) (CheckClientStateResult, error) {
|
||||
bridge.usersLock.RLock()
|
||||
defer bridge.usersLock.RUnlock()
|
||||
|
||||
users := maps.Values(bridge.users)
|
||||
|
||||
result := CheckClientStateResult{
|
||||
MissingMessages: make(map[string]map[string]user.DiagMailboxMessage),
|
||||
}
|
||||
|
||||
for _, usr := range users {
|
||||
if progressCB != nil {
|
||||
progressCB(fmt.Sprintf("Checking state for user %v", usr.Name()))
|
||||
}
|
||||
log := logrus.WithField("user", usr.Name()).WithField("diag", "state-check")
|
||||
log.Debug("Retrieving all server metadata")
|
||||
meta, err := usr.GetDiagnosticMetadata(ctx)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
success := true
|
||||
|
||||
if len(meta.Metadata) != len(meta.MessageIDs) {
|
||||
log.Errorf("Metadata (%v) and message(%v) list sizes do not match", len(meta.Metadata), len(meta.MessageIDs))
|
||||
}
|
||||
|
||||
log.Debug("Building state")
|
||||
state, err := meta.BuildMailboxToMessageMap(ctx, usr)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to build state")
|
||||
return result, err
|
||||
}
|
||||
|
||||
info, err := bridge.GetUserInfo(usr.ID())
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to get user info")
|
||||
return result, err
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("127.0.0.1:%v", bridge.GetIMAPPort())
|
||||
|
||||
for account, mboxMap := range state {
|
||||
if progressCB != nil {
|
||||
progressCB(fmt.Sprintf("Checking state for user %v's account '%v'", usr.Name(), account))
|
||||
}
|
||||
if err := func(account string, mboxMap user.AccountMailboxMap) error {
|
||||
client, err := goimapclient.Dial(addr)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to connect to imap client")
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = client.Logout()
|
||||
}()
|
||||
|
||||
if err := client.Login(account, string(info.BridgePass)); err != nil {
|
||||
return fmt.Errorf("failed to login for user %v:%w", usr.Name(), err)
|
||||
}
|
||||
|
||||
log := log.WithField("account", account)
|
||||
for mboxName, messageList := range mboxMap {
|
||||
log := log.WithField("mbox", mboxName)
|
||||
status, err := client.Select(mboxName, true)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("Failed to select mailbox %v", messageList)
|
||||
return fmt.Errorf("failed to select '%v':%w", mboxName, err)
|
||||
}
|
||||
|
||||
log.Debug("Checking message count")
|
||||
|
||||
if int(status.Messages) != len(messageList) {
|
||||
success = false
|
||||
log.Errorf("Message count doesn't match, got '%v' expected '%v'", status.Messages, len(messageList))
|
||||
}
|
||||
|
||||
ids, err := clientGetMessageIDs(client, mboxName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get message ids for mbox '%v': %w", mboxName, err)
|
||||
}
|
||||
|
||||
for _, msg := range messageList {
|
||||
imapFlags, ok := ids[msg.ID]
|
||||
if !ok {
|
||||
if meta.FailedMessageIDs.Contains(msg.ID) {
|
||||
log.Warningf("Missing message '%v', but it is part of failed message set", msg.ID)
|
||||
} else {
|
||||
log.Errorf("Missing message '%v'", msg.ID)
|
||||
}
|
||||
|
||||
result.AddMissingMessage(msg.UserID, msg)
|
||||
continue
|
||||
}
|
||||
|
||||
if checkFlags {
|
||||
if !imapFlags.Equals(msg.Flags) {
|
||||
log.Errorf("Message '%v' flags do mot match, got=%v, expected=%v",
|
||||
msg.ID,
|
||||
imapFlags.ToSlice(),
|
||||
msg.Flags.ToSlice(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !success {
|
||||
log.Errorf("State does not match")
|
||||
} else {
|
||||
log.Info("State matches")
|
||||
}
|
||||
|
||||
return nil
|
||||
}(account, mboxMap); err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check for orphaned messages (only present in All Mail)
|
||||
if progressCB != nil {
|
||||
progressCB(fmt.Sprintf("Checking user %v for orphans", usr.Name()))
|
||||
}
|
||||
log.Debugf("Checking for orphans")
|
||||
|
||||
for _, m := range meta.Metadata {
|
||||
filteredLabels := xslices.Filter(m.LabelIDs, func(t string) bool {
|
||||
switch t {
|
||||
case proton.AllMailLabel:
|
||||
return false
|
||||
case proton.AllSentLabel:
|
||||
return false
|
||||
case proton.AllDraftsLabel:
|
||||
return false
|
||||
case proton.OutboxLabel:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
if len(filteredLabels) == 0 {
|
||||
log.Warnf("Message %v is only present in All Mail (Subject=%v)", m.ID, m.Subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) DebugDownloadFailedMessages(
|
||||
ctx context.Context,
|
||||
result CheckClientStateResult,
|
||||
exportPath string,
|
||||
progressCB func(string, int, int),
|
||||
) error {
|
||||
bridge.usersLock.RLock()
|
||||
defer bridge.usersLock.RUnlock()
|
||||
|
||||
for userID, messages := range result.MissingMessages {
|
||||
usr, ok := bridge.users[userID]
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to find user with id %v", userID)
|
||||
}
|
||||
|
||||
userDir := filepath.Join(exportPath, userID)
|
||||
if err := os.MkdirAll(userDir, 0o700); err != nil {
|
||||
return fmt.Errorf("failed to create directory '%v': %w", userDir, err)
|
||||
}
|
||||
|
||||
if err := usr.DebugDownloadMessages(ctx, userDir, messages, progressCB); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func clientGetMessageIDs(client *goimapclient.Client, mailbox string) (map[string]imap.FlagSet, error) {
|
||||
status, err := client.Select(mailbox, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if status.Messages == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
resCh := make(chan *goimap.Message)
|
||||
|
||||
section, err := goimap.ParseBodySectionName("BODY[HEADER]")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fetchItems := []goimap.FetchItem{"BODY[HEADER]", goimap.FetchFlags}
|
||||
|
||||
seq, err := goimap.ParseSeqSet("1:*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := client.Fetch(
|
||||
seq,
|
||||
fetchItems,
|
||||
resCh,
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
messages := iterator.Collect(iterator.Chan(resCh))
|
||||
|
||||
ids := make(map[string]imap.FlagSet, len(messages))
|
||||
|
||||
for i, m := range messages {
|
||||
literal, err := io.ReadAll(m.GetBody(section))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
header, err := rfc822.NewHeader(literal)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse header for msg %v: %w", i, err)
|
||||
}
|
||||
|
||||
internalID, ok := header.GetChecked("X-Pm-Internal-Id")
|
||||
if !ok {
|
||||
logrus.Errorf("Message %v does not have internal id", internalID)
|
||||
continue
|
||||
}
|
||||
|
||||
messageFlags := imap.NewFlagSet(m.Flags...)
|
||||
|
||||
// Recent and Deleted are not part of the proton flag set.
|
||||
messageFlags.RemoveFromSelf("\\Recent")
|
||||
messageFlags.RemoveFromSelf("\\Deleted")
|
||||
|
||||
ids[internalID] = messageFlags
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
175
internal/bridge/draft_test.go
Normal file
175
internal/bridge/draft_test.go
Normal file
@ -0,0 +1,175 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/rfc822"
|
||||
"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/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
go_imap "github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBridge_HandleDraftsSendFromOtherClient(t *testing.T) {
|
||||
getGluonHeaderID := func(literal []byte) (string, string) {
|
||||
h, err := rfc822.NewHeader(literal)
|
||||
require.NoError(t, err)
|
||||
|
||||
gluonID, ok := h.GetChecked("X-Pm-Gluon-Id")
|
||||
require.True(t, ok)
|
||||
|
||||
externalID, ok := h.GetChecked("Message-Id")
|
||||
require.True(t, ok)
|
||||
|
||||
return gluonID, externalID
|
||||
}
|
||||
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
_, _, err := s.CreateUser("imap", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = s.CreateUser("bar", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The initial user should be fully synced.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||
waiter := waitForIMAPServerReady(b)
|
||||
defer waiter.Done()
|
||||
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
waiter.Wait()
|
||||
|
||||
info, err := b.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
// Create first draft in client.
|
||||
literal := fmt.Sprintf(`From: %v
|
||||
To: %v
|
||||
Date: Fri, 3 Feb 2023 01:04:32 +0100
|
||||
Subject: Foo
|
||||
|
||||
Hello
|
||||
`, info.Addresses[0], "bar@proton.local")
|
||||
|
||||
require.NoError(t, client.Append("Drafts", nil, time.Now(), strings.NewReader(literal)))
|
||||
// Verify the draft is available in client.
|
||||
require.Eventually(t, func() bool {
|
||||
status, err := client.Status("Drafts", []go_imap.StatusItem{go_imap.StatusMessages})
|
||||
require.NoError(t, err)
|
||||
return status.Messages == 1
|
||||
}, 2*time.Second, time.Second)
|
||||
|
||||
// Retrieve the new literal so we can have the Proton Message ID.
|
||||
messages, err := clientFetch(client, "Drafts")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(messages))
|
||||
|
||||
newLiteral, err := io.ReadAll(messages[0].GetBody(must(go_imap.ParseBodySectionName("BODY[]"))))
|
||||
require.NoError(t, err)
|
||||
logrus.Info(string(newLiteral))
|
||||
|
||||
newLiteralID, newLiteralExternID := getGluonHeaderID(newLiteral)
|
||||
|
||||
// Modify new literal.
|
||||
newLiteralModified := append(newLiteral, []byte(" world from client2")...) //nolint:gocritic
|
||||
|
||||
func() {
|
||||
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(b.GetSMTPPort())))
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = smtpClient.Close() }()
|
||||
|
||||
// Upgrade to TLS.
|
||||
require.NoError(t, smtpClient.StartTLS(&tls.Config{InsecureSkipVerify: true}))
|
||||
|
||||
// Authorize with SASL PLAIN.
|
||||
require.NoError(t, smtpClient.Auth(sasl.NewPlainClient(
|
||||
info.Addresses[0],
|
||||
info.Addresses[0],
|
||||
string(info.BridgePass)),
|
||||
))
|
||||
|
||||
// Send the message.
|
||||
require.NoError(t, smtpClient.SendMail(
|
||||
info.Addresses[0],
|
||||
[]string{"bar@proton.local"},
|
||||
bytes.NewReader(newLiteralModified),
|
||||
))
|
||||
}()
|
||||
|
||||
// Append message to Sent as the imap client would.
|
||||
require.NoError(t, client.Append("Sent", nil, time.Now(), strings.NewReader(literal)))
|
||||
|
||||
// Verify the sent message gets updated with the new literal.
|
||||
require.Eventually(t, func() bool {
|
||||
// Check if sent message matches the latest draft.
|
||||
messagesClient1, err := clientFetch(client, "Sent", "BODY[TEXT]", "BODY[]")
|
||||
require.NoError(t, err)
|
||||
|
||||
if len(messagesClient1) != 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
sentLiteral, err := io.ReadAll(messagesClient1[0].GetBody(must(go_imap.ParseBodySectionName("BODY[]"))))
|
||||
require.NoError(t, err)
|
||||
|
||||
sentLiteralID, sentLiteralExternID := getGluonHeaderID(sentLiteral)
|
||||
|
||||
sentLiteralText, err := io.ReadAll(messagesClient1[0].GetBody(must(go_imap.ParseBodySectionName("BODY[TEXT]"))))
|
||||
require.NoError(t, err)
|
||||
|
||||
sentLiteralStr := string(sentLiteralText)
|
||||
|
||||
literalMatches := sentLiteralStr == "Hello\r\n world from client2\r\n"
|
||||
|
||||
idIsDifferent := sentLiteralID != newLiteralID
|
||||
|
||||
externIDMatches := sentLiteralExternID == newLiteralExternID
|
||||
|
||||
return literalMatches && idIsDifferent && externIDMatches
|
||||
}, 2*time.Second, time.Second)
|
||||
})
|
||||
}, server.WithMessageDedup())
|
||||
}
|
||||
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"
|
||||
@ -35,6 +37,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
||||
"github.com/bradenaw/juniper/iterator"
|
||||
"github.com/bradenaw/juniper/stream"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
@ -252,14 +255,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 +288,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 +300,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 +317,330 @@ 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 TestBridge_CorruptedVaultClearsPreviousIMAPSyncState(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []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, 100)
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
var err error
|
||||
|
||||
userID, err = bridge.LoginFull(context.Background(), "imap", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for sync to finish
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
})
|
||||
|
||||
settingsPath, err := locator.ProvideSettingsPath()
|
||||
require.NoError(t, err)
|
||||
|
||||
syncConfigPath, err := locator.ProvideIMAPSyncConfigPath()
|
||||
require.NoError(t, err)
|
||||
|
||||
syncStatePath := imapservice.GetSyncConfigPath(syncConfigPath, userID)
|
||||
// Check sync state is complete
|
||||
{
|
||||
state, err := imapservice.NewSyncState(syncStatePath)
|
||||
require.NoError(t, err)
|
||||
syncStatus, err := state.GetSyncStatus(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.True(t, syncStatus.IsComplete())
|
||||
}
|
||||
|
||||
// corrupt the vault
|
||||
require.NoError(t, os.WriteFile(filepath.Join(settingsPath, "vault.enc"), []byte("Trash!"), 0o600))
|
||||
|
||||
// Bridge starts but can't find the gluon database dir; there should be no error.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
_, err := bridge.LoginFull(context.Background(), "imap", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
// Check sync state is reset.
|
||||
{
|
||||
state, err := imapservice.NewSyncState(syncStatePath)
|
||||
require.NoError(t, err)
|
||||
syncStatus, err := state.GetSyncStatus(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.False(t, syncStatus.IsComplete())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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()),
|
||||
@ -399,6 +719,10 @@ func createNumMessages(ctx context.Context, t *testing.T, c *proton.Client, addr
|
||||
}
|
||||
|
||||
func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, messages ...[]byte) []string {
|
||||
return createMessagesWithFlags(ctx, t, c, addrID, labelID, 0, messages...)
|
||||
}
|
||||
|
||||
func createMessagesWithFlags(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, flags proton.MessageFlag, messages ...[]byte) []string {
|
||||
user, err := c.GetUser(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -417,6 +741,13 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
|
||||
_, ok := addrKRs[addrID]
|
||||
require.True(t, ok)
|
||||
|
||||
var msgFlags proton.MessageFlag
|
||||
if flags == 0 {
|
||||
msgFlags = proton.MessageFlagReceived
|
||||
} else {
|
||||
msgFlags = flags
|
||||
}
|
||||
|
||||
str, err := c.ImportMessages(
|
||||
ctx,
|
||||
addrKRs[addrID],
|
||||
@ -427,7 +758,7 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
|
||||
Metadata: proton.ImportMetadata{
|
||||
AddressID: addrID,
|
||||
LabelIDs: []string{labelID},
|
||||
Flags: proton.MessageFlagReceived,
|
||||
Flags: msgFlags,
|
||||
},
|
||||
Message: message,
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -70,9 +70,11 @@ func prepareMobileConfig(
|
||||
password []byte,
|
||||
) *mobileconfig.Config {
|
||||
return &mobileconfig.Config{
|
||||
DisplayName: username,
|
||||
EmailAddress: addresses,
|
||||
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10),
|
||||
DisplayName: username,
|
||||
EmailAddress: addresses,
|
||||
AccountName: username,
|
||||
AccountDescription: username,
|
||||
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10),
|
||||
IMAP: &mobileconfig.IMAP{
|
||||
Hostname: hostname,
|
||||
Port: imapPort,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
4
internal/frontend/bridge-gui/bridge-gui-tester/.lldbinit
Normal file
4
internal/frontend/bridge-gui/bridge-gui-tester/.lldbinit
Normal file
@ -0,0 +1,4 @@
|
||||
# The following fix an issue happening using LLDB with OpenSSL 3.1 on ARM64 architecture. (GODT-2680)
|
||||
# WARNING: this file is ignored if you do not enable reading lldb config from cwd in ~/.lldbinit (`settings set target.load-cwd-lldbinit true`)
|
||||
settings set platform.plugin.darwin.ignored-exceptions EXC_BAD_INSTRUCTION
|
||||
process handle SIGILL -n false -p true -s false
|
||||
@ -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).
|
||||
|
||||
4
internal/frontend/bridge-gui/bridge-gui/.lldbinit
Normal file
4
internal/frontend/bridge-gui/bridge-gui/.lldbinit
Normal file
@ -0,0 +1,4 @@
|
||||
# The following fix an issue happening using LLDB with OpenSSL 3.1 on ARM64 architecture. (GODT-2680)
|
||||
# WARNING: this file is ignored if you do not enable reading lldb config from cwd in ~/.lldbinit (`settings set target.load-cwd-lldbinit true`)
|
||||
settings set platform.plugin.darwin.ignored-exceptions EXC_BAD_INSTRUCTION
|
||||
process handle SIGILL -n false -p true -s false
|
||||
@ -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,23 +102,47 @@ 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();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] launcher The launcher.
|
||||
/// \param[in] args The launcher arguments.
|
||||
//****************************************************************************************************************************************************
|
||||
void AppController::setLauncherArgs(const QString &launcher, const QStringList &args) {
|
||||
launcher_ = launcher;
|
||||
launcherArgs_ = args;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] sessionID The sessionID.
|
||||
//****************************************************************************************************************************************************
|
||||
void AppController::setSessionID(const QString &sessionID) {
|
||||
sessionID_ = sessionID;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The sessionID.
|
||||
//****************************************************************************************************************************************************
|
||||
QString AppController::sessionID() {
|
||||
return sessionID_;
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ class Exception;
|
||||
/// \brief App controller class.
|
||||
//****************************************************************************************************************************************************
|
||||
class AppController : public QObject {
|
||||
Q_OBJECT
|
||||
Q_OBJECT
|
||||
friend AppController &app();
|
||||
|
||||
public: // member functions.
|
||||
@ -52,10 +52,12 @@ public: // member functions.
|
||||
std::unique_ptr<bridgepp::Overseer> &bridgeOverseer() { return bridgeOverseer_; }; ///< Returns a reference the bridge overseer
|
||||
bridgepp::ProcessMonitor *bridgeMonitor() const; ///< Return the bridge worker.
|
||||
Settings &settings();; ///< Return the application settings.
|
||||
void setLauncherArgs(const QString &launcher, const QStringList &args);
|
||||
void setLauncherArgs(const QString &launcher, const QStringList &args); ///< Set the launcher arguments.
|
||||
void setSessionID(QString const &sessionID); ///< Set the sessionID.
|
||||
QString sessionID(); ///< Get the sessionID.
|
||||
|
||||
public slots:
|
||||
void onFatalError(bridgepp::Exception const& e); ///< Handle fatal errors.
|
||||
void onFatalError(bridgepp::Exception const &e); ///< Handle fatal errors.
|
||||
|
||||
private: // member functions
|
||||
AppController(); ///< Default constructor.
|
||||
@ -67,8 +69,9 @@ private: // data members
|
||||
std::unique_ptr<bridgepp::Log> log_; ///< The log.
|
||||
std::unique_ptr<bridgepp::Overseer> bridgeOverseer_; ///< The overseer for the bridge monitor worker.
|
||||
std::unique_ptr<Settings> settings_; ///< The application settings.
|
||||
QString launcher_;
|
||||
QStringList launcherArgs_;
|
||||
QString launcher_; ///< The launcher.
|
||||
QStringList launcherArgs_; ///< The launcher arguments.
|
||||
QString sessionID_; ///< The sessionID.
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
#include "Pch.h"
|
||||
#include "CommandLine.h"
|
||||
#include "Settings.h"
|
||||
#include <bridgepp/SessionID/SessionID.h>
|
||||
|
||||
|
||||
using namespace bridgepp;
|
||||
@ -142,5 +143,14 @@ CommandLineOptions parseCommandLine(int argc, char *argv[]) {
|
||||
|
||||
options.logLevel = parseLogLevel(argc, argv);
|
||||
|
||||
QString sessionID = parseGoCLIStringArgument(argc, argv, { "session-id" });
|
||||
if (sessionID.isEmpty()) {
|
||||
// The session ID was not passed to us on the command-line -> create one and add to the command-line for bridge
|
||||
sessionID = newSessionID();
|
||||
options.bridgeArgs.append("--session-id");
|
||||
options.bridgeArgs.append(sessionID);
|
||||
}
|
||||
app().setSessionID(sessionID);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
@ -19,7 +19,6 @@
|
||||
#include "LogUtils.h"
|
||||
#include "BuildConfig.h"
|
||||
#include <bridgepp/Log/LogUtils.h>
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
|
||||
|
||||
using namespace bridgepp;
|
||||
@ -33,15 +32,10 @@ Log &initLog() {
|
||||
log.registerAsQtMessageHandler();
|
||||
log.setEchoInConsole(true);
|
||||
|
||||
// remove old gui log files
|
||||
QDir const logsDir(userLogsDir());
|
||||
for (QFileInfo const fileInfo: logsDir.entryInfoList({ "gui_v*.log" }, QDir::Filter::Files)) { // entryInfolist apparently only support wildcards, not regex.
|
||||
QFile(fileInfo.absoluteFilePath()).remove();
|
||||
}
|
||||
|
||||
// create new GUI log file
|
||||
QString error;
|
||||
if (!log.startWritingToFile(logsDir.absoluteFilePath(QString("gui_v%1_%2.log").arg(PROJECT_VER).arg(QDateTime::currentSecsSinceEpoch())), &error)) {
|
||||
if (!log.startWritingToFile(QDir(userLogsDir()).absoluteFilePath(QString("%1_gui_000_v%2_%3.log").arg(app().sessionID(),
|
||||
PROJECT_VER, PROJECT_TAG)), &error)) {
|
||||
log.error(error);
|
||||
}
|
||||
|
||||
|
||||
@ -37,6 +37,15 @@
|
||||
using namespace bridgepp;
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
|
||||
QString const bugReportFile = ":qml/Resources/bug_report_flow.json";
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
@ -61,7 +70,7 @@ void QMLBackend::init(GRPCConfig const &serviceConfig) {
|
||||
app().grpc().setLog(&log);
|
||||
this->connectGrpcEvents();
|
||||
|
||||
app().grpc().connectToServer(bridgepp::userConfigDir(), serviceConfig, app().bridgeMonitor());
|
||||
app().grpc().connectToServer(app().sessionID(), bridgepp::userConfigDir(), serviceConfig, app().bridgeMonitor());
|
||||
app().log().info("Connected to backend via gRPC service.");
|
||||
|
||||
QString bridgeVer;
|
||||
@ -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));
|
||||
}
|
||||
|
||||
|
||||
@ -109,6 +120,12 @@ UserList const &QMLBackend::users() const {
|
||||
return *users_;
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return the if bridge considers internet is on.
|
||||
//****************************************************************************************************************************************************
|
||||
bool QMLBackend::isInternetOn() const {
|
||||
return isInternetOn_;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
@ -204,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.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -575,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.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -680,7 +769,7 @@ void QMLBackend::login(QString const &username, QString const &password) const {
|
||||
HANDLE_EXCEPTION(
|
||||
if (username.compare("coco@bandicoot", Qt::CaseInsensitive) == 0) {
|
||||
throw Exception("User requested bridge-gui to crash by trying to log as coco@bandicoot",
|
||||
"This error exists for test purposes and should be ignored.", __func__, tailOfLatestBridgeLog());
|
||||
"This error exists for test purposes and should be ignored.", __func__, tailOfLatestBridgeLog(app().sessionID()));
|
||||
}
|
||||
app().grpc().login(username, password);
|
||||
)
|
||||
@ -839,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);
|
||||
)
|
||||
}
|
||||
|
||||
@ -914,7 +1004,6 @@ void QMLBackend::sendBadEventUserFeedback(QString const &userID, bool doResync)
|
||||
if (!badEventDisplayQueue_.isEmpty()) {
|
||||
// we introduce a small delay here, so that the user notices the dialog disappear and pops up again.
|
||||
QTimer::singleShot(500, [&]() { this->displayBadEventDialog(badEventDisplayQueue_.front()); });
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -989,6 +1078,25 @@ void QMLBackend::setUpdateTrayIcon(QString const &stateString, QString const &st
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] isOn Does bridge consider internet as on.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::internetStatusChanged(bool isOn) {
|
||||
HANDLE_EXCEPTION(
|
||||
if (isInternetOn_ == isOn) {
|
||||
return;
|
||||
}
|
||||
|
||||
isInternetOn_ = isOn;
|
||||
if (isOn) {
|
||||
emit internetOn();
|
||||
} else {
|
||||
emit internetOff();
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] imapPort The IMAP port.
|
||||
/// \param[in] smtpPort The SMTP port.
|
||||
@ -1152,11 +1260,12 @@ void QMLBackend::connectGrpcEvents() {
|
||||
GRPCClient *client = &app().grpc();
|
||||
|
||||
// app events
|
||||
connect(client, &GRPCClient::internetStatus, this, [&](bool isOn) { if (isOn) { emit internetOn(); } else { emit internetOff(); }});
|
||||
connect(client, &GRPCClient::internetStatus, this, &QMLBackend::internetStatusChanged);
|
||||
connect(client, &GRPCClient::toggleAutostartFinished, this, &QMLBackend::toggleAutostartFinished);
|
||||
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>
|
||||
@ -45,17 +46,24 @@ public: // member functions.
|
||||
void init(GRPCConfig const &serviceConfig); ///< Initialize the backend.
|
||||
bool waitForEventStreamReaderToFinish(qint32 timeoutMs); ///< Wait for the event stream reader to finish.
|
||||
UserList const& users() const; ///< Return the list of users
|
||||
bool isInternetOn() const; ///< Check if bridge considers internet as on.
|
||||
void showMainWindow(QString const &reason); ///< Show the main window.
|
||||
void showHelp(QString const &reason); ///< Show the help page.
|
||||
void showSettings(QString const &reason); ///< Show the settings page.
|
||||
void selectUser(QString const &userID, bool forceShowWindow, QString const &reason); ///< Select the user and display its account details (or login screen).
|
||||
|
||||
// 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)
|
||||
@ -86,10 +94,11 @@ 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)
|
||||
|
||||
|
||||
// Qt Property system setters & getters.
|
||||
bool showOnStartup() const; ///< Getter for the 'showOnStartup' property.
|
||||
void setShowSplashScreen(bool show); ///< Setter for the 'showSplashScreen' property.
|
||||
@ -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.
|
||||
@ -198,6 +211,7 @@ public slots: // slots for functions that need to be processed locally.
|
||||
void setUpdateTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'update' state.
|
||||
|
||||
public slots: // slot for signals received from gRPC that need transformation instead of simple forwarding
|
||||
void internetStatusChanged(bool isOn); ///< Check if bridge considers internet as on.
|
||||
void onMailServerSettingsChanged(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Slot for the ConnectionModeChanged gRPC event.
|
||||
void onGenericError(bridgepp::ErrorInfo const &info); ///< Slot for generic errors received from the gRPC service.
|
||||
void onLoginFinished(QString const &userID, bool wasSignedOut); ///< Slot for LoginFinished gRPC event.
|
||||
@ -252,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.
|
||||
@ -280,8 +295,10 @@ private: // data members
|
||||
int smtpPort_ { 0 }; ///< The cached value for the SMTP port.
|
||||
bool useSSLForIMAP_ { false }; ///< The cached value for useSSLForIMAP.
|
||||
bool useSSLForSMTP_ { false }; ///< The cached value for useSSLForSMTP.
|
||||
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_;
|
||||
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>
|
||||
@ -19,6 +23,7 @@
|
||||
<file>qml/icons/ic-card-identity.svg</file>
|
||||
<file>qml/icons/ic-check.svg</file>
|
||||
<file>qml/icons/ic-chevron-down.svg</file>
|
||||
<file>qml/icons/ic-chevron-right.svg</file>
|
||||
<file>qml/icons/ic-chevron-up.svg</file>
|
||||
<file>qml/icons/ic-cog-wheel.svg</file>
|
||||
<file>qml/icons/ic-connected.svg</file>
|
||||
@ -31,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>
|
||||
@ -94,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.
|
||||
@ -348,6 +350,7 @@ void TrayIcon::refreshContextMenu() {
|
||||
return;
|
||||
}
|
||||
|
||||
bool const internetOn = app().backend().isInternetOn();
|
||||
menu_->clear();
|
||||
menu_->addAction(statusIcon_, stateString_, []() {app().backend().showMainWindow("tray menu status clicked");});
|
||||
menu_->addSeparator();
|
||||
@ -359,7 +362,9 @@ void TrayIcon::refreshContextMenu() {
|
||||
User const &user = *users.get(i);
|
||||
UserState const state = user.state();
|
||||
auto action = new QAction(user.primaryEmailOrUsername());
|
||||
action->setIcon((UserState::Connected == state) ? greenDot_ : (UserState::Locked == state ? orangeDot_ : greyDot_));
|
||||
if (internetOn) {
|
||||
action->setIcon((UserState::Connected == state) ? greenDot_ : (UserState::Locked == state ? orangeDot_ : greyDot_));
|
||||
}
|
||||
action->setData(user.id());
|
||||
connect(action, &QAction::triggered, this, &TrayIcon::onUserClicked);
|
||||
if ((i < 10) && onMac) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -305,22 +314,23 @@ int main(int argc, char *argv[]) {
|
||||
// these outputs and output them on the command-line.
|
||||
log.info(QString("New Sentry reporter - id: %1.").arg(getProtectedHostname()));
|
||||
|
||||
QString bridgeexec;
|
||||
QString const &sessionID = app().sessionID();
|
||||
QString bridgeExe;
|
||||
if (!cliOptions.attach) {
|
||||
if (isBridgeRunning()) {
|
||||
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.",
|
||||
QString(), __FUNCTION__, tailOfLatestBridgeLog());
|
||||
QString(), __FUNCTION__, tailOfLatestBridgeLog(sessionID));
|
||||
}
|
||||
|
||||
// before launching bridge, we remove any trailing service config file, because we need to make sure we get a newly generated one.
|
||||
FocusGRPCClient::removeServiceConfigFile(configDir);
|
||||
GRPCClient::removeServiceConfigFile(configDir);
|
||||
bridgeexec = launchBridge(cliOptions.bridgeArgs);
|
||||
bridgeExe = launchBridge(cliOptions.bridgeArgs);
|
||||
}
|
||||
|
||||
log.info(QString("Retrieving gRPC service configuration from '%1'").arg(QDir::toNativeSeparators(grpcServerConfigPath(configDir))));
|
||||
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(configDir, cliOptions.attach ? 0 : grpcServiceConfigWaitDelayMs,
|
||||
app().bridgeMonitor()));
|
||||
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(sessionID, configDir,
|
||||
cliOptions.attach ? 0 : grpcServiceConfigWaitDelayMs, app().bridgeMonitor()));
|
||||
if (!cliOptions.attach) {
|
||||
GRPCClient::removeServiceConfigFile(configDir);
|
||||
}
|
||||
@ -378,9 +388,9 @@ int main(int argc, char *argv[]) {
|
||||
QStringList args = cliOptions.bridgeGuiArgs;
|
||||
args.append(waitFlag);
|
||||
args.append(mainexec);
|
||||
if (!bridgeexec.isEmpty()) {
|
||||
if (!bridgeExe.isEmpty()) {
|
||||
args.append(waitFlag);
|
||||
args.append(bridgeexec);
|
||||
args.append(bridgeExe);
|
||||
}
|
||||
app().setLauncherArgs(cliOptions.launcher, args);
|
||||
result = QGuiApplication::exec();
|
||||
|
||||
@ -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.AlignTop
|
||||
|
||||
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-up.svg" : "/qml/icons/ic-chevron-down.svg"
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user