GODT-22: Frontend-backend

- GODT-1246 Implement settings view.
- GODT-1257 GODT-1246: Account and Help view
- GODT-1298: Minimal working build (panics)
- GODT-1298: loading QML (needs Popup window)
- GODT-1298: WARN: Adding PopupWindow not possible!
    In therecipe qt the `quickwidgets` classes are within `quick` module, but
    forgot to add library and include paths into cgo flags. Therefore
    compilation fails and it would be hard to patch therecipe in order to
    fix it.

    I am not sure if rewrite PopupWindow into go would make any difference,
    therefore I decided to use normal QML Window without borders.
- GODT-1298: Rework status window, add backend props, slots and signals.
- GODT-1298: Users
- GODT-1298: Login
- GODT-1298: WIP Help and bug report
- GODT-1178: MacOS dock icon control
- GODT-1298: Help, bug report, update and events
- GODT-1298: Apple Mail config and Settings (without cache on disk)
This commit is contained in:
Jakub
2021-08-09 14:40:56 +02:00
parent 0a9748a15d
commit e0d07d67a0
76 changed files with 4730 additions and 398 deletions

33
.gitignore vendored
View File

@ -6,9 +6,6 @@
.*.sw? .*.sw?
*~ *~
# Compiled Object files, Static and Dynamic libs (Shared Objects)
vendor
# Test files # Test files
godog.test godog.test
debug.test debug.test
@ -17,17 +14,12 @@ coverage.html
# Run files # Run files
mem.pprof mem.pprof
# Auto generated frontend # Auto generated
internal/frontend/qml/BridgeUI/*.qmlc
internal/frontend/qml/ImportExportUI/*.qmlc
internal/frontend/qml/ProtonUI/*.qmlc
internal/frontend/qml/ProtonUI/fontawesome.ttf
internal/frontend/qml/ProtonUI/images
internal/frontend/qml/ImportExportUI/images
frontend/qml/*.qmlc
# Credits files (generated).
internal/**/credits.go internal/**/credits.go
vendor
vendor-cache
/main.go
# Build files # Build files
/launcher-* /launcher-*
@ -37,18 +29,3 @@ internal/**/credits.go
/hasher /hasher
cmd/Desktop-Bridge/deploy cmd/Desktop-Bridge/deploy
cmd/Import-Export/deploy cmd/Import-Export/deploy
internal/frontend/qt*/moc.cpp
internal/frontend/qt*/moc.go
internal/frontend/qt*/moc.h
internal/frontend/qt*/moc_cgo_*.go
internal/frontend/qt*/moc_moc.h
internal/frontend/qt*/rcc.cpp
internal/frontend/qt*/rcc.qrc
internal/frontend/qt*/rcc_cgo_*.go
internal/frontend/rcc.cpp
internal/frontend/rcc.qrc
internal/frontend/rcc_cgo_*.go
vendor-cache/
/main.go

View File

@ -126,6 +126,7 @@ build-linux-qa:
extends: .build-base extends: .build-base
only: only:
- web - web
- branches
script: script:
- BUILD_TAGS="build_qa" make build - BUILD_TAGS="build_qa" make build
artifacts: artifacts:
@ -161,6 +162,7 @@ build-darwin-qa:
extends: .build-darwin-base extends: .build-darwin-base
only: only:
- web - web
- branches
script: script:
- BUILD_TAGS="build_qa" make build - BUILD_TAGS="build_qa" make build
artifacts: artifacts:

View File

@ -1,8 +1,6 @@
--- ---
run: run:
timeout: 10m timeout: 10m
build-tags:
- nogui
skip-dirs: skip-dirs:
- pkg/mime - pkg/mime

View File

@ -88,7 +88,7 @@ ${TGZ_TARGET}: ${DEPLOY_DIR}/${TARGET_OS}
cd ${DEPLOY_DIR}/${TARGET_OS} && tar czf ../../../../$@ . cd ${DEPLOY_DIR}/${TARGET_OS} && tar czf ../../../../$@ .
${DEPLOY_DIR}/linux: ${EXE_TARGET} ${DEPLOY_DIR}/linux: ${EXE_TARGET}
cp -pf ./internal/frontend/share/icons/${SRC_SVG} ${DEPLOY_DIR}/linux/logo.svg cp -pf ./internal/frontend/share/${SRC_SVG} ${DEPLOY_DIR}/linux/logo.svg
cp -pf ./LICENSE ${DEPLOY_DIR}/linux/ cp -pf ./LICENSE ${DEPLOY_DIR}/linux/
cp -pf ./Changelog.md ${DEPLOY_DIR}/linux/ cp -pf ./Changelog.md ${DEPLOY_DIR}/linux/
cp -pf ./dist/${EXE_NAME}.desktop ${DEPLOY_DIR}/linux/ cp -pf ./dist/${EXE_NAME}.desktop ${DEPLOY_DIR}/linux/
@ -98,7 +98,7 @@ ${DEPLOY_DIR}/darwin: ${EXE_TARGET}
mv ${EXE_TARGET}/Contents/MacOS/{${DIRNAME},${EXE_NAME}}; \ mv ${EXE_TARGET}/Contents/MacOS/{${DIRNAME},${EXE_NAME}}; \
perl -i -pe"s/>${DIRNAME}/>${EXE_NAME}/g" ${EXE_TARGET}/Contents/Info.plist; \ perl -i -pe"s/>${DIRNAME}/>${EXE_NAME}/g" ${EXE_TARGET}/Contents/Info.plist; \
fi fi
cp ./internal/frontend/share/icons/${SRC_ICNS} ${DARWINAPP_CONTENTS}/Resources/${SRC_ICNS} cp ./internal/frontend/share/${SRC_ICNS} ${DARWINAPP_CONTENTS}/Resources/${SRC_ICNS}
cp LICENSE ${DARWINAPP_CONTENTS}/Resources/ cp LICENSE ${DARWINAPP_CONTENTS}/Resources/
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngine.framework" rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngine.framework"
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebView.framework" rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebView.framework"
@ -106,7 +106,7 @@ ${DEPLOY_DIR}/darwin: ${EXE_TARGET}
./utils/remove_non_relative_links_darwin.sh "${EXE_TARGET}${EXE_BINARY_DARWIN}" ./utils/remove_non_relative_links_darwin.sh "${EXE_TARGET}${EXE_BINARY_DARWIN}"
${DEPLOY_DIR}/windows: ${EXE_TARGET} ${DEPLOY_DIR}/windows: ${EXE_TARGET}
cp ./internal/frontend/share/icons/${SRC_ICO} ${DEPLOY_DIR}/windows/logo.ico cp ./internal/frontend/share/${SRC_ICO} ${DEPLOY_DIR}/windows/logo.ico
cp LICENSE ${DEPLOY_DIR}/windows/ cp LICENSE ${DEPLOY_DIR}/windows/
QT_BUILD_TARGET:=build desktop QT_BUILD_TARGET:=build desktop
@ -127,9 +127,9 @@ ${EXE_TARGET}: check-has-go gofiles ${RESOURCE_FILE} ${VENDOR_TARGET}
WINDRES_YEAR:=$(shell date +%Y) WINDRES_YEAR:=$(shell date +%Y)
APP_VERSION_COMMA:=$(shell echo "${APP_VERSION}" | sed -e 's/[^0-9,.]*//g' -e 's/\./,/g') APP_VERSION_COMMA:=$(shell echo "${APP_VERSION}" | sed -e 's/[^0-9,.]*//g' -e 's/\./,/g')
resource.syso: ./internal/frontend/share/info.rc ./internal/frontend/share/icons/${SRC_ICO} .FORCE resource.syso: ./internal/frontend/share/info.rc ./internal/frontend/share/${SRC_ICO} .FORCE
rm -f ./*.syso rm -f ./*.syso
windres --target=pe-x86-64 -I ./internal/frontend/share/icons/ -D ICO_FILE=${SRC_ICO} -D EXE_NAME="${EXE_NAME}" -D FILE_VERSION="${APP_VERSION}" -D ORIGINAL_FILE_NAME="${EXE}" -D PRODUCT_VERSION="${APP_VERSION}" -D FILE_VERSION_COMMA=${APP_VERSION_COMMA} -D YEAR=${WINDRES_YEAR} -o $@ $< windres --target=pe-x86-64 -I ./internal/frontend/share/ -D ICO_FILE=${SRC_ICO} -D EXE_NAME="${EXE_NAME}" -D FILE_VERSION="${APP_VERSION}" -D ORIGINAL_FILE_NAME="${EXE}" -D PRODUCT_VERSION="${APP_VERSION}" -D FILE_VERSION_COMMA=${APP_VERSION_COMMA} -D YEAR=${WINDRES_YEAR} -o $@ $<
## Rules for therecipe/qt ## Rules for therecipe/qt
.PHONY: prepare-vendor update-vendor update-qt-docs .PHONY: prepare-vendor update-vendor update-qt-docs
@ -278,7 +278,7 @@ RUN_FLAGS?=-m -l=${LOG} --log-imap=${LOG_IMAP} ${LOG_SMTP}
run: run-nogui-cli run: run-nogui-cli
run-qt: ${EXE_TARGET} run-qt: ${EXE_TARGET}
PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} | tee last.log PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} 2>&1 | tee last.log
run-qt-cli: ${EXE_TARGET} run-qt-cli: ${EXE_TARGET}
PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} -c PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} -c
@ -296,9 +296,7 @@ run-qml-preview:
clean-frontend-qt: clean-frontend-qt:
# TODO: $(MAKE) -C internal/frontend/qt -f Makefile.local clean $(MAKE) -C internal/frontend -f Makefile.local clean
clean-frontend-qt-common:
# TODO: $(MAKE) -C internal/frontend/qt-common -f Makefile.local clean
clean-vendor: clean-frontend-qt clean-frontend-qt-common clean-vendor: clean-frontend-qt clean-frontend-qt-common
rm -rf ./vendor rm -rf ./vendor

4
go.mod
View File

@ -56,8 +56,10 @@ require (
github.com/ricochet2200/go-disk-usage/du v0.0.0-20210707232629-ac9918953285 github.com/ricochet2200/go-disk-usage/du v0.0.0-20210707232629-ac9918953285
github.com/sirupsen/logrus v1.7.0 github.com/sirupsen/logrus v1.7.0
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/stretchr/objx v0.2.0 // indirect
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d // indirect
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d // indirect
github.com/urfave/cli/v2 v2.2.0 github.com/urfave/cli/v2 v2.2.0
github.com/vmihailenco/msgpack/v5 v5.1.3 github.com/vmihailenco/msgpack/v5 v5.1.3
go.etcd.io/bbolt v1.3.6 go.etcd.io/bbolt v1.3.6

18
go.sum
View File

@ -42,8 +42,6 @@ github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4= github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
github.com/ProtonMail/go-rfc5322 v0.8.0 h1:7emrf75n3CDIduQflx7aT1nJa5h/kGsiFKUYX/+IAkU= github.com/ProtonMail/go-rfc5322 v0.8.0 h1:7emrf75n3CDIduQflx7aT1nJa5h/kGsiFKUYX/+IAkU=
github.com/ProtonMail/go-rfc5322 v0.8.0/go.mod h1:BwpTbkJxkMGkc+pC84AXZnwuWOisEULBpfPIyIKS/Us= github.com/ProtonMail/go-rfc5322 v0.8.0/go.mod h1:BwpTbkJxkMGkc+pC84AXZnwuWOisEULBpfPIyIKS/Us=
github.com/ProtonMail/go-srp v0.0.0-20210910093455-a843a0b9adff h1:eiue56XAPSkOpsy5Fwnyz4+Vd7i2cN5D4orc++Irt1g=
github.com/ProtonMail/go-srp v0.0.0-20210910093455-a843a0b9adff/go.mod h1:Uvv5cqSGCs8MTZ8sbKiCkBnaB6/OA3eq2mc77tl2VVA=
github.com/ProtonMail/go-srp v0.0.1 h1:J0O9Zb5XTC6iDrB7feH41cu+TUEB+l7uHctXIK6oS2o= github.com/ProtonMail/go-srp v0.0.1 h1:J0O9Zb5XTC6iDrB7feH41cu+TUEB+l7uHctXIK6oS2o=
github.com/ProtonMail/go-srp v0.0.1/go.mod h1:Uvv5cqSGCs8MTZ8sbKiCkBnaB6/OA3eq2mc77tl2VVA= github.com/ProtonMail/go-srp v0.0.1/go.mod h1:Uvv5cqSGCs8MTZ8sbKiCkBnaB6/OA3eq2mc77tl2VVA=
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ= github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ=
@ -195,6 +193,8 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e h1:XWcjeEtTFTOVA9Fs1w7n2XBftk5ib4oZrhzWk0B+3eA=
github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
@ -264,6 +264,7 @@ github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
@ -361,9 +362,11 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 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.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
@ -393,6 +396,12 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLwevFjaRiG8eeCMlq9WXQ2OO2bk=
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d h1:hAZyEG2swPRWjF0kqqdGERXUazYnRJdAk4a58f14z7Y=
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d/go.mod h1:7m8PDYDEtEVqfjoUQc2UrFqhG0CDmoVJjRlQxexndFc=
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d h1:AJRoBel/g9cDS+yE8BcN3E+TDD/xNAguG21aoR8DAIE=
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d/go.mod h1:mH55Ek7AZcdns5KPp99O0bg+78el64YCYWHiQKrOdt4=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
@ -430,6 +439,7 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -473,6 +483,7 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
@ -505,7 +516,9 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -539,6 +552,7 @@ golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=

11
internal/frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
# Auto generated
moc.cpp
moc.go
moc.h
moc_cgo_*.go
moc_moc.h
rcc.cpp
rcc.qrc
rcc_cgo_*.go
*.qmlc

View File

@ -0,0 +1,14 @@
FILES=$(shell find . -iname 'rcc.qrc')
FILES+=$(shell find . -iname 'rcc.cpp')
FILES+=$(shell find . -iname 'rcc_cgo*.go')
FILES+=$(shell find . -iname 'moc.go')
FILES+=$(shell find . -iname 'moc.cpp')
FILES+=$(shell find . -iname 'moc.h')
FILES+=$(shell find . -iname 'moc_cgo*.go')
FILES+=$(shell find ./qml -iname '*.qmlc')
clean:
rm -f ${FILES}

View File

@ -0,0 +1,76 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Package clientconfig provides automatic config of IMAP and SMTP.
// For now only for Apple Mail.
package clientconfig
import (
"errors"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/sirupsen/logrus"
)
type AutoConfig interface {
Name() string
Configure(imapPort int, smtpPort int, imapSSl, smtpSSL bool, user types.User, address string) error
}
var (
available = map[string]AutoConfig{} //nolint[gochecknoglobals]
ErrNotAvailable = errors.New("configuration not available")
)
const AppleMailClient = "Apple Mail"
func ConfigureAppleMail(user types.User, address string, s *settings.Settings) (needRestart bool, err error) {
return configure(AppleMailClient, user, address, s)
}
func configure(configName string, user types.User, address string, s *settings.Settings) (needRestart bool, err error) {
log := logrus.WithField("pkg", "client_config").WithField("client", configName)
config, ok := available[configName]
if !ok {
return false, ErrNotAvailable
}
imapPort := s.GetInt(settings.IMAPPortKey)
imapSSL := false
smtpPort := s.GetInt(settings.SMTPPortKey)
smtpSSL := s.GetBool(settings.SMTPSSLKey)
if address == "" {
address = user.GetPrimaryAddress()
}
if configName == AppleMailClient {
// If configuring apple mail for Catalina or newer, users should use SSL.
needRestart = false
if !smtpSSL && useragent.IsCatalinaOrNewer() {
smtpSSL = true
s.SetBool(settings.SMTPSSLKey, true)
log.Warn("Detected Catalina or newer with bad SMTP SSL settings, now using SSL, bridge needs to restart")
needRestart = true
}
}
return needRestart, config.Configure(imapPort, smtpPort, imapSSL, smtpSSL, user, address)
}

View File

@ -0,0 +1,121 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build darwin
package clientconfig
import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/pkg/mobileconfig"
)
const (
bigSurPreferncesPane = "/System/Library/PreferencePanes/Profiles.prefPane"
)
func init() { //nolint[gochecknoinit]
available[AppleMailClient] = &appleMail{}
}
type appleMail struct{}
func (c *appleMail) Name() string { return AppleMailClient }
func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, user types.User, address string) error {
mc := prepareMobileConfig(imapPort, smtpPort, imapSSL, smtpSSL, user, address)
confPath, err := saveConfigTemporarily(mc)
if err != nil {
return err
}
if useragent.IsBigSurOrNewer() {
return exec.Command("open", bigSurPreferncesPane, confPath).Run() //nolint[gosec] G204: open command is safe, mobileconfig is generated by us
}
return exec.Command("open", confPath).Run() //nolint[gosec] G204: open command is safe, mobileconfig is generated by us
}
func prepareMobileConfig(imapPort, smtpPort int, imapSSL, smtpSSL bool, user types.User, address string) *mobileconfig.Config {
displayName := address
addresses := address
if user.IsCombinedAddressMode() {
displayName = user.GetPrimaryAddress()
addresses = strings.Join(user.GetAddresses(), ",")
}
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
return &mobileconfig.Config{
EmailAddress: addresses,
DisplayName: displayName,
Identifier: "protonmail " + displayName + timestamp,
IMAP: &mobileconfig.IMAP{
Hostname: bridge.Host,
Port: imapPort,
TLS: imapSSL,
Username: displayName,
Password: user.GetBridgePassword(),
},
SMTP: &mobileconfig.SMTP{
Hostname: bridge.Host,
Port: smtpPort,
TLS: smtpSSL,
Username: displayName,
},
}
}
func saveConfigTemporarily(mc *mobileconfig.Config) (fname string, err error) {
dir, err := ioutil.TempDir("", "protonmail-autoconfig")
if err != nil {
return
}
// Make sure the temporary file is deleted.
go (func() {
<-time.After(10 * time.Minute)
_ = os.RemoveAll(dir)
})()
// Make sure the file is only readable for the current user.
fname = filepath.Clean(filepath.Join(dir, "protonmail.mobileconfig"))
f, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE, 0600)
if err != nil {
return
}
if err = mc.WriteOut(f); err != nil {
_ = f.Close()
return
}
_ = f.Close()
return
}

View File

@ -24,6 +24,7 @@ import (
"github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/config/useragent" "github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/frontend/cli" "github.com/ProtonMail/proton-bridge/internal/frontend/cli"
"github.com/ProtonMail/proton-bridge/internal/frontend/qt"
"github.com/ProtonMail/proton-bridge/internal/frontend/types" "github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/locations" "github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/updater" "github.com/ProtonMail/proton-bridge/internal/updater"
@ -59,6 +60,23 @@ func New(
) Frontend { ) Frontend {
bridgeWrap := types.NewBridgeWrap(bridge) bridgeWrap := types.NewBridgeWrap(bridge)
switch frontendType { switch frontendType {
case "qt":
return qt.New(
version,
buildVersion,
programName,
showWindowOnStart,
panicHandler,
locations,
settings,
eventListener,
updater,
userAgent,
bridgeWrap,
noEncConfirmator,
autostart,
restarter,
)
case "cli": case "cli":
return cli.New( return cli.New(
panicHandler, panicHandler,

View File

@ -27,12 +27,61 @@ Item {
property ColorScheme colorScheme property ColorScheme colorScheme
property var user property var user
implicitHeight: children[0].implicitHeight property var _spacing: 12
implicitWidth: children[0].implicitWidth property var _leftRightMargins: {
switch(root.type) {
case AccountDelegate.SmallView: return 12
case AccountDelegate.LargeView: return 0
}
}
property var _topBottomMargins: {
switch(root.type) {
case AccountDelegate.SmallView: return 10
case AccountDelegate.LargeView: return 0
}
}
property color usedSpaceColor : {
if (!root.enabled) return root.colorScheme.text_weak
if (root.type == AccountDelegate.SmallView) return root.colorScheme.text_weak
if (root.usedFraction < .50) return root.colorScheme.signal_success
if (root.usedFraction < .75) return root.colorScheme.signal_warning
return root.colorScheme.signal_danger
}
property real usedFraction: root.user.totalBytes ? root.user.usedBytes / root.user.totalBytes : 0
property string totalSpace: root.spaceWithUnits(root.user.totalBytes)
property string usedSpace: root.spaceWithUnits(root.user.usedBytes)
function spaceWithUnits(bytes){
if (bytes*1 !== bytes ) return "0 kB"
var units = ['B',"kB", "MB", "TB"];
var i = parseInt(Math.floor(Math.log(bytes)/Math.log(1024)));
return Math.round(bytes*10 / Math.pow(1024, i))/10 + " " + units[i]
}
signal clicked()
// width expected to be set by parent object
implicitHeight : children[0].implicitHeight + 2*root._topBottomMargins
enum ViewType{
SmallView, LargeView
}
property var type : AccountDelegate.SmallView
RowLayout { RowLayout {
anchors.fill: parent spacing: root._spacing
spacing: 12
anchors {
top: root.top
left: root.left
right: root.rigth
leftMargin : root._leftRightMargins
rightMargin : root._leftRightMargins
topMargin : root._topBottomMargins
bottomMargin : root._topBottomMargins
}
Rectangle { Rectangle {
id: avatar id: avatar
@ -48,8 +97,19 @@ Item {
colorScheme: root.colorScheme colorScheme: root.colorScheme
anchors.fill: parent anchors.fill: parent
text: root.user.avatarText.toUpperCase() text: root.user.avatarText.toUpperCase()
type: Label.LabelType.Body type: {
color: root.colorScheme.text_invert switch (root.type) {
case AccountDelegate.SmallView: return Label.Body
case AccountDelegate.LargeView: return Label.Title
}
}
font.weight: Font.Normal
color: {
switch(root.type) {
case AccountDelegate.SmallView: return root.colorScheme.text_norm
case AccountDelegate.LargeView: return root.colorScheme.text_invert
}
}
horizontalAlignment: Qt.AlignHCenter horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter verticalAlignment: Qt.AlignVCenter
} }
@ -63,16 +123,78 @@ Item {
spacing: 0 spacing: 0
Label { Label {
Layout.maximumWidth: root.width - (
root._spacing + avatar.width + 2*root._leftRightMargins
)
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: user.username text: user.username
type: Label.LabelType.Body type: {
switch (root.type) {
case AccountDelegate.SmallView: return Label.Body
case AccountDelegate.LargeView: return Label.Title
}
}
elide: Text.ElideMiddle
}
Item { implicitHeight: root.type == AccountDelegate.LargeView ? 6 : 0 }
RowLayout {
Label {
colorScheme: root.colorScheme
text: root.usedSpace
color: root.usedSpaceColor
type: {
switch (root.type) {
case AccountDelegate.SmallView: return Label.Caption
case AccountDelegate.LargeView: return Label.Body
}
}
} }
Label { Label {
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: user.captionText text: " / " + root.totalSpace
type: Label.LabelType.Caption color: root.colorScheme.text_weak
type: {
switch (root.type) {
case AccountDelegate.SmallView: return Label.Caption
case AccountDelegate.LargeView: return Label.Body
} }
} }
} }
} }
Rectangle {
visible: root.type == AccountDelegate.LargeView
width: 140
height: 4
radius: 3
color: root.colorScheme.border_weak
Rectangle {
radius: 3
color: root.usedSpaceColor
anchors {
top : parent.top
bottom : parent.bottom
left : parent.left
}
width: Math.min(1,Math.max(0.02,root.usedFraction)) * parent.width
}
}
}
Item {
Layout.fillWidth: true
}
}
MouseArea {
anchors.fill: root
onClicked: root.clicked()
}
}

View File

@ -21,34 +21,245 @@ import QtQuick.Controls 2.12
import Proton 4.0 import Proton 4.0
Item { ScrollView {
id: root id: root
property ColorScheme colorScheme property ColorScheme colorScheme
property var backend
property var notifications
property var user
implicitHeight: children[0].implicitHeight clip: true
implicitWidth: children[0].implicitWidth contentWidth: pane.width
contentHeight: pane.height
ColumnLayout { property int _leftRightMargins: 64
anchors.fill: parent property int _topBottomMargins: 68
spacing: 0 property int _spacing: 22
Rectangle { Rectangle {
Layout.fillWidth: true anchors {
Layout.minimumHeight: 277 bottom: pane.bottom
Layout.maximumHeight: 277
color: root.colorScheme.background_norm
ColumnLayout {
} }
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: root.colorScheme.background_weak color: root.colorScheme.background_weak
width: root.width
height: configuration.height + root._topBottomMargins
}
signal showSignIn()
signal showSetupGuide(var user, string address)
ColumnLayout {
id: pane
width: root.width
ColumnLayout {
spacing: root._spacing
Layout.topMargin: root._topBottomMargins
Layout.leftMargin: root._leftRightMargins
Layout.rightMargin: root._leftRightMargins
Layout.maximumWidth: root.width - 2*root._leftRightMargins
RowLayout { // account delegate with action buttons
Layout.fillWidth: true
AccountDelegate {
Layout.fillWidth: true
colorScheme: root.colorScheme
user: root.user
type: AccountDelegate.LargeView
enabled: root.user.loggedIn
}
Button {
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme
text: qsTr("Sign out")
secondary: true
visible: root.user.loggedIn
onClicked: root.user.logout()
}
Button {
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme
icon.source: "icons/ic-trash.svg"
secondary: true
visible: root.user.loggedIn
onClicked: root.user.remove()
}
Button {
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme
text: qsTr("Sign in")
secondary: true
visible: !root.user.loggedIn
onClicked: root.parent.rightContent.showSignIn()
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: root.colorScheme.border_weak
}
SettingsItem {
colorScheme: root.colorScheme
text: qsTr("Email clients")
actionText: qsTr("Configure")
description: "MISSING WIREFRAME" // TODO
type: SettingsItem.Button
enabled: root.user.loggedIn
visible: !root.user.splitMode
onClicked: root.showSetupGuide(root.user,user.addresses[0])
}
SettingsItem {
id: splitMode
colorScheme: root.colorScheme
text: qsTr("Split addresses")
description: qsTr("Split addresses allows you to configure multiple email addresses individually. Changing its mode will require you to delete your accounts(s) from your email client and begin the setup process from scratch.")
type: SettingsItem.Toggle
checked: root.user.splitMode
visible: root.user.addresses.length > 1
enabled: root.user.loggedIn
onClicked: {
if (!splitMode.checked){
root.notifications.askEnableSplitMode(user)
} else {
root.user.toggleSplitMode(!splitMode.checked)
}
}
}
RowLayout {
Layout.fillWidth: true
enabled: root.user.loggedIn
visible: root.user.splitMode
ComboBox {
id: addressSelector
Layout.fillWidth: true
model: root.user.addresses
property var _topBottomMargins : 8
property var _leftRightMargins : 16
background: RoundedRectangle {
radiusTopLeft : 6
radiusTopRight : 6
radiusBottomLeft : addressSelector.down ? 0 : 6
radiusBottomRight : addressSelector.down ? 0 : 6
height: addressSelector.contentItem.height
//width: addressSelector.contentItem.width
fillColor : root.colorScheme.background_norm
strokeColor : root.colorScheme.border_norm
strokeWidth : 1
}
delegate: Rectangle {
id: listItem
width: root.width
height: children[0].height + 4 + 2*addressSelector._topBottomMargins
Label {
anchors {
top : parent.top
left : parent.left
topMargin : addressSelector._topBottomMargins + 4
leftMargin : addressSelector._leftRightMargins
}
colorScheme: root.colorScheme
text: modelData
elide: Text.ElideMiddle
}
property bool isOver: false
color: {
if (listItem.isOver) return root.colorScheme.interaction_weak_hover
if (addressSelector.highlightedIndex === index) return root.colorScheme.interaction_weak
return root.colorScheme.background_norm
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: listItem.isOver = true
onExited: listItem.isOver = false
onClicked : {
addressSelector.currentIndex = index
addressSelector.popup.close()
}
}
}
contentItem: Label {
topPadding : addressSelector._topBottomMargins+4
bottomPadding : addressSelector._topBottomMargins
leftPadding : addressSelector._leftRightMargins
rightPadding : addressSelector._leftRightMargins
colorScheme: root.colorScheme
text: addressSelector.displayText
elide: Text.ElideMiddle
}
}
Button {
colorScheme: root.colorScheme
text: qsTr("Configure")
secondary: true
onClicked: root.showSetupGuide(root.user, addressSelector.displayText)
}
}
Item {implicitHeight: 1}
}
ColumnLayout {
id: configuration
Layout.bottomMargin: root._topBottomMargins
Layout.leftMargin: root._leftRightMargins
Layout.rightMargin: root._leftRightMargins
Layout.maximumWidth: root.width - 2*root._leftRightMargins
spacing: root._spacing
visible: root.user.loggedIn
property string currentAddress: addressSelector.displayText
Item {height: 1}
Label {
colorScheme: root.colorScheme
text: qsTr("Mailbox details")
type: Label.Body_semibold
}
Configuration {
colorScheme: root.colorScheme
title: qsTr("IMAP")
hostname: root.backend.hostname
port: root.backend.portIMAP.toString()
username: configuration.currentAddress
password: root.user.password
security: "STARTTLS"
}
Configuration {
colorScheme: root.colorScheme
title: qsTr("SMTP")
hostname : root.backend.hostname
port : root.backend.portSMTP.toString()
username : configuration.currentAddress
password : root.user.password
security : root.backend.useSSLforSMTP ? "SSL" : "STARTTLS"
}
} }
} }
} }

View File

@ -28,9 +28,13 @@ Popup {
property ColorScheme colorScheme property ColorScheme colorScheme
property Notification notification property Notification notification
property var mainWindow
topMargin: 37
leftMargin: (mainWindow.width - root.implicitWidth)/2
implicitHeight: contentLayout.implicitHeight + contentLayout.anchors.topMargin + contentLayout.anchors.bottomMargin implicitHeight: contentLayout.implicitHeight + contentLayout.anchors.topMargin + contentLayout.anchors.bottomMargin
implicitWidth: contentLayout.implicitWidth + contentLayout.anchors.leftMargin + contentLayout.anchors.rightMargin implicitWidth: 600 // contentLayout.implicitWidth + contentLayout.anchors.leftMargin + contentLayout.anchors.rightMargin
popupType: ApplicationWindow.PopupType.Banner popupType: ApplicationWindow.PopupType.Banner

View File

@ -25,12 +25,7 @@ import Notifications 1.0
QtObject { QtObject {
id: root id: root
property var backend property var backend: go
signal login(string username, string password)
signal login2FA(string username, string code)
signal login2Password(string username, string password)
signal loginAbort(string username)
property Notifications _notifications: Notifications { property Notifications _notifications: Notifications {
id: notifications id: notifications
@ -45,19 +40,23 @@ QtObject {
visible: false visible: false
backend: root.backend backend: root.backend
notifications: notifications notifications: root._notifications
onLogin: { onLogin: {
root.login(username, password) backend.login(username, password)
} }
onLogin2FA: { onLogin2FA: {
root.login2FA(username, code) backend.login2FA(username, code)
} }
onLogin2Password: { onLogin2Password: {
root.login2Password(username, password) backend.login2Password(username, password)
} }
onLoginAbort: { onLoginAbort: {
root.loginAbort(username) backend.loginAbort(username)
}
onVisibleChanged: {
backend.dockIconVisible = visible
} }
} }
@ -66,20 +65,45 @@ QtObject {
visible: false visible: false
backend: root.backend backend: root.backend
notifications: notifications notifications: root._notifications
property var x_center: 10
property var x_min: 0
property var x_max: 100
property var y_center: 1000
property var y_min: 0
property var y_max: 10000
x: bound(x_center,x_min, x_max-statusWindow.width)
y: bound(y_center,y_min, y_max-statusWindow.height)
onShowMainWindow: { onShowMainWindow: {
mainWindow.visible = true mainWindow.visible = true
} }
onShowHelp: { onShowHelp: {
mainWindow.showHelp()
mainWindow.visible = true
} }
onShowSettings: { onShowSettings: {
mainWindow.showSettings()
mainWindow.visible = true
} }
onShowSignIn: {
mainWindow.showSignIn(username)
mainWindow.visible = true
}
onQuit: { onQuit: {
backend.quit() backend.quit()
} }
function bound(num, lower_limit, upper_limit) {
return Math.max(lower_limit, Math.min(upper_limit, num))
}
} }
property SystemTrayIcon _trayIcon: SystemTrayIcon { property SystemTrayIcon _trayIcon: SystemTrayIcon {
@ -88,96 +112,52 @@ QtObject {
iconSource: "./icons/ic-systray.svg" iconSource: "./icons/ic-systray.svg"
onActivated: { onActivated: {
function calcStatusWindowPosition(statusWidth, statusHeight) { function calcStatusWindowPosition(statusWidth, statusHeight) {
function bound(num, lower_limit, upper_limit) { function isInInterval(num, lower_limit, upper_limit) {
return Math.max(lower_limit, Math.min(upper_limit, num)) return lower_limit <= num && num <= upper_limit
}
// checks if rect1 fits within rect2
function isRectFit(rect1, rect2) {
//if (rect2.)
if ((rect2.left > rect1.left) ||
(rect2.right < rect1.right) ||
(rect2.top > rect1.top) ||
(rect2.bottom < rect1.bottom)) {
return false
} }
return true
}
// First we get icon center position. // First we get icon center position.
// On some platforms (X11 / Wayland) Qt does not provide icon geometry info. // On some platforms (X11 / Wayland) Qt does not provide icon geometry info.
// In this case we rely on cursor position // In this case we rely on cursor position
var iconWidth = geometry.width *1.2
var iconHeight = geometry.height *1.2
var iconCenter = Qt.point(geometry.x + (geometry.width / 2), geometry.y + (geometry.height / 2)) var iconCenter = Qt.point(geometry.x + (geometry.width / 2), geometry.y + (geometry.height / 2))
if (geometry.width == 0 && geometry.height == 0) { if (geometry.width == 0 && geometry.height == 0) {
iconCenter = backend.getCursorPos() iconCenter = backend.getCursorPos()
// fallback: simple guess, no data to estimate
iconWidth = 25
iconHeight = 25
} }
// Now bound this position to virtual screen available rect // Find screen
// TODO: here we should detect which screen mouse is on and use that screen available geometry to bound var screen = Qt.application.screens[0]
iconCenter.x = bound(iconCenter.x, 0, Qt.application.screens[0].desktopAvailableWidth)
iconCenter.y = bound(iconCenter.y, 0, Qt.application.screens[0].desktopAvailableHeight)
var x = 0 for (var i in Qt.application.screens) {
var y = 0 screen = Qt.application.screens[i]
if (
// Check if window may fit above isInInterval(iconCenter.x, screen.virtualX, screen.virtualX+screen.width) &&
x = iconCenter.x - statusWidth / 2 isInInterval(iconCenter.y, screen.virtualY, screen.virtualY+screen.heigh)
y = iconCenter.y - statusHeight ) {
if (isRectFit( return
Qt.rect(x, y, statusWidth, statusHeight), }
// TODO: we should detect which screen mouse is on and use that screen available geometry to bound
Qt.rect(0, 0, Qt.application.screens[0].desktopAvailableWidth, Qt.application.screens[0].desktopAvailableHeight)
)) {
return Qt.point(x, y)
} }
// Check if window may fit below // Calculate allowed square where status window top left corner can be positioned
x = iconCenter.x - statusWidth / 2 statusWindow.x_center = iconCenter.x
y = iconCenter.y statusWindow.y_center = iconCenter.y
if (isRectFit( statusWindow.x_min = screen.virtualX + iconWidth
Qt.rect(x, y, statusWidth, statusHeight), statusWindow.x_max = screen.virtualX + screen.width - iconWidth
// TODO: we should detect which screen mouse is on and use that screen available geometry to bound statusWindow.y_min = screen.virtualY + iconHeight
Qt.rect(0, 0, Qt.application.screens[0].desktopAvailableWidth, Qt.application.screens[0].desktopAvailableHeight) statusWindow.y_max = screen.virtualY + screen.height - iconHeight
)) {
return Qt.point(x, y)
}
// Check if window may fit to the left
x = iconCenter.x - statusWidth
y = iconCenter.y - statusHeight / 2
if (isRectFit(
Qt.rect(x, y, statusWidth, statusHeight),
// TODO: we should detect which screen mouse is on and use that screen available geometry to bound
Qt.rect(0, 0, Qt.application.screens[0].desktopAvailableWidth, Qt.application.screens[0].desktopAvailableHeight)
)) {
return Qt.point(x, y)
}
// Check if window may fit to the right
x = iconCenter.x
y = iconCenter.y - statusHeight / 2
if (isRectFit(
Qt.rect(x, y, statusWidth, statusHeight),
// TODO: we should detect which screen mouse is on and use that screen available geometry to bound
Qt.rect(0, 0, Qt.application.screens[0].desktopAvailableWidth, Qt.application.screens[0].desktopAvailableHeight)
)) {
return Qt.point(x, y)
}
// TODO: add fallback
} }
switch (reason) { switch (reason) {
case SystemTrayIcon.Unknown: case SystemTrayIcon.Unknown:
break; break;
case SystemTrayIcon.Context: case SystemTrayIcon.Context:
case SystemTrayIcon.Trigger:!statusWindow.visible case SystemTrayIcon.Trigger:
if (!statusWindow.visible) { calcStatusWindowPosition()
var point = calcStatusWindowPosition(statusWindow.width, statusWindow.height)
statusWindow.x = point.x
statusWindow.y = point.y
}
statusWindow.visible = !statusWindow.visible statusWindow.visible = !statusWindow.visible
break break
case SystemTrayIcon.DoubleClick: case SystemTrayIcon.DoubleClick:

View File

@ -236,6 +236,53 @@ ColumnLayout {
} }
} }
Button {
colorScheme: root.colorScheme
text: "Login Finished"
onClicked: {
root.backend.loginFinished()
user.resetLoginRequests()
}
}
RowLayout {
TextField {
colorScheme: root.colorScheme
label: "used:"
text: user && user.usedBytes ? user.usedBytes : 0
validator: DoubleValidator {bottom: 1; top: 1024*1024*1024*1024*1024}
onEditingFinished: {
user.usedBytes = parseFloat(text)
}
implicitWidth: 200
}
TextField {
colorScheme: root.colorScheme
label: "total:"
text: user && user.totalBytes ? user.totalBytes : 0
validator: DoubleValidator {bottom: 1; top: 1024*1024*1024*1024*1024}
onEditingFinished: {
user.totalBytes = parseFloat(text)
}
implicitWidth: 200
}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Split mode"}
Toggle { colorScheme: root.colorScheme; checked: user ? user.splitMode : false; onClicked: {user.splitMode = !user.splitMode}}
Button { colorScheme: root.colorScheme; text: "Toggle Finished"; onClicked: {user.toggleSplitModeFinished()}}
}
TextArea {
colorScheme: root.colorScheme
text: user && user.addresses ? user.addresses.join("\n") : "user@protonmail.com"
Layout.fillWidth: true
onEditingFinished: {
user.addresses = text.split("\n")
}
}
Item { Item {
Layout.fillHeight: true Layout.fillHeight: true

View File

@ -33,8 +33,10 @@ import Notifications 1.0
Window { Window {
id: root id: root
width: 640 x: 10
height: 480 y: 10
width: 800
height: 600
property ColorScheme colorScheme: ProtonStyle.darkStyle property ColorScheme colorScheme: ProtonStyle.darkStyle
@ -103,12 +105,21 @@ Window {
QtObject { QtObject {
property string username: "" property string username: ""
property bool loggedIn: false property bool loggedIn: false
property bool splitMode: false
property bool setupGuideSeen: true property bool setupGuideSeen: true
property string captionText: "50.3 MB / 20 GB" property var usedBytes: 5350*1024*1024
property var totalBytes: 20*1024*1024*1024
property string avatarText: "jd" property string avatarText: "jd"
property string password: "SMj975NnEYYsqu55GGmlpv"
property var addresses: [
"janedoe@protonmail.com",
"jane@pm.me",
"jdoe@pm.me"
]
signal loginUsernamePasswordError() signal loginUsernamePasswordError()
signal loginFreeUserError() signal loginFreeUserError()
signal loginConnectionError() signal loginConnectionError()
@ -130,6 +141,30 @@ Window {
root.log("<- User (" + username + "): " + msg) root.log("<- User (" + username + "): " + msg)
} }
function toggleSplitMode(makeActive) {
userSignal("toggle split mode "+makeActive)
}
signal toggleSplitModeFinished()
function configureAppleMail(address){
userSignal("confugure apple mail "+address)
}
function logout(){
userSignal("logout")
loggedIn = false
}
function remove(){
console.log("remove this", users.count)
for (var i=0; i<users.count; i++) {
if (users.get(i) === this) {
users.remove(i,1)
return
}
}
}
onLoginUsernamePasswordError: { onLoginUsernamePasswordError: {
userSignal("loginUsernamePasswordError") userSignal("loginUsernamePasswordError")
} }
@ -193,6 +228,17 @@ Window {
newLoginUser.login2PasswordRequested.connect(root.login2PasswordRequested) newLoginUser.login2PasswordRequested.connect(root.login2PasswordRequested)
newLoginUser.login2PasswordError.connect(root.login2PasswordError) newLoginUser.login2PasswordError.connect(root.login2PasswordError)
newLoginUser.login2PasswordErrorAbort.connect(root.login2PasswordErrorAbort) newLoginUser.login2PasswordErrorAbort.connect(root.login2PasswordErrorAbort)
// add one user on start
var haveUserOnStart = false
if (haveUserOnStart) {
var newUserObject = root.userComponent.createObject(root)
newUserObject.username = "LerooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooyJenkins@protonmail.com"
newUserObject.loggedIn = true
newUserObject.setupGuideSeen = true
root.users.append( { object: newUserObject } )
}
} }
@ -216,6 +262,10 @@ Window {
TabButton { TabButton {
text: "Log" text: "Log"
} }
TabButton {
text: "Settings signals"
}
} }
StackLayout { StackLayout {
@ -284,6 +334,7 @@ Window {
enabled: bridge === undefined || bridge === null enabled: bridge === undefined || bridge === null
onClicked: { onClicked: {
bridge = bridgeComponent.createObject() bridge = bridgeComponent.createObject()
if (true) bridge._mainWindow.show()
} }
} }
@ -368,9 +419,8 @@ Window {
spacing: 5 spacing: 5
Switch { Switch {
colorScheme: root.colorScheme
text: "Internet connection" text: "Internet connection"
colorScheme: root.colorScheme
checked: true checked: true
onCheckedChanged: { onCheckedChanged: {
checked ? root.internetOn() : root.internetOff() checked ? root.internetOn() : root.internetOff()
@ -378,115 +428,124 @@ Window {
} }
Button { Button {
colorScheme: root.colorScheme
text: "Update manual ready" text: "Update manual ready"
colorScheme: root.colorScheme
onClicked: { onClicked: {
root.updateManualReady("3.14.1592") root.updateManualReady("3.14.1592")
} }
} }
Button {
colorScheme: root.colorScheme
Button {
text: "Update manual done" text: "Update manual done"
colorScheme: root.colorScheme
onClicked: { onClicked: {
root.updateManualRestartNeeded() root.updateManualRestartNeeded()
} }
} }
Button {
colorScheme: root.colorScheme
Button {
text: "Update manual error" text: "Update manual error"
colorScheme: root.colorScheme
onClicked: { onClicked: {
root.updateManualError() root.updateManualError()
} }
} }
Button {
colorScheme: root.colorScheme
Button {
text: "Update force" text: "Update force"
colorScheme: root.colorScheme
onClicked: { onClicked: {
root.updateForce("3.14.1592") root.updateForce("3.14.1592")
} }
} }
Button {
colorScheme: root.colorScheme
Button {
text: "Update force error" text: "Update force error"
colorScheme: root.colorScheme
onClicked: { onClicked: {
root.updateForceError() root.updateForceError()
} }
} }
Button { Button {
colorScheme: root.colorScheme
text: "Update silent done" text: "Update silent done"
colorScheme: root.colorScheme
onClicked: { onClicked: {
root.updateSilentRestartNeeded() root.updateSilentRestartNeeded()
} }
} }
Button { Button {
text: "Update silent error"
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: "Update solent error"
onClicked: { onClicked: {
root.updateSilentError() root.updateSilentError()
} }
} }
Button { Button {
text: "Update is latest version"
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: "Bug report send OK"
onClicked: { onClicked: {
root.updateIsLatestVersion()
}
}
Button {
text: "Bug report send OK"
colorScheme: root.colorScheme
onClicked: {
root.reportBugFinished()
root.bugReportSendSuccess() root.bugReportSendSuccess()
} }
} }
Button { Button {
colorScheme: root.colorScheme
text: "Bug report send error" text: "Bug report send error"
colorScheme: root.colorScheme
onClicked: { onClicked: {
root.reportBugFinished()
root.bugReportSendError() root.bugReportSendError()
} }
} }
Button { Button {
colorScheme: root.colorScheme
text: "Cache anavailable" text: "Cache anavailable"
onClicked: {
root.cacheAnavailable()
}
}
Button {
colorScheme: root.colorScheme colorScheme: root.colorScheme
onClicked: {
root.cacheUnavailable()
}
}
Button {
text: "Cache can't move" text: "Cache can't move"
colorScheme: root.colorScheme
onClicked: { onClicked: {
root.cacheCantMove() root.cacheCantMove()
} }
} }
Button { Button {
text: "Cache location change success"
onClicked: {
root.cacheLocationChangeSuccess()
}
colorScheme: root.colorScheme colorScheme: root.colorScheme
}
Button {
text: "Disk full" text: "Disk full"
colorScheme: root.colorScheme
onClicked: { onClicked: {
root.diskFull() root.diskFull()
} }
} }
} }
} }
TextArea { TextArea {
colorScheme: root.colorScheme
id: logTextArea id: logTextArea
colorScheme: root.colorScheme
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
@ -496,20 +555,90 @@ Window {
textFormat: TextEdit.RichText textFormat: TextEdit.RichText
//readOnly: true //readOnly: true
} }
ScrollView {
id: settingsTab
ColumnLayout {
RowLayout {
Label {colorScheme: root.colorScheme; text: "Automatic updates:"}
Toggle {colorScheme: root.colorScheme; checked: root.isAutomaticUpdateOn; onClicked: root.isAutomaticUpdateOn = !root.isAutomaticUpdateOn}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Autostart:"}
Toggle {colorScheme: root.colorScheme; checked: root.isAutostartOn; onClicked: root.isAutostartOn = !root.isAutostartOn}
Button {colorScheme: root.colorScheme; text: "Toggle finished"; onClicked: root.toggleAutostartFinished()}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Beta:"}
Toggle {colorScheme: root.colorScheme; checked: root.isBetaEnabled; onClicked: root.isBetaEnabled = !root.isBetaEnabled}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "DoH:"}
Toggle {colorScheme: root.colorScheme; checked: root.isDoHEnabled; onClicked: root.isDoHEnabled = !root.isDoHEnabled}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Ports:"}
TextField {
colorScheme:root.colorScheme
label: "IMAP"
text: root.portIMAP
onEditingFinished: root.portIMAP = this.text*1
validator: IntValidator {bottom: 1; top: 65536}
}
TextField {
colorScheme:root.colorScheme
label: "SMTP"
text: root.portSMTP
onEditingFinished: root.portSMTP = this.text*1
validator: IntValidator {bottom: 1; top: 65536}
}
Button {colorScheme: root.colorScheme; text: "Change finished"; onClicked: root.changePortFinished()}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "SMTP using SSL:"}
Toggle {colorScheme: root.colorScheme; checked: root.useSSLforSMTP; onClicked: root.useSSLforSMTP = !root.useSSLforSMTP}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Local cache:"}
Toggle {colorScheme: root.colorScheme; checked: root.isDiskCacheEnabled; onClicked: root.isDiskCacheEnabled = !root.isDiskCacheEnabled}
TextField {
colorScheme:root.colorScheme
label: "Path"
text: root.diskCachePath
implicitWidth: 160
onEditingFinished: root.diskCachePath = this.text
}
Button {colorScheme: root.colorScheme; text: "Change finished:"; onClicked: root.changeLocalCacheFinished()}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Reset:"}
Button {colorScheme: root.colorScheme; text: "Finished"; onClicked: root.resetFinished()}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Check update:"}
Button {colorScheme: root.colorScheme; text: "Finished"; onClicked: root.checkUpdatesFinished()}
}
}
}
} }
property Bridge bridge property Bridge bridge
property string goos: "linux"
property bool dockIconVisible: false
// this signals are used only when trying to login with new user (i.e. not in users model) // this signals are used only when trying to login with new user (i.e. not in users model)
signal loginUsernamePasswordError() signal loginUsernamePasswordError(string errorMsg)
signal loginFreeUserError() signal loginFreeUserError(string errorMsg)
signal loginConnectionError() signal loginConnectionError(string errorMsg)
signal login2FARequested() signal login2FARequested()
signal login2FAError() signal login2FAError(string errorMsg)
signal login2FAErrorAbort() signal login2FAErrorAbort(string errorMsg)
signal login2PasswordRequested() signal login2PasswordRequested()
signal login2PasswordError() signal login2PasswordError(string errorMsg)
signal login2PasswordErrorAbort() signal login2PasswordErrorAbort(string errorMsg)
signal loginFinished()
signal internetOff() signal internetOff()
signal internetOn() signal internetOn()
@ -521,14 +650,140 @@ Window {
signal updateForceError() signal updateForceError()
signal updateSilentRestartNeeded() signal updateSilentRestartNeeded()
signal updateSilentError() signal updateSilentError()
signal updateIsLatestVersion()
function checkUpdates(){
console.log("check updates")
}
signal checkUpdatesFinished()
property bool isDiskCacheEnabled: true
property string diskCachePath: "/home/bridge"
signal cacheUnavailable()
signal cacheCantMove()
signal cacheLocationChangeSuccess()
signal diskFull()
function changeLocalCache(enableDiskCache, diskCachePath) {
console.debug("-> disk cache", enableDiskCache, diskCachePath)
}
signal changeLocalCacheFinished()
// Settings
property bool isAutomaticUpdateOn : true
function toggleAutomaticUpdate(makeItActive) {
console.debug("-> silent updates", makeItActive, root.isAutomaticUpdateOn)
root.isAutomaticUpdateOn = makeItActive
}
property bool isAutostartOn : true // Example of settings with loading state
function toggleAutostart(makeItActive) {
console.debug("-> autostart", makeItActive, root.isAutomaticUpdateOn)
}
signal toggleAutostartFinished()
property bool isBetaEnabled : false
function toggleBeta(makeItActive){
console.debug("-> beta", makeItActive, root.isBetaEnabled)
root.isBetaEnabled = makeItActive
}
property bool isDoHEnabled : true
function toggleDoH(makeItActive){
console.debug("-> DoH", makeItActive, root.isDoHEnabled)
root.isDoHEnabled = makeItActive
}
property bool useSSLforSMTP: false
function toggleUseSSLforSMTP(makeItActive){
console.debug("-> SMTP SSL", makeItActive, root.useSSLforSMTP)
}
signal toggleUseSSLFinished()
property string hostname: "127.0.0.1"
property int portIMAP: 1143
property int portSMTP: 1025
function changePorts(imapPort, smtpPort){
console.debug("-> ports", imapPort, smtpPort)
}
function isPortFree(port){
if (port == portIMAP) return false
if (port == portSMTP) return false
if (port == 12345) return false
return true
}
signal changePortFinished()
signal portIssueIMAP()
signal portIssueSMTP()
function triggerReset() {
console.debug("-> trigger reset")
}
signal resetFinished()
property string logsPath: "/home/cuto" // StandardPaths.locate(StandardPaths.DesktopLocation)
property string version: "v2.0.X"
property string licensePath: "/home/cuto" // StandardPaths.locate(StandardPaths.DesktopLocation)
property string releaseNotesLink: "https://protonmail.com/download/bridge/early_releases.html"
property string currentEmailClient: "" // "Apple Mail 14.0"
function updateCurrentMailClient(){
currentEmailClient = "Apple Mail 14.0"
}
function reportBug(description,address,emailClient,includeLogs){
console.log("report bug")
console.log(" description",description)
console.log(" address",address)
console.log(" emailClient",emailClient)
console.log(" includeLogs",includeLogs)
}
signal reportBugFinished()
signal bugReportSendSuccess() signal bugReportSendSuccess()
signal bugReportSendError() signal bugReportSendError()
signal cacheAnavailable() property var availableKeychain: ["gnome-keyring", "pass"]
signal cacheCantMove() property string selectedKeychain
function selectKeychain(wantedKeychain){
selectedKeychain = wantedKeychain
}
signal hasNoKeychain()
signal noActiveKeyForRecipient(string email)
signal showMainWindow()
signal addressChanged(string address)
signal addressChangedLogout(string address)
signal userDisconnected(string username)
signal apiCertIssue()
function login(username, password) {
root.log("-> login(" + username + ", " + password + ")")
loginUser.username = username
loginUser.isLoginRequested = true
}
function login2FA(username, code) {
root.log("-> login2FA(" + username + ", " + code + ")")
loginUser.isLogin2FAProvided = true
}
function login2Password(username, password) {
root.log("-> login2FA(" + username + ", " + password + ")")
loginUser.isLogin2PasswordProvided = true
}
function loginAbort(username) {
root.log("-> loginAbort(" + username + ")")
loginUser.resetLoginRequests()
}
signal diskFull()
onLoginUsernamePasswordError: { onLoginUsernamePasswordError: {
console.debug("<- loginUsernamePasswordError") console.debug("<- loginUsernamePasswordError")
@ -557,6 +812,9 @@ Window {
onLogin2PasswordErrorAbort: { onLogin2PasswordErrorAbort: {
console.debug("<- login2PasswordErrorAbort") console.debug("<- login2PasswordErrorAbort")
} }
onLoginFinished: {
console.debug("<- loginFinished")
}
onInternetOff: { onInternetOff: {
console.debug("<- internetOff") console.debug("<- internetOff")
@ -571,30 +829,6 @@ Window {
Bridge { Bridge {
backend: root backend: root
onLogin: {
root.log("-> login(" + username + ", " + password + ")")
loginUser.username = username
loginUser.isLoginRequested = true
}
onLogin2FA: {
root.log("-> login2FA(" + username + ", " + code + ")")
loginUser.isLogin2FAProvided = true
}
onLogin2Password: {
root.log("-> login2FA(" + username + ", " + password + ")")
loginUser.isLogin2PasswordProvided = true
}
onLoginAbort: {
root.log("-> loginAbort(" + username + ")")
loginUser.resetLoginRequests()
}
} }
} }

View File

@ -0,0 +1,167 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import Proton 4.0
SettingsView {
id: root
property var selectedAddress
Label {
text: qsTr("Report a problem")
colorScheme: root.colorScheme
type: Label.Heading
}
TextArea {
id: description
property int _minChars: 150
property bool _inputOK: description.text.length>=description._minChars
label: qsTr("Description")
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.minimumHeight: 100
hint: description.text.length + "/800"
placeholderText: qsTr("Tell us what went wrong or isn't working (min. 150 characters).")
onEditingFinished: {
if (!description._inputOK) {
description.error = true
description.assistiveText = qsTr("Enter a problem description (min. 150 characters)")
} else {
description.error = false
description.assistiveText = ""
}
}
}
TextField {
id: address
property bool _inputOK: root.isValidEmail(address.text)
label: qsTr("Your contact email")
colorScheme: root.colorScheme
Layout.fillWidth: true
placeholderText: qsTr("e.g. jane.doe@protonmail.com")
onEditingFinished: {
if (!address._inputOK) {
address.error = true
address.assistiveText = qsTr("Enter valid email address")
} else {
address.assistiveText = ""
address.error = false
}
}
}
TextField {
id: emailClient
property bool _inputOK: emailClient.text.length > 0
label: qsTr("Your email client (including version)")
colorScheme: root.colorScheme
Layout.fillWidth: true
placeholderText: qsTr("e.g. Apple Mail 14.0")
onEditingFinished: {
if (!emailClient._inputOK) {
emailClient.assistiveText = qsTr("Enter an email client name and version")
emailClient.error = true
} else {
emailClient.assistiveText = ""
emailClient.error = false
}
}
}
RowLayout {
CheckBox {
id: includeLogs
text: qsTr("Include my recent logs")
colorScheme: root.colorScheme
checked: true
}
Button {
Layout.leftMargin: 12
text: qsTr("View logs")
secondary: true
colorScheme: root.colorScheme
onClicked: Qt.openUrlExternally("file://"+root.backend.logsPath)
}
}
Label {
text: {
var address = "bridge@protonmail.com"
var mailTo = `<a href="mailto://${address}">${address}</a>`
return qsTr("These reports are not end-to-end encrypted. In case of sensitive information, contact us at %1.").arg(mailTo)
}
colorScheme: root.colorScheme
Layout.fillWidth: true
wrapMode: Text.WordWrap
type: Label.Caption
color: root.colorScheme.text_weak
}
Button {
id: sendButton
text: qsTr("Send")
colorScheme: root.colorScheme
onClicked: root.submit()
enabled: description._inputOK && address._inputOK && emailClient._inputOK
Connections {target: root.backend; onReportBugFinished: sendButton.loading = false }
}
function setDefaultValue() {
description.text = ""
address.text = root.selectedAddress
emailClient.text = root.backend.currentEmailClient
includeLogs.checked = true
}
function isValidEmail(text){
var reEmail = /\w+@\w+\.\w+/
return reEmail.test(text)
}
function submit() {
sendButton.loading = true
root.backend.reportBug(
description.text,
address.text,
emailClient.text,
includeLogs.checked
)
}
Component.onCompleted: root.setDefaultValue()
onBack: {
root.setDefaultValue()
root.parent.showHelpView()
}
}

View File

@ -0,0 +1,73 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12
import Proton 4.0
Rectangle {
id: root
property ColorScheme colorScheme
property string title
property string hostname
property string port
property string username
property string password
property string security
implicitWidth: 304
implicitHeight: content.height + 2*root._margin
color: root.colorScheme.background_norm
radius: 9
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
Label {
colorScheme: root.colorScheme
text: root.title
type: Label.Body_semibold
}
Item{}
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 }
}
}

View File

@ -0,0 +1,81 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls.impl 2.12
import Proton 4.0
ColumnLayout {
id: root
Layout.fillWidth: true
property var colorScheme
property string label
property string value
RowLayout {
Layout.fillWidth: true
ColumnLayout {
Label {
colorScheme: root.colorScheme
text: root.label
type: Label.Body
}
TextEdit {
id: valueText
text: root.value
color: root.colorScheme.text_weak
readOnly: true
selectByMouse: true
selectByKeyboard: true
selectionColor: root.colorScheme.text_weak
}
}
Item {
Layout.fillWidth: true
}
ColorImage {
source: "icons/ic-copy.svg"
color: root.colorScheme.text_norm
height: root.colorScheme.body_font_size
MouseArea {
anchors.fill: parent
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
}
}

View File

@ -26,12 +26,26 @@ Item {
property ColorScheme colorScheme property ColorScheme colorScheme
property var backend property var backend
property var notifications
signal login(string username, string password) signal login(string username, string password)
signal login2FA(string username, string code) signal login2FA(string username, string code)
signal login2Password(string username, string password) signal login2Password(string username, string password)
signal loginAbort(string username) signal loginAbort(string username)
signal showSetupGuide(var user, string address)
property var noUser: QtObject {
property var avatarText: ""
property var username: ""
property var password: ""
property var usedBytes: 1
property var totalBytes: 1
property var loggedIn: false
property var splitMode: false
property var addresses: []
}
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: 0
@ -91,6 +105,8 @@ Item {
horizontalPadding: 0 horizontalPadding: 0
icon.source: "./icons/ic-question-circle.svg" icon.source: "./icons/ic-question-circle.svg"
onClicked: rightContent.showHelpView()
} }
Button { Button {
@ -109,10 +125,14 @@ Item {
horizontalPadding: 0 horizontalPadding: 0
icon.source: "./icons/ic-cog-wheel.svg" icon.source: "./icons/ic-cog-wheel.svg"
onClicked: rightContent.showGeneralSettings()
} }
} }
// Separator Item {implicitHeight:10}
// Separator line
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumHeight: 1 Layout.minimumHeight: 1
@ -122,14 +142,20 @@ Item {
ListView { ListView {
id: accounts id: accounts
property var _topBottomMargins: 24
property var _leftRightMargins: 16
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Layout.leftMargin: 16 Layout.leftMargin: accounts._leftRightMargins
Layout.rightMargin: 16 Layout.rightMargin: accounts._leftRightMargins
Layout.topMargin: 24 Layout.topMargin: accounts._topBottomMargins
Layout.bottomMargin: 24 Layout.bottomMargin: accounts._topBottomMargins
spacing: 12 spacing: 12
clip: true
boundsBehavior: Flickable.StopAtBounds
header: Rectangle { header: Rectangle {
height: headerLabel.height+16 height: headerLabel.height+16
@ -142,11 +168,28 @@ Item {
} }
} }
highlight: Rectangle {
color: leftBar.colorScheme.interaction_default_active
radius: 4
}
model: root.backend.users model: root.backend.users
delegate: AccountDelegate{ delegate: AccountDelegate{
width: leftBar.width - 2*accounts._leftRightMargins
id: accountDelegate id: accountDelegate
colorScheme: leftBar.colorScheme colorScheme: leftBar.colorScheme
user: modelData user: root.backend.users.get(index)
onClicked: {
var user = root.backend.users.get(index)
accounts.currentIndex = index
if (user.loggedIn) {
rightContent.showAccount()
} else {
signIn.username = user.username
rightContent.showSignIn()
}
}
} }
} }
@ -181,15 +224,16 @@ Item {
icon.source: "./icons/ic-plus.svg" icon.source: "./icons/ic-plus.svg"
onClicked: root.showSignIn() onClicked: {
signIn.username = ""
rightContent.showSignIn()
}
} }
} }
} }
} }
Rectangle { Rectangle { // right content background
id: rightPlane
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
@ -199,14 +243,44 @@ Item {
id: rightContent id: rightContent
anchors.fill: parent anchors.fill: parent
AccountView { AccountView { // 0
colorScheme: root.colorScheme colorScheme: root.colorScheme
backend: root.backend
notifications: root.notifications
user: {
if (accounts.currentIndex < 0) return root.noUser
if (root.backend.users.count == 0) return root.noUser
return root.backend.users.get(accounts.currentIndex)
}
onShowSignIn: {
signIn.username = this.user.username
rightContent.showSignIn()
}
onShowSetupGuide: {
root.showSetupGuide(user,address)
}
}
GridLayout { // 1
columns: 2
Button {
id: backButton
Layout.leftMargin: 18
Layout.topMargin: 10
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme
onClicked: rightContent.showAccount()
icon.source: "icons/ic-arrow-left.svg"
secondary: true
horizontalPadding: 8
} }
GridLayout {
SignIn { SignIn {
id: signIn
Layout.topMargin: 68 Layout.topMargin: 68
Layout.leftMargin: 80 Layout.leftMargin: 80 - backButton.width - 18
Layout.rightMargin: 80 Layout.rightMargin: 80
Layout.bottomMargin: 68 Layout.bottomMargin: 68
Layout.preferredWidth: 320 Layout.preferredWidth: 320
@ -214,21 +288,70 @@ Item {
Layout.fillHeight: true Layout.fillHeight: true
colorScheme: root.colorScheme colorScheme: root.colorScheme
user: (root.backend.users.count === 1 && root.backend.users.get(0).loggedIn === false) ? root.backend.users.get(0) : undefined
backend: root.backend backend: root.backend
onLogin : { root.login ( username , password ) } onLogin : { root.backend.login ( username , password ) }
onLogin2FA : { root.login2FA ( username , code ) } onLogin2FA : { root.backend.login2FA ( username , code ) }
onLogin2Password : { root.login2Password ( username , password ) } onLogin2Password : { root.backend.login2Password ( username , password ) }
onLoginAbort : { root.loginAbort ( username ) } onLoginAbort : { root.backend.loginAbort ( username ) }
} }
} }
GeneralSettings { // 2
colorScheme: root.colorScheme
backend: root.backend
notifications: root.notifications
}
PortSettings { // 3
colorScheme: root.colorScheme
backend: root.backend
}
SMTPSettings { // 4
colorScheme: root.colorScheme
backend: root.backend
}
LocalCacheSettings { // 5
colorScheme: root.colorScheme
backend: root.backend
notifications: root.notifications
}
HelpView { // 6
colorScheme: root.colorScheme
backend: root.backend
}
BugReportView { // 7
colorScheme: root.colorScheme
backend: root.backend
selectedAddress: {
if (accounts.currentIndex < 0) return ""
if (root.backend.users.count == 0) return ""
return root.backend.users.get(accounts.currentIndex).addresses[0]
}
}
function showAccount () { rightContent.currentIndex = 0 }
function showSignIn () { rightContent.currentIndex = 1 }
function showGeneralSettings () { rightContent.currentIndex = 2 }
function showPortSettings () { rightContent.currentIndex = 3 }
function showSMTPSettings () { rightContent.currentIndex = 4 }
function showLocalCacheSettings () { rightContent.currentIndex = 5 }
function showHelpView () { rightContent.currentIndex = 6 }
function showBugReport () { rightContent.currentIndex = 7 }
} }
} }
} }
function showLocalCacheSettings(){rightContent.showLocalCacheSettings() }
function showSettings(){rightContent.showGeneralSettings() }
function showHelp(){rightContent.showHelpView() }
function showSignIn(username){
signIn.username = username
rightContent.showSignIn()
}
function showSignIn() {
rightContent.currentIndex = 1
}
} }

View File

@ -0,0 +1,168 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import QtQuick.Controls.impl 2.13
import Proton 4.0
SettingsView {
id: root
property bool _isAdvancedShown: false
property var notifications
Label {
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: root.backend.isAutomaticUpdateOn
onClicked: root.backend.toggleAutomaticUpdate(!autoUpdate.checked)
}
SettingsItem {
id: autostart
colorScheme: root.colorScheme
text: qsTr("Automatically start Bridge")
description: qsTr("The app will autostart everytime you reset your device.")
type: SettingsItem.Toggle
checked: root.backend.isAutostartOn
onClicked: {
autostart.loading = true
root.backend.toggleAutostart(!autoUpdate.checked)
}
Connections{
target: root.backend
onToggleAutostartFinished: {
autostart.loading = false
}
}
}
SettingsItem {
id: beta
colorScheme: root.colorScheme
text: qsTr("Enable Beta access")
description: qsTr("Be the first one to see new features.")
type: SettingsItem.Toggle
checked: root.backend.isBetaEnabled
onClicked: {
if (!beta.checked) {
root.notifications.askEnableBeta()
} else {
root.notifications.askDisableBeta()
}
}
}
RowLayout {
ColorImage {
Layout.alignment: Qt.AlignTop
source: root._isAdvancedShown ? "icons/ic-chevron-up.svg" : "icons/ic-chevron-down.svg"
color: root.colorScheme.interaction_norm
height: root.colorScheme.body_font_size
MouseArea {
anchors.fill: parent
onClicked: root._isAdvancedShown = !root._isAdvancedShown
}
}
Label {
id: advSettLabel
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: doh
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Alternative routing")
description: qsTr("If Protons servers are blocked in your location, alternative network routing will be used to reach Proton.")
type: SettingsItem.Toggle
checked: root.backend.isDoHEnabled
onClicked: root.backend.toggleDoH(!doh.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()
}
SettingsItem {
id: smtp
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("SMTP connection mode")
actionText: qsTr("Change")
description: qsTr("Change the protocol Bridge and your client use to connect.")
type: SettingsItem.Button
onClicked: root.parent.showSMTPSettings()
}
SettingsItem {
id: cache
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Local cache")
actionText: qsTr("Configure")
description: qsTr("Configure Bridge's local cache settings.")
type: SettingsItem.Button
onClicked: root.parent.showLocalCacheSettings()
}
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()
}
}
onBack: root.parent.showAccount()
}

View File

@ -0,0 +1,110 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import Proton 4.0
SettingsView {
id: root
Label {
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")
actionIcon: "./icons/ic-external-link.svg"
description: qsTr("Get help setting up your client with our instructions and FAQs.")
type: SettingsItem.PrimaryButton
onClicked: {Qt.openUrlExternally("https://protonmail.com/bridge/install")}
}
SettingsItem {
id: checkUpdates
colorScheme: root.colorScheme
text: qsTr("Updates")
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.")
type: SettingsItem.Button
onClicked: {
checkUpdates.loading = true
root.backend.checkUpdates()
}
Connections {target: root.backend; onCheckUpdatesFinished: checkUpdates.loading = false}
}
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(root.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: {
root.backend.updateCurrentMailClient()
root.parent.showBugReport()
}
}
Label {
Layout.alignment: Qt.AlignHCenter
colorScheme: root.colorScheme
type: Label.Caption
color: root.colorScheme.text_weak
textFormat: Text.RichText
linkColor: root.colorScheme.interaction_norm_active
text: {
var version = root.backend.version
var license = qsTr("License")
var licensePath = root.backend.licensePath
var release= qsTr("Release notes")
var releaseNotesLink = root.backend.releaseNotesLink
return `<p style="text-align:center;">Proton Mail Bridge v${version}<br>
© 2021 Proton Technologies AG<br>
<a style="color: ${linkColor};" href="${licensePath}">${license}</a>
<a style="color: ${linkColor};" href="${releaseNotesLink}">${release}</a>
</p>`
}
onLinkActivated: Qt.openUrlExternally(link)
}
onBack: {
root.parent.showAccount()
}
}

View File

@ -0,0 +1,146 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import QtQuick.Controls.impl 2.13
import QtQuick.Dialogs 1.1
import Proton 4.0
SettingsView {
id: root
property var notifications
property bool _diskCacheEnabled: true
property string _diskCachePath: "/home"
Label {
colorScheme: root.colorScheme
text: qsTr("Local cache")
type: Label.Heading
Layout.fillWidth: true
}
Label {
colorScheme: root.colorScheme
text: qsTr("Bridge caches your encrypted messages localy to optimise the communication with the local client. Disabling this feature might have a nevative impact on performance.")
type: Label.Body
color: root.colorScheme.text_weak
Layout.fillWidth: true
Layout.maximumWidth: this.parent.Layout.maximumWidth
wrapMode: Text.WordWrap
}
SettingsItem {
colorScheme: root.colorScheme
text: qsTr("Enable local cache")
description: "When enabled messages are stored on disk." // TODO: wrong text in wireframe
type: SettingsItem.Toggle
checked: root._diskCacheEnabled
onClicked: root._diskCacheEnabled = !root._diskCacheEnabled
}
SettingsItem {
colorScheme: root.colorScheme
text: qsTr("Current cache location")
actionText: qsTr("Change location")
description: root._diskCachePath
type: SettingsItem.Button
enabled: root._diskCacheEnabled
onClicked: {
pathDialog.open()
}
FileDialog {
id: pathDialog
title: qsTr("Select cache location")
folder: shortcuts.home
onAccepted: root.sanitizePath(pathDialog.fileUrl.toString())
selectFolder: true
}
}
RowLayout {
spacing: 12
Button {
id: submitButton
colorScheme: root.colorScheme
text: qsTr("Save and restart")
enabled: (
root.backend.diskCachePath != root._diskCachePath ||
root.backend.isDiskCacheEnabled != root._diskCacheEnabled
)
onClicked: {
root.submit()
}
}
Button {
colorScheme: root.colorScheme
text: qsTr("Cancel")
onClicked: root.back()
secondary: true
}
Connections {
target: root.backend
onChangeLocalCacheFinished: {
submitButton.loading = false
root.setDefaultValues()
}
}
}
onBack: {
root.parent.showGeneralSettings()
root.setDefaultValues()
}
function submit(){
console.log("submit")
if (!root._diskCacheEnabled && root.backend.isDiskCacheEnabled) {
root.notifications.askDisableLocalCache()
return
}
if (root._diskCacheEnabled && !root.backend.isDiskCacheEnabled) {
root.notifications.askEnableLocalCache(root._diskCachePath)
return
}
// Not asking, only changing path
submitButton.loading = true
root.backend.changeLocalCache(root.backend.isDiskCacheEnabled, root._diskCachePath)
}
function setDefaultValues(){
root._diskCacheEnabled = root.backend.isDiskCacheEnabled
root._diskCachePath = root.backend.diskCachePath
}
function sanitizePath(path) {
var pattern = "file://"
if (root.backend.goos=="windows") pattern+="/"
root._diskCachePath = path.replace(pattern, "")
}
Component.onCompleted: root.setDefaultValues()
}

View File

@ -62,7 +62,7 @@ ApplicationWindow {
return return
} }
root.showSetup(user) root.showSetup(user,user.addresses[0])
} }
onRowsAboutToBeRemoved: { onRowsAboutToBeRemoved: {
@ -78,15 +78,6 @@ ApplicationWindow {
} }
} }
function showSetup(user) {
setupGuide.user = user
if (setupGuide.user) {
contentLayout._showSetup = true
} else {
contentLayout._showSetup = false
}
}
StackLayout { StackLayout {
id: contentLayout id: contentLayout
@ -111,12 +102,18 @@ ApplicationWindow {
} }
ContentWrapper { ContentWrapper {
id: contentWrapper
colorScheme: root.colorScheme colorScheme: root.colorScheme
backend: root.backend backend: root.backend
notifications: root.notifications
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
onShowSetupGuide: {
root.showSetup(user,address)
}
onLogin: { onLogin: {
root.login(username, password) root.login(username, password)
} }
@ -161,7 +158,7 @@ ApplicationWindow {
Layout.fillWidth: true Layout.fillWidth: true
onDismissed: { onDismissed: {
root.showSetup(null) root.showSetup(null,"")
} }
} }
} }
@ -169,5 +166,25 @@ ApplicationWindow {
NotificationPopups { NotificationPopups {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notifications: root.notifications notifications: root.notifications
mainWindow: root
}
function showLocalCacheSettings() { contentWrapper.showLocalCacheSettings() }
function showSettings() { contentWrapper.showSettings() }
function showHelp() { contentWrapper.showHelp() }
function showSignIn(username) {
if (contentLayout.currentIndex == 1) return
contentWrapper.showSignIn(username)
}
function showSetup(user, address) {
setupGuide.user = user
setupGuide.address = address
if (setupGuide.user) {
contentLayout._showSetup = true
} else {
contentLayout._showSetup = false
}
} }
} }

View File

@ -56,8 +56,7 @@ Dialog {
switch (root.notification.type) { switch (root.notification.type) {
case Notification.NotificationType.Info: case Notification.NotificationType.Info:
// TODO: Add info icon? return "./icons/ic-info.svg"
return ""
case Notification.NotificationType.Success: case Notification.NotificationType.Success:
return "./icons/ic-success.svg" return "./icons/ic-success.svg"
case Notification.NotificationType.Warning: case Notification.NotificationType.Warning:
@ -110,6 +109,8 @@ Dialog {
action: modelData action: modelData
secondary: index > 0 secondary: index > 0
loading: notification.loading
} }
} }
} }

View File

@ -28,6 +28,7 @@ Item {
property ColorScheme colorScheme property ColorScheme colorScheme
property var notifications property var notifications
property var mainWindow
property int notificationWhitelist: NotificationFilter.FilterConsts.All property int notificationWhitelist: NotificationFilter.FilterConsts.All
property int notificationBlacklist: NotificationFilter.FilterConsts.None property int notificationBlacklist: NotificationFilter.FilterConsts.None
@ -42,6 +43,7 @@ Item {
Banner { Banner {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: bannerNotificationFilter.topmost notification: bannerNotificationFilter.topmost
mainWindow: root.mainWindow
} }
NotificationDialog { NotificationDialog {
@ -66,17 +68,17 @@ Item {
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.bugReportSendSuccess notification: root.notifications.disableBeta
} }
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.bugReportSendError notification: root.notifications.enableBeta
} }
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.cacheAnavailable notification: root.notifications.cacheUnavailable
} }
NotificationDialog { NotificationDialog {
@ -88,4 +90,24 @@ Item {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.diskFull notification: root.notifications.diskFull
} }
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.enableSplitMode
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.disableLocalCache
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.enableLocalCache
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.resetBridge
}
} }

View File

@ -39,6 +39,7 @@ QtObject {
property bool dismissed: false property bool dismissed: false
property bool active: false property bool active: false
property bool loading: false
readonly property var occurred: active ? new Date() : undefined readonly property var occurred: active ? new Date() : undefined
property var data property var data

View File

@ -29,6 +29,13 @@ QtObject {
property StatusWindow frontendStatus property StatusWindow frontendStatus
property SystemTrayIcon frontendTray property SystemTrayIcon frontendTray
signal askDisableBeta()
signal askEnableBeta()
signal askEnableSplitMode(var user)
signal askDisableLocalCache()
signal askEnableLocalCache(var path)
signal askResetBridge()
enum Group { enum Group {
Connection = 1, Connection = 1,
Update = 2, Update = 2,
@ -48,12 +55,20 @@ QtObject {
root.updateForceError, root.updateForceError,
root.updateSilentRestartNeeded, root.updateSilentRestartNeeded,
root.updateSilentError, root.updateSilentError,
root.updateIsLatestVersion,
root.disableBeta,
root.enableBeta,
root.bugReportSendSuccess, root.bugReportSendSuccess,
root.bugReportSendError, root.bugReportSendError,
root.cacheAnavailable, root.cacheUnavailable,
root.cacheCantMove, root.cacheCantMove,
root.accountChanged, root.accountChanged,
root.diskFull root.diskFull,
root.cacheLocationChangeSuccess,
root.enableSplitMode,
root.disableLocalCache,
root.enableLocalCache,
root.resetBridge
] ]
// Connection // Connection
@ -93,10 +108,18 @@ QtObject {
action: [ action: [
Action { Action {
text: qsTr("Update") text: qsTr("Install update")
onTriggered: { onTriggered: {
// TODO: call update from backend root.backend.installUpdate()
root.updateManualReady.active = false
}
},
Action {
text: qsTr("Update manually")
onTriggered: {
Qt.openUrlExternally(root.backend.getLandingPage())
root.updateManualReady.active = false root.updateManualReady.active = false
} }
}, },
@ -104,7 +127,6 @@ QtObject {
text: qsTr("Remind me later") text: qsTr("Remind me later")
onTriggered: { onTriggered: {
// TODO: start timer here
root.updateManualReady.active = false root.updateManualReady.active = false
} }
} }
@ -128,14 +150,14 @@ QtObject {
text: qsTr("Restart Bridge") text: qsTr("Restart Bridge")
onTriggered: { onTriggered: {
// TODO root.backend.restart()
root.updateManualRestartNeeded.active = false root.updateManualRestartNeeded.active = false
} }
} }
} }
property Notification updateManualError: Notification { property Notification updateManualError: Notification {
text: qsTr("Bridge couldnt update") text: qsTr("Bridge couldnt update. Please update manually.")
icon: "./icons/ic-exclamation-circle-filled.svg" icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Warning type: Notification.NotificationType.Warning
group: Notifications.Group.Update group: Notifications.Group.Update
@ -147,19 +169,28 @@ QtObject {
} }
} }
action: Action { action: [
Action {
text: qsTr("Update manually") text: qsTr("Update manually")
onTriggered: { onTriggered: {
// TODO Qt.openUrlExternally(root.backend.getLandingPage())
root.updateManualError.active = false root.updateManualError.active = false
} }
},
Action {
text: qsTr("Remind me later")
onTriggered: {
root.updateManualReady.active = false
} }
} }
]
}
property Notification updateForce: Notification { property Notification updateForce: Notification {
text: qsTr("Update to ProtonMail Bridge") + " " + (data ? data.version : "") text: qsTr("Update to ProtonMail Bridge") + " " + (data ? data.version : "")
description: qsTr("This version of Bridge is no longer supported, please update. Learn why. To update manually, go to: https:/protonmail.com/bridge/download") description: qsTr("This version of Bridge is no longer supported, please update.")
icon: "./icons/ic-exclamation-circle-filled.svg" icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger type: Notification.NotificationType.Danger
group: Notifications.Group.Update | Notifications.Group.Dialogs group: Notifications.Group.Update | Notifications.Group.Dialogs
@ -175,18 +206,26 @@ QtObject {
action: [ action: [
Action { Action {
text: qsTr("Update") text: qsTr("Install update")
onTriggered: { onTriggered: {
// TODO: trigger update here root.backend.installUpdate()
root.updateForce.active = false root.updateForce.active = false
} }
}, },
Action { Action {
text: qsTr("Quite Bridge") text: qsTr("Update manually")
onTriggered: { onTriggered: {
// TODO: quit Bridge here Qt.openUrlExternally(root.backend.getLandingPage())
root.updateForce.active = false
}
},
Action {
text: qsTr("Quit Bridge")
onTriggered: {
root.backend.quit()
root.updateForce.active = false root.updateForce.active = false
} }
} }
@ -195,7 +234,7 @@ QtObject {
property Notification updateForceError: Notification { property Notification updateForceError: Notification {
text: qsTr("Bridge coudnt update") text: qsTr("Bridge coudnt update")
description: qsTr("You must update manually. Go to: https:/protonmail.com/bridge/download") description: qsTr("You must update manually.")
icon: "./icons/ic-exclamation-circle-filled.svg" icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger type: Notification.NotificationType.Danger
group: Notifications.Group.Update | Notifications.Group.Dialogs group: Notifications.Group.Update | Notifications.Group.Dialogs
@ -213,15 +252,15 @@ QtObject {
text: qsTr("Update manually") text: qsTr("Update manually")
onTriggered: { onTriggered: {
// TODO: trigger update here Qt.openUrlExternally(root.backend.getLandingPage())
root.updateForceError.active = false root.updateForceError.active = false
} }
}, },
Action { Action {
text: qsTr("Quite Bridge") text: qsTr("Quit Bridge")
onTriggered: { onTriggered: {
// TODO: quit Bridge here root.backend.quit()
root.updateForce.active = false root.updateForce.active = false
} }
} }
@ -245,7 +284,7 @@ QtObject {
text: qsTr("Restart Bridge") text: qsTr("Restart Bridge")
onTriggered: { onTriggered: {
// TODO root.backend.restart()
root.updateSilentRestartNeeded.active = false root.updateSilentRestartNeeded.active = false
} }
} }
@ -268,18 +307,105 @@ QtObject {
text: qsTr("Update manually") text: qsTr("Update manually")
onTriggered: { onTriggered: {
// TODO Qt.openUrlExternally(root.backend.getLandingPage())
root.updateSilentError.active = false root.updateSilentError.active = false
} }
} }
} }
property Notification updateIsLatestVersion: Notification {
text: qsTr("Bridge is up to date")
icon: "./icons/ic-info-circle-filled.svg"
type: Notification.NotificationType.Info
group: Notifications.Group.Update
Connections {
target: root.backend
onUpdateIsLatestVersion: {
root.updateIsLatestVersion.active = true
}
}
action: Action {
text: qsTr("Ok")
onTriggered: {
root.updateIsLatestVersion.active = false
}
}
}
property Notification disableBeta: Notification {
text: qsTr("Disable beta access?")
description: qsTr("This resets Bridge to the current release and will restart the app. Your preferences, cached data, and email client configurations will be cleared. ")
icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Warning
group: Notifications.Group.Update | Notifications.Group.Dialogs
Connections {
target: root
onAskDisableBeta: {
root.disableBeta.active = true
}
}
action: [
Action {
text: qsTr("Remind me later")
onTriggered: {
root.disableBeta.active = false
}
},
Action {
text: qsTr("Disable and restart")
onTriggered: {
root.backend.toggleBeta(false)
root.disableBeta.loading = true
}
}
]
}
property Notification enableBeta: Notification {
text: qsTr("Enable beta access?")
description: qsTr("Bridge will update to the latest beta version according to your update preferences. Disabling beta access later on will reset Bridge and require you to reconfigure your client.")
icon: "./icons/ic-info-circle-filled.svg"
type: Notification.NotificationType.Info
group: Notifications.Group.Update | Notifications.Group.Dialogs
Connections {
target: root
onAskEnableBeta: {
root.enableBeta.active = true
}
}
action: [
Action {
text: qsTr("Enable")
onTriggered: {
root.backend.toggleBeta(true)
root.enableBeta.active = false
}
},
Action {
text: qsTr("Cancel")
onTriggered: {
root.enableBeta.active = false
}
}
]
}
// Bug reports // Bug reports
property Notification bugReportSendSuccess: Notification { property Notification bugReportSendSuccess: Notification {
text: qsTr("Bug report sent") text: qsTr("Thank you for the report. We'll get back to you as soon as we can.")
description: qsTr("Weve received your report, thank you! Our team will get back to you as soon as we can.") icon: "./icons/ic-info-circle-filled.svg"
type: Notification.NotificationType.Success type: Notification.NotificationType.Success
group: Notifications.Group.Configuration | Notifications.Group.Dialogs group: Notifications.Group.Configuration
Connections { Connections {
target: root.backend target: root.backend
@ -302,10 +428,10 @@ QtObject {
} }
property Notification bugReportSendError: Notification { property Notification bugReportSendError: Notification {
text: qsTr("There was a problem") text: qsTr("Report could not be sent. Try again or email us directly.")
description: qsTr("There was a problem with sending your report. Please try again later or contact us directly at security@protonmail.com") icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Warning type: Notification.NotificationType.Danger
group: Notifications.Group.Configuration | Notifications.Group.Dialogs group: Notifications.Group.Configuration
Connections { Connections {
target: root.backend target: root.backend
@ -323,7 +449,7 @@ QtObject {
} }
// Cache // Cache
property Notification cacheAnavailable: Notification { property Notification cacheUnavailable: Notification {
text: qsTr("Cache location is unavailable") text: qsTr("Cache location is unavailable")
description: qsTr("Check the directory or change it in your settings.") description: qsTr("Check the directory or change it in your settings.")
type: Notification.NotificationType.Warning type: Notification.NotificationType.Warning
@ -331,8 +457,8 @@ QtObject {
Connections { Connections {
target: root.backend target: root.backend
onCacheAnavailable: { onCacheUnavailable: {
root.cacheAnavailable.active = true root.cacheUnavailable.active = true
} }
} }
@ -340,13 +466,15 @@ QtObject {
Action { Action {
text: qsTr("Quit Bridge") text: qsTr("Quit Bridge")
onTriggered: { onTriggered: {
root.cacheAnavailable.active = false root.backend.quit()
root.cacheUnavailable.active = false
} }
}, },
Action { Action {
text: qsTr("Change location") text: qsTr("Change location")
onTriggered: { onTriggered: {
root.cacheAnavailable.active = false root.cacheUnavailable.active = false
root.frontendMain.showLocalCacheSettings()
} }
} }
] ]
@ -376,6 +504,31 @@ QtObject {
text: qsTr("Change location") text: qsTr("Change location")
onTriggered: { onTriggered: {
root.cacheCantMove.active = false root.cacheCantMove.active = false
root.frontendMain.showLocalCacheSettings()
}
}
]
}
property Notification cacheLocationChangeSuccess: Notification {
text: qsTr("Cache location successfully changed")
icon: "./icons/ic-info-circle-filled.svg"
type: Notification.NotificationType.Success
group: Notifications.Group.Configuration
Connections {
target: root.backend
onCacheLocationChangeSuccess: {
console.log("notify location changed succesfully")
root.cacheLocationChangeSuccess.active = true
}
}
action: [
Action {
text: qsTr("Ok")
onTriggered: {
root.cacheLocationChangeSuccess.active = false
} }
} }
] ]
@ -414,6 +567,7 @@ QtObject {
Action { Action {
text: qsTr("Quit Bridge") text: qsTr("Quit Bridge")
onTriggered: { onTriggered: {
root.backend.quit()
root.diskFull.active = false root.diskFull.active = false
} }
}, },
@ -421,6 +575,171 @@ QtObject {
text: qsTr("Settings") text: qsTr("Settings")
onTriggered: { onTriggered: {
root.diskFull.active = false root.diskFull.active = false
root.frontendMain.showLocalCacheSettings()
}
}
]
}
property Notification enableSplitMode: Notification {
text: qsTr("Enable split mode?")
description: qsTr("Changing between split and combined address mode will require you to delete your accounts(s) from your email client and begin the setup process from scratch.")
type: Notification.NotificationType.Warning
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
property var user
Connections {
target: root
onAskEnableSplitMode: {
root.enableSplitMode.user = user
root.enableSplitMode.active = true
}
}
Connections {
target: (root && root.enableSplitMode && root.enableSplitMode.user ) ? root.enableSplitMode.user : null
onToggleSplitModeFinished: {
root.enableSplitMode.active = false
root.enableSplitMode.loading = false
}
}
action: [
Action {
text: qsTr("Cancel")
onTriggered: {
root.enableSplitMode.active = false
}
},
Action {
text: qsTr("Enable split mode")
onTriggered: {
root.enableSplitMode.loading = true
root.enableSplitMode.user.toggleSplitMode(true)
}
}
]
}
property Notification disableLocalCache: Notification {
text: qsTr("Disable local cache?")
description: qsTr("This action will clear your local cache, including locally stored messages. Bridge will restart.")
type: Notification.NotificationType.Warning
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
Connections {
target: root
onAskDisableLocalCache: {
root.disableLocalCache.active = true
}
}
Connections {
target: root.backend
onChangeLocalCacheFinished: {
root.disableLocalCache.active = false
root.disableLocalCache.loading = false
}
}
action: [
Action {
text: qsTr("Cancel")
onTriggered: {
root.disableLocalCache.active = false
}
},
Action {
text: qsTr("Disable and restart")
onTriggered: {
root.disableLocalCache.loading = true
root.backend.changeLocalCache(false, root.backend.diskCachePath)
}
}
]
}
property Notification enableLocalCache: Notification {
text: qsTr("Enable local cache?")
description: qsTr("Bridge will restart.")
type: Notification.NotificationType.Warning
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
property var path
Connections {
target: root
onAskEnableLocalCache: {
root.enableLocalCache.active = true
root.enableLocalCache.path = path
}
}
Connections {
target: root.backend
onChangeLocalCacheFinished: {
root.enableLocalCache.active = false
root.enableLocalCache.loading = false
}
}
action: [
Action {
text: qsTr("Enable and restart")
onTriggered: {
root.enableLocalCache.loading = true
root.backend.changeLocalCache(true, root.enableLocalCache.path)
}
},
Action {
text: qsTr("Cancel")
onTriggered: {
root.enableLocalCache.active = false
}
}
]
}
property Notification resetBridge: Notification {
text: qsTr("Reset Bridge?")
description: qsTr("This will clear your accounts, preferences, and cached data. You will need to reconfigure your email client. Bridge will automatically restart")
type: Notification.NotificationType.Warning
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
property var user
Connections {
target: root
onAskResetBridge: {
root.resetBridge.active = true
}
}
Connections {
target: root.backend
onResetFinished: {
root.resetBridge.active = false
root.resetBridge.loading = false
}
}
action: [
Action {
text: qsTr("Cancel")
onTriggered: {
root.resetBridge.active = false
}
},
Action {
text: qsTr("Reset and restart")
onTriggered: {
root.resetBridge.loading = true
root.backend.triggerReset()
} }
} }
] ]

View File

@ -0,0 +1,154 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import QtQuick.Controls.impl 2.13
import Proton 4.0
SettingsView {
id: root
property bool _valuesOK: !imapField.error && !smtpField.error
property bool _valuesChanged: (
imapField.text*1 != root.backend.portIMAP ||
smtpField.text*1 != root.backend.portSMTP
)
Label {
colorScheme: root.colorScheme
text: qsTr("Default ports")
type: Label.Heading
Layout.fillWidth: true
}
Label {
colorScheme: root.colorScheme
text: qsTr("Changes require reconfiguration of your email client. Bridge will automatically restart.")
type: Label.Body
color: root.colorScheme.text_weak
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
RowLayout {
spacing: 16
TextField {
id: imapField
colorScheme: root.colorScheme
label: qsTr("IMAP port")
Layout.preferredWidth: 160
onEditingFinished: root.validate(imapField)
}
TextField {
id: smtpField
colorScheme: root.colorScheme
label: qsTr("SMTP port")
Layout.preferredWidth: 160
onEditingFinished: root.validate(smtpField)
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: root.colorScheme.border_weak
}
RowLayout {
spacing: 12
Button {
id: submitButton
colorScheme: root.colorScheme
text: qsTr("Save and restart")
enabled: root._valuesOK && root._valuesChanged
onClicked: {
submitButton.loading = true
root.submit()
}
}
Button {
colorScheme: root.colorScheme
text: qsTr("Cancel")
onClicked: root.back()
secondary: true
}
Connections {
target: root.backend
onChangePortFinished: submitButton.loading = false
}
}
onBack: {
root.parent.showGeneralSettings()
root.setDefaultValues()
}
function validate(field) {
var num = field.text*1
if (! (num > 1 && num < 65536) ) {
field.error = true
field.assistiveText = qsTr("Invalid port number.")
return
}
if (imapField.text == smtpField.text) {
field.error = true
field.assistiveText = qsTr("Port numbers must be different.")
return
}
field.error = false
field.assistiveText = ""
}
function isPortFree(field) {
field.error = false
field.assistiveText = ""
var num = field.text*1
if (num == root.backend.portIMAP) return true
if (num == root.backend.portSMTP) return true
if (!root.backend.isPortFree(num)) {
field.error = true
field.assistiveText = qsTr("Port occupied.")
submitButton.loading = false
return false
}
}
function submit(){
submitButton.loading = true
if (!isPortFree(imapField)) return
if (!isPortFree(smtpField)) return
root.backend.changePorts(imapField.text, smtpField.text)
}
function setDefaultValues(){
imapField.text = backend.portIMAP
smtpField.text = backend.portSMTP
}
Component.onCompleted: root.setDefaultValues()
}

View File

@ -246,9 +246,25 @@ T.Button {
} }
} }
border.color: control.colorScheme.border_norm border.color: {
return control.colorScheme.border_norm
}
border.width: secondary && !borderless ? 1 : 0 border.width: secondary && !borderless ? 1 : 0
opacity: control.enabled || control.loading ? 1.0 : 0.5 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.error("ColorScheme not defined")
}
}
} }

View File

@ -21,7 +21,7 @@ import QtQuick 2.8
Rectangle { Rectangle {
id: root id: root
color: Style.transparent color: "transparent"
property color fillColor : Style.currentStyle.background_norm property color fillColor : Style.currentStyle.background_norm
property color strokeColor : Style.currentStyle.background_strong property color strokeColor : Style.currentStyle.background_strong

View File

@ -20,6 +20,7 @@ import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import QtQuick.Controls.impl 2.12 import QtQuick.Controls.impl 2.12
import QtQuick.Templates 2.12 as T import QtQuick.Templates 2.12 as T
import "."
Item { Item {
id: root id: root
@ -86,9 +87,10 @@ Item {
property alias wrapMode: control.wrapMode property alias wrapMode: control.wrapMode
implicitWidth: background.width implicitWidth: background.width
implicitHeight: control.implicitHeight + implicitHeight: control.implicitHeight + Math.max(
Math.max(label.implicitHeight + label.anchors.topMargin + label.anchors.bottomMargin, hint.implicitHeight + hint.anchors.topMargin + hint.anchors.bottomMargin) + label.implicitHeight + label.anchors.topMargin + label.anchors.bottomMargin,
assistiveText.implicitHeight hint.implicitHeight + hint.anchors.topMargin + hint.anchors.bottomMargin
) + assistiveText.implicitHeight
property alias label: label.text property alias label: label.text
property alias hint: hint.text property alias hint: hint.text
@ -96,6 +98,8 @@ Item {
property bool error: false property bool error: false
signal editingFinished()
// Backgroud is moved away from within control as it will be clipped with scrollview // Backgroud is moved away from within control as it will be clipped with scrollview
Rectangle { Rectangle {
id: background id: background
@ -200,12 +204,16 @@ Item {
T.TextArea { T.TextArea {
id: control id: control
implicitWidth: Math.max(contentWidth + leftPadding + rightPadding, implicitWidth: Math.max(
contentWidth + leftPadding + rightPadding,
implicitBackgroundWidth + leftInset + rightInset, implicitBackgroundWidth + leftInset + rightInset,
placeholder.implicitWidth + leftPadding + rightPadding) placeholder.implicitWidth + leftPadding + rightPadding
implicitHeight: Math.max(contentHeight + topPadding + bottomPadding, )
implicitHeight: Math.max(
contentHeight + topPadding + bottomPadding,
implicitBackgroundHeight + topInset + bottomInset, implicitBackgroundHeight + topInset + bottomInset,
placeholder.implicitHeight + topPadding + bottomPadding) placeholder.implicitHeight + topPadding + bottomPadding
)
padding: 8 padding: 8
leftPadding: 12 leftPadding: 12
@ -216,6 +224,8 @@ Item {
selectionColor: control.palette.highlight selectionColor: control.palette.highlight
selectedTextColor: control.palette.highlightedText selectedTextColor: control.palette.highlightedText
onEditingFinished: root.editingFinished()
cursorDelegate: Rectangle { cursorDelegate: Rectangle {
id: cursor id: cursor
width: 1 width: 1

View File

@ -0,0 +1,107 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import QtQuick.Controls.impl 2.13
RowLayout{
id: root
property var colorScheme
property bool checked
property bool disabled
property bool hovered
property bool loading
signal clicked
Rectangle {
id: indicator
implicitWidth: 40
implicitHeight: 24
radius: 20
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
}
Rectangle {
anchors.verticalCenter: indicator.verticalCenter
anchors.left: indicator.left
anchors.leftMargin: root.checked ? 16 : 0
width: 24
height: 24
radius: 12
color: {
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
} else {
if (root.hovered) return root.colorScheme.field_hover
return root.colorScheme.field_norm
}
}
ColorImage {
anchors.centerIn: parent
source: "../icons/ic-check.svg"
color: root.colorScheme.background_norm
height: root.colorScheme.body_font_size
visible: root.checked
}
}
ColorImage {
id: loader
anchors.centerIn: parent
source: "../icons/Loader_16.svg"
color: root.colorScheme.text_norm
height: root.colorScheme.body_font_size
visible: root.loading
RotationAnimation {
target: loader
loops: Animation.Infinite
duration: 1000
from: 0
to: 360
direction: RotationAnimation.Clockwise
running: root.loading
}
}
MouseArea {
anchors.fill: indicator
hoverEnabled: true
onEntered: {root.hovered = true }
onExited: {root.hovered = false }
onClicked: { root.clicked();}
onPressed: {root.hovered = true }
onReleased: { root.hovered = containsMouse }
}
}
}

View File

@ -34,3 +34,4 @@ RoundedRectangle 4.0 RoundedRectangle.qml
Switch 4.0 Switch.qml Switch 4.0 Switch.qml
TextArea 4.0 TextArea.qml TextArea 4.0 TextArea.qml
TextField 4.0 TextField.qml TextField 4.0 TextField.qml
Toggle 4.0 Toggle.qml

View File

@ -0,0 +1,120 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import QtQuick.Controls.impl 2.13
import Proton 4.0
SettingsView {
id: root
Label {
colorScheme: root.colorScheme
text: qsTr("SMTP connection mode")
type: Label.Heading
Layout.fillWidth: true
}
Label {
colorScheme: root.colorScheme
text: qsTr("Changes require reconfiguration of email client. Bridge will automatically restart.")
type: Label.Body
color: root.colorScheme.text_weak
Layout.fillWidth: true
Layout.maximumWidth: this.parent.Layout.maximumWidth
wrapMode: Text.WordWrap
}
ColumnLayout {
spacing: 16
ButtonGroup{ id: protocolSelection }
Label {
colorScheme: root.colorScheme
text: qsTr("SMTP connection security")
}
RadioButton {
id: sslButton
colorScheme: root.colorScheme
ButtonGroup.group: protocolSelection
text: qsTr("SSL")
}
RadioButton {
id: starttlsButton
colorScheme: root.colorScheme
ButtonGroup.group: protocolSelection
text: qsTr("STARTLS")
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: root.colorScheme.border_weak
}
RowLayout {
spacing: 12
Button {
id: submitButton
colorScheme: root.colorScheme
text: qsTr("Save and restart")
onClicked: {
submitButton.loading = true
root.submit()
}
}
Button {
colorScheme: root.colorScheme
text: qsTr("Cancel")
onClicked: root.back()
secondary: true
}
Connections {
target: root.backend
onToggleUseSSLFinished: submitButton.loading = false
}
}
onBack: {
root.parent.showGeneralSettings()
root.setDefaultValues()
}
function submit(){
submitButton.loading = true
root.backend.toggleUseSSLforSMTP(sslButton.checked)
}
function setDefaultValues(){
sslButton.checked = root.backend.useSSLforSMTP
starttlsButton.checked = !root.backend.useSSLforSMTP
}
Component.onCompleted: root.setDefaultValues()
}

View File

@ -0,0 +1,105 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import Proton 4.0
ColumnLayout {
id: root
property var colorScheme
property string text: "Text"
property string actionText: "Action"
property string actionIcon: ""
property string description: "Lorem ipsum dolor sit amet"
property var type: SettingsItem.ActionType.Toggle
property bool checked: true
property bool disabled: false
property bool loading: false
signal clicked
spacing: 20
Layout.fillWidth: true
Layout.maximumWidth: root.parent.Layout.maximumWidth
enum ActionType {
Toggle = 1, Button = 2, PrimaryButton = 3
}
RowLayout {
Layout.fillWidth: true
ColumnLayout {
Label {
id:mainLabel
colorScheme: root.colorScheme
text: root.text
type: Label.Body_semibold
}
Label {
Layout.minimumWidth: mainLabel.width
Layout.maximumWidth: root.Layout.maximumWidth - root.spacing - (
toggle.visible ? toggle.width : button.width
)
wrapMode: Text.WordWrap
colorScheme: root.colorScheme
text: root.description
color: root.colorScheme.text_weak
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
Toggle {
id: toggle
colorScheme: root.colorScheme
visible: root.type == SettingsItem.ActionType.Toggle
checked: root.checked
loading: root.loading
onClicked: { if (!root.loading) root.clicked() }
}
Button {
id: button
colorScheme: root.colorScheme
visible: root.type == SettingsItem.Button || root.type == SettingsItem.PrimaryButton
text: root.actionText + (root.actionIcon != "" ? " " : "")
loading: root.loading
icon.source: root.actionIcon
onClicked: { if (!root.loading) root.clicked() }
secondary: root.type != SettingsItem.PrimaryButton
}
}
Rectangle {
Layout.fillWidth: true
color: colorScheme.border_weak
height: 1
}
}

View File

@ -0,0 +1,71 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick 2.13
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import QtQuick.Controls.impl 2.13
import Proton 4.0
ScrollView {
id: root
property var colorScheme
property var backend
default property alias items: content.children
signal back()
property int _leftRightMargins: 64
property int _topBottomMargins: 68
property int _spacing: 22
clip: true
contentWidth: pane.width
contentHeight: pane.height
RowLayout{
id: pane
width: root.width
ColumnLayout {
id: content
spacing: root._spacing
Layout.maximumWidth: root.width - 2*root._leftRightMargins
Layout.fillWidth: true
Layout.topMargin: root._topBottomMargins
Layout.bottomMargin: root._topBottomMargins
Layout.leftMargin: root._leftRightMargins
Layout.rightMargin: root._leftRightMargins
}
}
Button {
anchors {
top: parent.top
left: parent.left
topMargin: 10
leftMargin: 18
}
colorScheme: root.colorScheme
onClicked: root.back()
icon.source: "icons/ic-arrow-left.svg"
secondary: true
horizontalPadding: 8
}
}

View File

@ -30,12 +30,14 @@ Item {
property var backend property var backend
property var user property var user
property string address
signal dismissed() signal dismissed()
implicitHeight: children[0].implicitHeight implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth implicitWidth: children[0].implicitWidth
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: 0
@ -56,7 +58,7 @@ Item {
Label { Label {
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: user ? user.username : "" text: address
color: root.colorScheme.text_weak color: root.colorScheme.text_weak
type: Label.LabelType.Lead type: Label.LabelType.Lead
} }
@ -80,7 +82,13 @@ Item {
Repeater { Repeater {
model: clients model: clients
Rectangle {
implicitWidth: clientRow.width
implicitHeight: clientRow.height
ColumnLayout { ColumnLayout {
id: clientRow
RowLayout { RowLayout {
Layout.topMargin: 12 Layout.topMargin: 12
Layout.bottomMargin: 12 Layout.bottomMargin: 12
@ -106,6 +114,20 @@ Item {
color: root.colorScheme.border_weak color: root.colorScheme.border_weak
} }
} }
MouseArea {
anchors.fill: parent
onClicked: {
if (model.name != "Apple Mail") {
console.log(" TODO configure ", model.name)
return
}
root.user.configureAppleMail(root.address)
root.dismissed()
}
}
}
} }
Item { Layout.fillHeight: true } Item { Layout.fillHeight: true }

View File

@ -42,18 +42,9 @@ Item {
property var backend property var backend
property var window property var window
// in case of adding new account this property should be undefined property alias username: usernameTextField.text
property var user
state: "Page 1" state: "Page 1"
onUserChanged: {
stackLayout.currentIndex = 0
loginNormalLayout.reset()
passwordTextField.text = ""
login2FALayout.reset()
login2PasswordLayout.reset()
}
onLoginAbort: { onLoginAbort: {
stackLayout.currentIndex = 0 stackLayout.currentIndex = 0
loginNormalLayout.reset() loginNormalLayout.reset()
@ -78,15 +69,15 @@ Item {
} }
Connections { Connections {
target: user !== undefined ? user : root.backend target: root.backend
onLoginUsernamePasswordError: { onLoginUsernamePasswordError: {
console.assert(stackLayout.currentIndex == 0, "Unexpected loginUsernamePasswordError") console.assert(stackLayout.currentIndex == 0, "Unexpected loginUsernamePasswordError")
console.assert(signInButton.loading == true, "Unexpected loginUsernamePasswordError") console.assert(signInButton.loading == true, "Unexpected loginUsernamePasswordError")
stackLayout.loginFailed() stackLayout.loginFailed()
errorLabel.text = qsTr("Your email and/or password are incorrect") if (errorMsg!="") errorLabel.text = errorMsg
else errorLabel.text = qsTr("Your email and/or password are incorrect")
} }
onLoginFreeUserError: { onLoginFreeUserError: {
@ -152,6 +143,14 @@ Item {
errorLabel.text = qsTr("Incorrect login credentials. Please try again.") errorLabel.text = qsTr("Incorrect login credentials. Please try again.")
passwordTextField.text = "" passwordTextField.text = ""
} }
onLoginFinished: {
stackLayout.currentIndex = 0
loginNormalLayout.reset()
passwordTextField.text = ""
login2FALayout.reset()
login2PasswordLayout.reset()
}
} }
ColumnLayout { ColumnLayout {
@ -218,8 +217,6 @@ Item {
id: usernameTextField id: usernameTextField
label: qsTr("Username or email") label: qsTr("Username or email")
text: user !== undefined ? user.username : ""
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 24 Layout.topMargin: 24
@ -304,12 +301,7 @@ Item {
enabled = false enabled = false
loading = true loading = true
if (root.user !== undefined) { root.login(usernameTextField.text, Qt.btoa(passwordTextField.text))
root.user.login(usernameTextField.text, passwordTextField.text)
return
}
root.login(usernameTextField.text, passwordTextField.text)
} }
} }
@ -394,12 +386,7 @@ Item {
enabled = false enabled = false
loading = true loading = true
if (root.user !== undefined) { root.login2FA(usernameTextField.text, Qt.btoa(twoFactorPasswordTextField.text))
root.user.login2FA(usernameTextField.text, twoFactorPasswordTextField.text)
return
}
root.login2FA(usernameTextField.text, twoFactorPasswordTextField.text)
} }
} }
} }
@ -471,12 +458,7 @@ Item {
enabled = false enabled = false
loading = true loading = true
if (root.user !== undefined) { root.login2Password(usernameTextField.text, Qt.btoa(secondPasswordTextField.text))
root.user.login2Password(usernameTextField.text, secondPasswordTextField.text)
return
}
root.login2Password(usernameTextField.text, secondPasswordTextField.text)
} }
} }
} }

View File

@ -22,20 +22,16 @@ import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13 import QtQuick.Controls 2.13
import Proton 4.0 import Proton 4.0
import ProtonBackend 1.0
import Notifications 1.0 import Notifications 1.0
// Because of https://bugreports.qt.io/browse/QTBUG-69777 and other bugs alike it is impossible Window {
// to use Window with flags: Qt.Popup here since it won't close by it's own on click outside.
PopupWindow {
id: root id: root
title: "ProtonMail Bridge" title: "ProtonMail Bridge"
height: contentLayout.implicitHeight height: contentLayout.implicitHeight
width: contentLayout.implicitWidth width: contentLayout.implicitWidth
minimumHeight: 201 flags: Qt.FramelessWindowHint
minimumWidth: 448
property ColorScheme colorScheme: ProtonStyle.currentStyle property ColorScheme colorScheme: ProtonStyle.currentStyle
@ -47,15 +43,19 @@ PopupWindow {
signal showMainWindow() signal showMainWindow()
signal showHelp() signal showHelp()
signal showSettings() signal showSettings()
signal showSignIn(string username)
signal quit() signal quit()
ColumnLayout { ColumnLayout {
id: contentLayout id: contentLayout
Layout.minimumHeight: 201
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: 0
ColumnLayout { ColumnLayout {
Layout.minimumWidth: 448
Layout.fillWidth: true Layout.fillWidth: true
spacing: 0 spacing: 0
@ -171,13 +171,17 @@ PopupWindow {
interactive: contentHeight > parent.height interactive: contentHeight > parent.height
snapMode: ListView.SnapToItem snapMode: ListView.SnapToItem
boundsBehavior: Flickable.StopAtBounds
delegate: Item { delegate: Item {
id: viewItem
width: ListView.view.width width: ListView.view.width
implicitHeight: children[0].implicitHeight implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth implicitWidth: children[0].implicitWidth
property var user: root.backend.users.get(index)
RowLayout { RowLayout {
spacing: 0 spacing: 0
anchors.fill: parent anchors.fill: parent
@ -187,15 +191,19 @@ PopupWindow {
Layout.margins: 12 Layout.margins: 12
user: modelData user: viewItem.user
colorScheme: root.colorScheme colorScheme: root.colorScheme
} }
Button { Button {
Layout.margins: 12 Layout.margins: 12
colorScheme: root.colorScheme colorScheme: root.colorScheme
visible: true visible: !viewItem.user.loggedIn
text: "test" text: qsTr("Sign in")
onClicked: {
root.showSignIn(viewItem.username)
root.visible = false
}
} }
} }
} }
@ -297,4 +305,8 @@ PopupWindow {
} }
} }
} }
onActiveChanged: {
if (!active) root.close()
}
} }

View File

@ -239,7 +239,7 @@ Item {
root.loginAbort(username) root.loginAbort(username)
} }
user: (backend.users.count === 1 && backend.users.get(0).loggedIn === false) ? backend.users.get(0) : undefined username: (backend.users.count === 1 && backend.users.get(0).loggedIn === false) ? backend.users.get(0).username : ""
backend: root.backend backend: root.backend
window: root.window window: root.window
} }

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="ic-chevron-down">
<path id="icon" fill-rule="evenodd" clip-rule="evenodd" d="M2.3 6.30001L8 12L13.7 6.30001L13 5.60001L8 10.58L3 5.60001L2.3 6.30001Z" fill="#17181C"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 283 B

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="ic-chevron-up">
<path id="icon" fill-rule="evenodd" clip-rule="evenodd" d="M13.7 9.7L7.99999 4L2.29999 9.7L2.99999 10.4L7.99999 5.42L13 10.4L13.7 9.7Z" fill="#17181C"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 284 B

View File

@ -0,0 +1,4 @@
<svg width="12" height="13" viewBox="0 0 12 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 2V0H12V10H9V9H11V1H5V2H4Z" fill="#262A33"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 3V13H8V3H0ZM7 4V12H1V4H7Z" fill="#262A33"/>
</svg>

After

Width:  |  Height:  |  Size: 255 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 2L13.2427 2L13.2427 1.99998L13.2426 2L13 2L9 2V3H12.2426L5.76613 9.47651L6.47324 10.1836L13 3.65686V7H14V3V2ZM2 2H5V3H3L3 13L13 13V11H14V14H13H3H2V13V3V2Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 327 B

View File

@ -0,0 +1,12 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="32" cy="32" r="30" fill="url(#paint0_linear)"/>
<path d="M32 18C30.3431 18 29 19.3431 29 21C29 22.6569 30.3431 24 32 24C33.6569 24 35 22.6569 35 21C35 19.3431 33.6569 18 32 18Z" fill="white"/>
<path d="M30 28C28.8954 28 28 28.8954 28 30C28 31.1046 28.8954 32 30 32V42C28.8954 42 28 42.8954 28 44C28 45.1046 28.8954 46 30 46H34C35.1046 46 36 45.1046 36 44C36 42.8954 35.1046 42 34 42V30C34 28.8954 33.1046 28 32 28H30Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear" x1="32" y1="62" x2="14.4192" y2="7.69125" gradientUnits="userSpaceOnUse">
<stop stop-color="#4F6DE6"/>
<stop offset="0.483234" stop-color="#63A1FE"/>
<stop offset="1" stop-color="#82D2FF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 807 B

View File

@ -0,0 +1,5 @@
<svg width="14" height="13" viewBox="0 0 14 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 5H6V10H5V5Z" fill="#262A33"/>
<path d="M8 5H9V10H8V5Z" fill="#262A33"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 2H14V3H12V13H2V3H0V2H4V0H10V2ZM9 1H5V2H9V1ZM11 12H3V3H11V12Z" fill="#262A33"/>
</svg>

After

Width:  |  Height:  |  Size: 319 B

View File

@ -0,0 +1,24 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build darwin
// +build build_qt
#include <stdbool.h>
void SetDockIconVisibleState(bool visible);
bool GetDockIconVisibleState();

View File

@ -0,0 +1,42 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build darwin
// +build build_qt
#include "DockIcon.h"
#include <Cocoa/Cocoa.h>
void SetDockIconVisibleState(bool visible) {
if (visible) {
[NSApp setActivationPolicy: NSApplicationActivationPolicyRegular];
return;
} else {
[NSApp setActivationPolicy: NSApplicationActivationPolicyAccessory];
return;
}
}
bool GetDockIconVisibleState() {
switch ([NSApp activationPolicy]) {
case NSApplicationActivationPolicyAccessory:
case NSApplicationActivationPolicyProhibited:
return false;
case NSApplicationActivationPolicyRegular:
return true;
}
}

View File

@ -0,0 +1,33 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build darwin
// +build build_qt
package dockicon
// #cgo CFLAGS: -x objective-c
// #cgo LDFLAGS: -framework Cocoa
// #include "DockIcon.h"
import "C"
func SetDockIconVisibleState(visible bool) {
C.SetDockIconVisibleState(C.bool(visible))
}
func GetDockIconVisibleState() bool {
return bool(C.GetDockIconVisibleState())
}

View File

@ -0,0 +1,26 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build !darwin
// +build build_qt
package dockicon
func SetDockIconVisibleState(visible bool) {}
func GetDockIconVisibleState() bool {
return true
}

View File

@ -0,0 +1,146 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build build_qt
// Package qt provides communication between Qt/QML frontend and Go backend
package qt
import (
"fmt"
"sync"
"github.com/ProtonMail/go-autostart"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/sirupsen/logrus"
"github.com/therecipe/qt/qml"
"github.com/therecipe/qt/widgets"
)
type FrontendQt struct {
programName, programVersion string
panicHandler types.PanicHandler
locations *locations.Locations
settings *settings.Settings
eventListener listener.Listener
updater types.Updater
userAgent *useragent.UserAgent
bridge types.Bridger
noEncConfirmator types.NoEncConfirmator
autostart *autostart.App
restarter types.Restarter
authClient pmapi.Client
auth *pmapi.Auth
password []byte
newVersionInfo updater.VersionInfo
log *logrus.Entry
usersMtx sync.Mutex
app *widgets.QApplication
engine *qml.QQmlApplicationEngine
qml *QMLBackend
}
func New(
version,
buildVersion,
programName string,
showWindowOnStart bool,
panicHandler types.PanicHandler,
locations *locations.Locations,
settings *settings.Settings,
eventListener listener.Listener,
updater types.Updater,
userAgent *useragent.UserAgent,
bridge types.Bridger,
_ types.NoEncConfirmator,
autostart *autostart.App,
restarter types.Restarter,
) *FrontendQt {
return &FrontendQt{
programName: "Proton Mail Bridge",
programVersion: version,
log: logrus.WithField("pkg", "frontend/qt"),
panicHandler: panicHandler,
locations: locations,
settings: settings,
eventListener: eventListener,
updater: updater,
userAgent: userAgent,
bridge: bridge,
autostart: autostart,
restarter: restarter,
}
}
func (f *FrontendQt) Loop() error {
err := f.initiateQtApplication()
if err != nil {
return err
}
go func() {
defer f.panicHandler.HandlePanic()
f.watchEvents()
}()
if ret := f.app.Exec(); ret != 0 {
err := fmt.Errorf("Event loop ended with return value: %v", ret)
f.log.Warn("App exec", err)
return err
}
return nil
}
func (f *FrontendQt) NotifyManualUpdate(version updater.VersionInfo, canInstall bool) {
if canInstall {
f.qml.UpdateManualReady(version.Version.String())
} else {
f.qml.UpdateManualError()
}
}
func (f *FrontendQt) SetVersion(version updater.VersionInfo) {
f.newVersionInfo = version
f.qml.SetReleaseNotesLink(version.ReleaseNotesPage)
f.qml.SetLandingPageLink(version.LandingPage)
}
func (f *FrontendQt) NotifySilentUpdateInstalled() {
f.qml.UpdateSilentRestartNeeded()
}
func (f *FrontendQt) NotifySilentUpdateError(err error) {
f.log.WithError(err).Warn("Update failed, asking for manual.")
f.qml.UpdateManualError()
}
func (f *FrontendQt) WaitUntilFrontendIsReady() {
// TODO: Implement
}

View File

@ -0,0 +1,85 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build build_qt
// Package qt provides communication between Qt/QML frontend and Go backend
package qt
import (
"strings"
"github.com/ProtonMail/proton-bridge/internal/events"
)
func (f *FrontendQt) watchEvents() {
f.WaitUntilFrontendIsReady()
errorCh := f.eventListener.ProvideChannel(events.ErrorEvent)
credentialsErrorCh := f.eventListener.ProvideChannel(events.CredentialsErrorEvent)
noActiveKeyForRecipientCh := f.eventListener.ProvideChannel(events.NoActiveKeyForRecipientEvent)
internetOffCh := f.eventListener.ProvideChannel(events.InternetOffEvent)
internetOnCh := f.eventListener.ProvideChannel(events.InternetOnEvent)
secondInstanceCh := f.eventListener.ProvideChannel(events.SecondInstanceEvent)
restartBridgeCh := f.eventListener.ProvideChannel(events.RestartBridgeEvent)
addressChangedCh := f.eventListener.ProvideChannel(events.AddressChangedEvent)
addressChangedLogoutCh := f.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
logoutCh := f.eventListener.ProvideChannel(events.LogoutEvent)
updateApplicationCh := f.eventListener.ProvideChannel(events.UpgradeApplicationEvent)
userChangedCh := f.eventListener.ProvideChannel(events.UserRefreshEvent)
certIssue := f.eventListener.ProvideChannel(events.TLSCertIssue)
for {
select {
case errorDetails := <-errorCh:
if strings.Contains(errorDetails, "IMAP failed") {
f.qml.PortIssueIMAP()
}
if strings.Contains(errorDetails, "SMTP failed") {
f.qml.PortIssueSMTP()
}
case <-credentialsErrorCh:
f.qml.NotifyHasNoKeychain()
case email := <-noActiveKeyForRecipientCh:
f.qml.NoActiveKeyForRecipient(email)
case <-internetOffCh:
f.qml.InternetOff()
case <-internetOnCh:
f.qml.InternetOn()
case <-secondInstanceCh:
f.qml.ShowMainWindow()
case <-restartBridgeCh:
f.restart()
case address := <-addressChangedCh:
f.qml.AddressChanged(address)
case address := <-addressChangedLogoutCh:
f.qml.AddressChangedLogout(address)
case userID := <-logoutCh:
user, err := f.bridge.GetUser(userID)
if err != nil {
return
}
f.qml.UserDisconnected(user.Username())
case <-updateApplicationCh:
f.updateForce()
case userID := <-userChangedCh:
f.userChanged(userID)
case <-certIssue:
f.qml.ApiCertIssue()
}
}
}

View File

@ -0,0 +1,45 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build build_qt
package qt
func (f *FrontendQt) setVersion() {
f.qml.SetVersion(f.programVersion)
}
func (f *FrontendQt) setLogsPath() {
path, err := f.locations.ProvideLogsPath()
if err != nil {
f.log.WithError(err).Error("Cannot update path folder")
return
}
f.qml.SetLogsPath(path)
}
func (f *FrontendQt) setLicensePath() {
f.qml.SetLicensePath(f.locations.GetLicenseFilePath())
}
func (f *FrontendQt) setCurrentEmailClient() {
f.qml.SetCurrentEmailClient(f.userAgent.String())
}
func (f *FrontendQt) reportBug(description, address, emailClient string, includeLogs bool) {
//TODO
}

View File

@ -0,0 +1,71 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build build_qt
package qt
import (
"errors"
qmlLog "github.com/ProtonMail/proton-bridge/internal/frontend/qt/log"
"github.com/therecipe/qt/core"
"github.com/therecipe/qt/qml"
"github.com/therecipe/qt/quickcontrols2"
"github.com/therecipe/qt/widgets"
"os"
)
func (f *FrontendQt) initiateQtApplication() error {
qmlLog.InstallMessageHandler()
f.app = widgets.NewQApplication(len(os.Args), os.Args)
core.QCoreApplication_SetApplicationName(f.programName)
core.QCoreApplication_SetApplicationVersion(f.programVersion)
// High DPI scaling for windows.
core.QCoreApplication_SetAttribute(core.Qt__AA_EnableHighDpiScaling, false)
// Software OpenGL: to avoid dedicated GPU.
core.QCoreApplication_SetAttribute(core.Qt__AA_UseSoftwareOpenGL, true)
// Bridge runs background, no window is needed to be opened.
f.app.SetQuitOnLastWindowClosed(false)
// QML Engine and path
f.engine = qml.NewQQmlApplicationEngine(f.app)
f.qml = NewQMLBackend(nil)
f.qml.setup(f)
f.engine.RootContext().SetContextProperty("go", f.qml)
f.engine.AddImportPath("qrc:/qml/")
f.engine.AddPluginPath("qrc:/qml/")
// Add style: if colorScheme / style is forgotten we should fallback to
// default style and should be Proton
quickcontrols2.QQuickStyle_AddStylePath("qrc:/qml/")
quickcontrols2.QQuickStyle_SetStyle("Proton")
f.engine.Load(core.NewQUrl3("qrc:/qml/Bridge.qml", 0))
// Check QML is loaded properly.
if len(f.engine.RootObjects()) == 0 {
return errors.New("QML not loaded properly")
}
return nil
}

View File

@ -0,0 +1,83 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build !build_qt
package qt
import (
"fmt"
"net/http"
"github.com/ProtonMail/go-autostart"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/sirupsen/logrus"
)
var log = logrus.WithField("pkg", "frontend-nogui") //nolint[gochecknoglobals]
type FrontendHeadless struct{}
func New(
version,
buildVersion,
programName string,
showWindowOnStart bool,
panicHandler types.PanicHandler,
locations *locations.Locations,
settings *settings.Settings,
eventListener listener.Listener,
updater types.Updater,
userAgent *useragent.UserAgent,
bridge types.Bridger,
noEncConfirmator types.NoEncConfirmator,
autostart *autostart.App,
restarter types.Restarter,
) *FrontendHeadless {
return &FrontendHeadless{}
}
func (s *FrontendHeadless) Loop() error {
log.Info("Check status on localhost:8081")
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Bridge is running")
})
return http.ListenAndServe(":8081", nil)
}
func (s *FrontendHeadless) NotifyManualUpdate(update updater.VersionInfo, canInstall bool) {
// NOTE: Save the update somewhere so that it can be installed when user chooses "install now".
}
func (s *FrontendHeadless) WaitUntilFrontendIsReady() {
}
func (s *FrontendHeadless) SetVersion(update updater.VersionInfo) {
}
func (s *FrontendHeadless) NotifySilentUpdateInstalled() {
}
func (s *FrontendHeadless) NotifySilentUpdateError(err error) {
}
func (s *FrontendHeadless) InstanceExistAlert() {}

View File

@ -0,0 +1,151 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build build_qt
package qt
import (
"time"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/frontend/clientconfig"
"github.com/ProtonMail/proton-bridge/pkg/keychain"
"github.com/ProtonMail/proton-bridge/pkg/ports"
)
func (f *FrontendQt) setIsDiskCacheEnabled() {
//TODO
}
func (f *FrontendQt) setDiskCachePath() {
//TODO
}
func (f *FrontendQt) changeLocalCache(enableDiskCache bool, diskCachePath string) {
//TODO
}
func (f *FrontendQt) setIsAutostartOn() {
f.qml.SetIsAutostartOn(f.autostart.IsEnabled())
}
func (f *FrontendQt) toggleAutostart(makeItEnabled bool) {
defer f.qml.ToggleAutostartFinished()
if makeItEnabled == f.autostart.IsEnabled() {
f.setIsAutostartOn()
return
}
var err error
if makeItEnabled {
err = f.autostart.Enable()
} else {
err = f.autostart.Disable()
}
f.setIsAutostartOn()
if err != nil {
f.log.
WithField("makeItEnabled", makeItEnabled).
WithField("isEnabled", f.qml.IsAutostartOn()).
WithError(err).
Error("Autostart change failed")
}
}
func (f *FrontendQt) toggleDoH(makeItEnabled bool) {
if f.settings.GetBool(settings.AllowProxyKey) == makeItEnabled {
f.qml.SetIsDoHEnabled(makeItEnabled)
return
}
f.settings.SetBool(settings.AllowProxyKey, makeItEnabled)
f.restart()
}
func (f *FrontendQt) toggleUseSSLforSMTP(makeItEnabled bool) {
if f.settings.GetBool(settings.SMTPSSLKey) == makeItEnabled {
f.qml.SetUseSSLforSMTP(makeItEnabled)
return
}
f.settings.SetBool(settings.SMTPPortKey, makeItEnabled)
f.restart()
}
func (f *FrontendQt) changePorts(imapPort, smtpPort int) {
f.settings.SetInt(settings.IMAPPortKey, imapPort)
f.settings.SetInt(settings.SMTPPortKey, smtpPort)
f.restart()
}
func (f *FrontendQt) isPortFree(port int) bool {
return ports.IsPortFree(port)
}
func (f *FrontendQt) configureAppleMail(userID, address string) {
user, err := f.bridge.GetUser(userID)
if err != nil {
f.log.WithField("userID", userID).Error("Cannot configure AppleMail for user")
return
}
needRestart, err := clientconfig.ConfigureAppleMail(user, address, f.settings)
if err != nil {
f.log.WithError(err).Error("Apple Mail config failed")
}
if needRestart {
// There is delay needed for external window to open
time.Sleep(2 * time.Second)
f.restart()
}
}
func (f *FrontendQt) triggerReset() {
defer f.qml.ResetFinished()
//TODO
f.restart()
}
func (f *FrontendQt) setKeychain() {
availableKeychain := []string{}
for chain := range keychain.Helpers {
availableKeychain = append(availableKeychain, chain)
}
f.qml.SetAvailableKeychain(availableKeychain)
f.qml.SetSelectedKeychain(f.bridge.GetKeychainApp())
}
func (f *FrontendQt) selectKeychain(wantKeychain string) {
if f.bridge.GetKeychainApp() == wantKeychain {
return
}
f.bridge.SetKeychainApp(wantKeychain)
f.restart()
}
func (f *FrontendQt) restart() {
f.log.Info("Restarting bridge")
f.restarter.SetToRestart()
f.app.Exit(0)
}
func (f *FrontendQt) quit() {
f.log.Warn("Your wish is my command.. I quit!")
f.app.Exit(0)
}

View File

@ -0,0 +1,130 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build build_qt
package qt
import (
"sync"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/updater"
)
var checkingUpdates = sync.Mutex{}
func (f *FrontendQt) checkUpdates() error {
version, err := f.updater.Check()
if err != nil {
return err
}
f.SetVersion(version)
return nil
}
func (f *FrontendQt) checkUpdatesAndNotify(isRequestFromUser bool) {
checkingUpdates.Lock()
defer checkingUpdates.Lock()
defer f.qml.CheckUpdatesFinished()
if err := f.checkUpdates(); err != nil {
f.log.WithError(err).Error("An error occurred while checking updates")
if isRequestFromUser {
f.qml.UpdateManualError()
}
return
}
if !f.updater.IsUpdateApplicable(f.newVersionInfo) {
f.log.Debug("No need to update")
if isRequestFromUser {
f.qml.UpdateIsLatestVersion()
}
return
}
if !f.updater.CanInstall(f.newVersionInfo) {
f.log.Debug("A manual update is required")
f.qml.UpdateManualReady(f.newVersionInfo.Version.String())
return
}
}
func (f *FrontendQt) updateForce() {
checkingUpdates.Lock()
defer checkingUpdates.Lock()
version := ""
if err := f.checkUpdates(); err == nil {
version = f.newVersionInfo.Version.String()
}
f.qml.UpdateForce(version)
}
func (f *FrontendQt) setIsAutomaticUpdateOn() {
f.qml.SetIsAutomaticUpdateOn(f.settings.GetBool(settings.AutoUpdateKey))
}
func (f *FrontendQt) toggleAutomaticUpdate(makeItEnabled bool) {
f.qml.SetIsAutomaticUpdateOn(makeItEnabled)
isEnabled := f.settings.GetBool(settings.AutoUpdateKey)
if makeItEnabled == isEnabled {
return
}
f.settings.SetBool(settings.AutoUpdateKey, makeItEnabled)
f.checkUpdatesAndNotify(false)
}
func (f *FrontendQt) setIsBetaEnabled() {
channel := f.bridge.GetUpdateChannel()
f.qml.SetIsBetaEnabled(channel == updater.EarlyChannel)
}
func (f *FrontendQt) toggleBeta(makeItEnabled bool) {
channel := f.bridge.GetUpdateChannel()
if makeItEnabled == (channel == updater.EarlyChannel) {
f.qml.SetIsBetaEnabled(makeItEnabled)
return
}
channel = updater.StableChannel
if makeItEnabled {
channel = updater.EarlyChannel
}
needRestart, err := f.bridge.SetUpdateChannel(channel)
f.setIsBetaEnabled()
if err != nil {
f.log.WithError(err).Warn("Switching udpate channel failed.")
f.qml.UpdateManualError()
return
}
if needRestart {
f.restart()
return
}
f.checkUpdatesAndNotify(false)
}

View File

@ -0,0 +1,224 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build build_qt
package qt
import (
"context"
"encoding/base64"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
func (f *FrontendQt) loadUsers() {
f.usersMtx.Lock()
defer f.usersMtx.Unlock()
f.qml.Users().clear()
for _, user := range f.bridge.GetUsers() {
f.qml.Users().addUser(newQMLUserFromBacked(f, user))
}
// If there are no active accounts.
if f.qml.Users().Count() == 0 {
f.log.Info("No active accounts")
}
}
func (f *FrontendQt) userChanged(userID string) {
f.usersMtx.Lock()
defer f.usersMtx.Unlock()
fUsers := f.qml.Users()
index := fUsers.indexByID(userID)
user, err := f.bridge.GetUser(userID)
if user == nil || err != nil {
if index >= 0 { // delete existing user
fUsers.removeUser(index)
}
return
}
if index < 0 { // add non-existing user
fUsers.addUser(newQMLUserFromBacked(f, user))
return
}
// update exiting user
fUsers.users[index].update(user)
}
func newQMLUserFromBacked(f *FrontendQt, user types.User) *QMLUser {
qu := NewQMLUser(nil)
qu.ID = user.ID()
qu.update(user)
qu.ConnectToggleSplitMode(func(activateSplitMode bool) {
go func() {
defer qu.ToggleSplitModeFinished()
if activateSplitMode == user.IsCombinedAddressMode() {
user.SwitchAddressMode()
}
qu.SetSplitMode(!user.IsCombinedAddressMode())
}()
})
qu.ConnectLogout(func() {
qu.SetLoggedIn(false)
go user.Logout()
})
qu.ConnectConfigureAppleMail(func(address string) {
go f.configureAppleMail(qu.ID, address)
})
return qu
}
func (f *FrontendQt) login(username, password string) {
var err error
f.password, err = base64.StdEncoding.DecodeString(password)
if err != nil {
f.log.WithError(err).Error("Cannot decode password")
f.qml.LoginUsernamePasswordError("Cannot decode password")
f.loginClean()
return
}
f.authClient, f.auth, err = f.bridge.Login(username, f.password)
if err != nil {
f.qml.LoginUsernamePasswordError(err.Error())
f.loginClean()
return
}
if f.auth.HasTwoFactor() {
f.qml.Login2FARequested()
return
}
if f.auth.HasMailboxPassword() {
f.qml.Login2PasswordRequested()
return
}
f.finishLogin()
}
func (f *FrontendQt) login2FA(username, code string) {
if f.auth == nil || f.authClient == nil {
f.log.Errorf("Login 2FA: authethication incomplete %p %p", f.auth, f.authClient)
f.qml.Login2FAErrorAbort("Missing authentication, try again.")
f.loginClean()
return
}
twoFA, err := base64.StdEncoding.DecodeString(code)
if err != nil {
f.log.WithError(err).Error("Cannot decode 2fa code")
f.qml.LoginUsernamePasswordError("Cannot decode 2fa code")
f.loginClean()
return
}
err = f.authClient.Auth2FA(context.Background(), string(twoFA))
if err == pmapi.ErrBad2FACodeTryAgain {
f.log.Warn("Login 2FA: retry 2fa")
f.qml.Login2FAError("")
return
}
if err == pmapi.ErrBad2FACode {
f.log.Warn("Login 2FA: abort 2fa")
f.qml.Login2FAErrorAbort("")
f.loginClean()
return
}
if err != nil {
f.log.WithError(err).Warn("Login 2FA: failed.")
f.qml.Login2FAErrorAbort(err.Error())
f.loginClean()
return
}
if f.auth.HasMailboxPassword() {
f.qml.Login2PasswordRequested()
return
}
f.finishLogin()
}
func (f *FrontendQt) login2Password(username, mboxPassword string) {
var err error
f.password, err = base64.StdEncoding.DecodeString(mboxPassword)
if err != nil {
f.log.WithError(err).Error("Cannot decode mbox password")
f.qml.LoginUsernamePasswordError("Cannot decode mbox password")
f.loginClean()
return
}
f.finishLogin()
}
func (f *FrontendQt) finishLogin() {
defer f.loginClean()
if f.auth == nil || f.authClient == nil {
f.log.Errorf("Finish login: Authethication incomplete %p %p", f.auth, f.authClient)
f.qml.Login2PasswordErrorAbort("Missing authentication, try again.")
return
}
user, err := f.bridge.FinishLogin(f.authClient, f.auth, f.password)
if err != nil {
f.log.Errorf("Authethication incomplete %p %p", f.auth, f.authClient)
f.qml.Login2PasswordErrorAbort("Missing authentication, try again.")
return
}
index := f.qml.Users().indexByID(user.ID())
if index < 0 {
qu := newQMLUserFromBacked(f, user)
qu.SetSetupGuideSeen(false)
f.qml.Users().addUser(qu)
return
}
f.qml.Users().users[index].update(user)
f.qml.LoginFinished()
}
func (f *FrontendQt) loginAbort(username string) {
f.loginClean()
}
func (f *FrontendQt) loginClean() {
f.auth = nil
f.authClient = nil
for i := range f.password {
f.password[i] = '\x00'
}
f.password = f.password[0:0]
}

View File

@ -0,0 +1,70 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build build_qt
package qt
import (
"regexp"
"strings"
"github.com/therecipe/qt/core"
"github.com/therecipe/qt/gui"
)
// getCursorPos returns current mouse position to be able to use in QML
func getCursorPos() *core.QPoint {
return gui.QCursor_Pos()
}
// newQByteArrayFromString is a wrapper for new QByteArray from string.
func newQByteArrayFromString(name string) *core.QByteArray {
return core.NewQByteArray2(name, len(name))
}
var (
reMultiSpaces = regexp.MustCompile(`\s{2,}`)
reStartWithSymbol = regexp.MustCompile(`^[.,/#!$@%^&*;:{}=\-_` + "`" + `~()]`)
)
// getInitials based on webapp implementation:
// https://github.com/ProtonMail/WebClients/blob/55d96a8b4afaaa4372fc5f1ef34953f2070fd7ec/packages/shared/lib/helpers/string.ts#L145
func getInitials(fullName string) string {
words := strings.Split(
reMultiSpaces.ReplaceAllString(fullName, " "),
" ",
)
n := 0
for _, word := range words {
if !reStartWithSymbol.MatchString(word) {
words[n] = word
n++
}
}
if n == 0 {
return "?"
}
initials := words[0][0:1]
if n != 1 {
initials += words[n-1][0:1]
}
return strings.ToUpper(initials)
}

View File

@ -0,0 +1,44 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build build_qt
#include "log.h"
#include "_cgo_export.h"
#include <QObject>
#include <QByteArray>
#include <QString>
#include <QVector>
#include <QtGlobal>
void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
Q_UNUSED( type )
Q_UNUSED( context )
QByteArray localMsg = msg.toUtf8().prepend("WHITESPACE");
logMsgPacked(
const_cast<char*>( (localMsg.constData()) +10 ),
localMsg.size()-10
);
}
void InstallMessageHandler() {
qInstallMessageHandler(messageHandler);
}

View File

@ -0,0 +1,46 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build build_qt
// Package log redirects QML logs to logrus
package log
//#include "log.h"
import "C"
import (
"github.com/sirupsen/logrus"
"github.com/therecipe/qt/core"
)
var logQML = logrus.WithField("pkg", "frontent/qml")
// InstallMessageHandler is registering logQML as logger for QML calls.
func InstallMessageHandler() {
C.InstallMessageHandler()
}
//export logMsgPacked
func logMsgPacked(data *C.char, len C.int) {
logQML.Warn(C.GoStringN(data, len))
}
// logDummy is here to trigger qtmoc to create cgo instructions
type logDummy struct {
core.QObject
}

View File

@ -0,0 +1,36 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#ifndef LOGRUS_QML_LOG_H
#define LOGRUS_QML_LOG_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif // C++
void InstallMessageHandler();
;
#ifdef __cplusplus
}
#endif // C++
#endif // LOGRUS_QML_LOG_H

View File

@ -0,0 +1,203 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build build_qt
package qt
import (
"runtime"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
dockIcon "github.com/ProtonMail/proton-bridge/internal/frontend/qt/dockicon"
"github.com/therecipe/qt/core"
)
// QMLBackend connects QML frontend with Go backend.
type QMLBackend struct {
core.QObject
_ func() *core.QPoint `slot:"getCursorPos"`
_ func() `slot:"quit"`
_ func() `slot:"restart"`
_ bool `property:dockIconVisible`
_ QMLUserModel `property:"users"`
// TODO copy stuff from Bridge_test.qml backend object
_ string `property:"goos"`
_ func(username, password string) `slot:"login"`
_ func(username, code string) `slot:"login2FA"`
_ func(username, password string) `slot:"login2Password"`
_ func(username string) `slot:"loginAbort"`
_ func(errorMsg string) `signal:"loginUsernamePasswordError"`
_ func(errorMsg string) `signal:"loginFreeUserError"`
_ func(errorMsg string) `signal:"loginConnectionError"`
_ func() `signal:"login2FARequested"`
_ func(errorMsg string) `signal:"login2FAError"`
_ func(errorMsg string) `signal:"login2FAErrorAbort"`
_ func() `signal:"login2PasswordRequested"`
_ func(errorMsg string) `signal:"login2PasswordError"`
_ func(errorMsg string) `signal:"login2PasswordErrorAbort"`
_ func() `signal:"loginFinished"`
_ func() `signal:"internetOff"`
_ func() `signal:"internetOn"`
_ func(version string) `signal:"updateManualReady"`
_ func() `signal:"updateManualRestartNeeded"`
_ func() `signal:"updateManualError"`
_ func(version string) `signal:"updateForce"`
_ func() `signal:"updateForceError"`
_ func() `signal:"updateSilentRestartNeeded"`
_ func() `signal:"updateSilentError"`
_ func() `signal:"updateIsLatestVersion"`
_ func() `slot:"checkUpdates"`
_ func() `signal:"checkUpdatesFinished"`
_ bool `property:"isDiskCacheEnabled"`
_ string `property:"diskCachePath"`
_ func() `signal:"cacheUnavailable"`
_ func() `signal:"cacheCantMove"`
_ func() `signal:"cacheLocationChangeSuccess"`
_ func() `signal:"diskFull"`
_ func(enableDiskCache bool, diskCachePath string) `slot:"changeLocalCache"`
_ func() `signal:"changeLocalCacheFinished"`
_ bool `property:"isAutomaticUpdateOn"`
_ func(makeItActive bool) `slot:"toggleAutomaticUpdate"`
_ bool `property:"isAutostartOn"`
_ func(makeItActive bool) `slot:"toggleAutostart"`
_ func() `signal:"toggleAutostartFinished"`
_ bool `property:"isBetaEnabled"`
_ func(makeItActive bool) `slot:"toggleBeta"`
_ bool `property:"isDoHEnabled"`
_ func(makeItActive bool) `slot:"toggleDoH"`
_ bool `property:"useSSLforSMTP"`
_ func(makeItActive bool) `slot:"toggleUseSSLforSMTP"`
_ func() `signal:"toggleUseSSLFinished"`
_ string `property:"hostname"`
_ int `property:"portIMAP"`
_ int `property:"portSMTP"`
_ func(imapPort, smtpPort int) `slot:"changePorts"`
_ func(port int) bool `slot:"isPortFree"`
_ func() `signal:"changePortFinished"`
_ func() `signal:"portIssueIMAP"`
_ func() `signal:"portIssueSMTP"`
_ func() `slot:"triggerReset"`
_ func() `signal:"resetFinished"`
_ string `property:"version"`
_ string `property:"logsPath"`
_ string `property:"licensePath"`
_ string `property:"releaseNotesLink"`
_ string `property:"landingPageLink"`
_ string `property:"currentEmailClient"`
_ func() `slot:"updateCurrentMailClient"`
_ func(description, address, emailClient string, includeLogs bool) `slot:"reportBug"`
_ func() `signal:"reportBugFinished"`
_ func() `signal:"bugReportSendSuccess"`
_ func() `signal:"bugReportSendError"`
_ []string `property:"availableKeychain"`
_ string `property:"selectedKeychain"`
_ func(keychain string) `slot:"selectKeychain"`
_ func() `signal:"notifyHasNoKeychain"`
_ func(email string) `signal:noActiveKeyForRecipient`
_ func() `signal:showMainWindow`
_ func(address string) `signal:addressChanged`
_ func(address string) `signal:addressChangedLogout`
_ func(username string) `signal:userDisconnected`
_ func() `signal:apiCertIssue`
}
func (q *QMLBackend) setup(f *FrontendQt) {
q.ConnectGetCursorPos(getCursorPos)
q.ConnectQuit(f.quit)
q.ConnectRestart(f.restart)
q.ConnectIsDockIconVisible(func() bool {
return dockIcon.GetDockIconVisibleState()
})
q.ConnectSetDockIconVisible(func(visible bool) {
dockIcon.SetDockIconVisibleState(visible)
})
q.SetUsers(NewQMLUserModel(nil))
f.loadUsers()
q.SetGoos(runtime.GOOS)
q.ConnectLogin(func(u, p string) { go f.login(u, p) })
q.ConnectLogin2FA(func(u, p string) { go f.login2FA(u, p) })
q.ConnectLogin2Password(func(u, p string) { go f.login2Password(u, p) })
q.ConnectLoginAbort(func(u string) { go f.loginAbort(u) })
go f.checkUpdatesAndNotify(false)
q.ConnectCheckUpdates(func() { go f.checkUpdatesAndNotify(true) })
f.setIsDiskCacheEnabled()
f.setDiskCachePath()
q.ConnectChangeLocalCache(f.changeLocalCache)
f.setIsAutomaticUpdateOn()
q.ConnectToggleAutomaticUpdate(func(m bool) { go f.toggleAutomaticUpdate(m) })
f.setIsAutostartOn()
q.ConnectToggleAutostart(f.toggleAutostart)
f.setIsBetaEnabled()
q.ConnectToggleBeta(func(m bool) { go f.toggleBeta(m) })
q.SetIsDoHEnabled(f.settings.GetBool(settings.AllowProxyKey))
q.ConnectToggleDoH(f.toggleDoH)
q.SetUseSSLforSMTP(f.settings.GetBool(settings.SMTPSSLKey))
q.ConnectToggleUseSSLforSMTP(f.toggleUseSSLforSMTP)
q.SetHostname(bridge.Host)
q.SetPortIMAP(f.settings.GetInt(settings.IMAPPortKey))
q.SetPortSMTP(f.settings.GetInt(settings.SMTPPortKey))
q.ConnectChangePorts(f.changePorts)
q.ConnectIsPortFree(f.isPortFree)
q.ConnectTriggerReset(func() { go f.triggerReset() })
f.setVersion()
f.setLogsPath()
// release notes link is set by update
f.setLicensePath()
f.setCurrentEmailClient()
q.ConnectUpdateCurrentMailClient(func() { go f.setCurrentEmailClient() })
q.ConnectReportBug(func(d, a, e string, i bool) { go f.reportBug(d, a, e, i) })
f.setKeychain()
q.ConnectSelectKeychain(func(k string) { go f.selectKeychain(k) })
}

View File

@ -0,0 +1,135 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// +build build_qt
package qt
import (
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/therecipe/qt/core"
)
// QMLUserModel stores list of of users
type QMLUserModel struct {
core.QAbstractListModel
_ map[int]*core.QByteArray `property:"roles"`
_ int `property:"count"`
_ func() `constructor:"init"`
_ func(row int) *core.QVariant `slot:"get"`
users []*QMLUser
}
func (um *QMLUserModel) init() {
um.SetRoles(map[int]*core.QByteArray{
int(core.Qt__UserRole + 1): newQByteArrayFromString("object"),
})
um.ConnectRowCount(um.rowCount)
um.ConnectData(um.data)
um.ConnectGet(um.get)
um.users = []*QMLUser{}
um.setCount()
}
func (um *QMLUserModel) data(index *core.QModelIndex, property int) *core.QVariant {
if !index.IsValid() {
return core.NewQVariant()
}
return um.get(index.Row())
}
func (um *QMLUserModel) get(index int) *core.QVariant {
if index < 0 || index >= um.rowCount(nil) {
return core.NewQVariant()
}
return um.users[index].ToVariant()
}
func (um *QMLUserModel) rowCount(*core.QModelIndex) int {
return len(um.users)
}
func (um *QMLUserModel) setCount() {
um.SetCount(len(um.users))
}
func (um *QMLUserModel) addUser(user *QMLUser) {
um.BeginInsertRows(core.NewQModelIndex(), um.rowCount(nil), um.rowCount(nil))
um.users = append(um.users, user)
um.setCount()
um.EndInsertRows()
}
func (um *QMLUserModel) removeUser(row int) {
um.BeginRemoveRows(core.NewQModelIndex(), row, row)
um.users = append(um.users[:row], um.users[row+1:]...)
um.setCount()
um.EndRemoveRows()
}
func (um *QMLUserModel) clear() {
um.BeginRemoveRows(core.NewQModelIndex(), 0, um.rowCount(nil))
um.users = []*QMLUser{}
um.setCount()
um.EndRemoveRows()
}
func (um *QMLUserModel) indexByID(id string) int {
for i, qu := range um.users {
if id == qu.ID {
return i
}
}
return -1
}
// QMLUser holds data, slots and signals and for user.
type QMLUser struct {
core.QObject
_ string `property:"username"`
_ string `property:"avatarText"`
_ bool `property:"loggedIn"`
_ bool `property:"splitMode"`
_ bool `property:"setupGuideSeen"`
_ float32 `property:"usedBytes"`
_ float32 `property:"totalBytes"`
_ string `property:"password"`
_ []string `property:"addresses"`
_ func(makeItActive bool) `slot:"toggleSplitMode"`
_ func() `signal:"toggleSplitModeFinished"`
_ func() `slot:"logout"`
_ func(address string) `slot:"configureAppleMail"`
ID string
}
func (qu *QMLUser) update(user types.User) {
username := user.Username()
qu.SetAvatarText(getInitials(username))
qu.SetUsername(username)
qu.SetLoggedIn(user.IsConnected())
qu.SetSplitMode(!user.IsCombinedAddressMode())
qu.SetSetupGuideSeen(true)
qu.SetUsedBytes(1.0) // TODO
qu.SetTotalBytes(10000.0) // TODO
qu.SetPassword(user.GetBridgePassword())
qu.SetAddresses(user.GetAddresses())
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 KiB

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1024 1024" style="enable-background:new 0 0 1024 1024;" xml:space="preserve">
<style type="text/css">
.st0{fill:#9397CD;}
.st1{fill:#262A33;}
.st2{fill:#FFFFFF;}
</style>
<g>
<circle class="st0" cx="512.2" cy="512.1" r="512"/>
</g>
<g>
<circle class="st1" cx="850" cy="850" r="174"/>
</g>
<g>
<path class="st2" d="M784.4,773.1h90.7c15.1,0,26.7,3.7,34.8,11.2s12.1,16.8,12.1,27.8c0,9.3-2.9,17.2-8.7,23.8
c-3.8,4.4-9.5,7.9-16.9,10.5c11.3,2.7,19.5,7.4,24.9,14c5.3,6.6,8,14.9,8,24.9c0,8.1-1.9,15.4-5.7,21.9s-8.9,11.6-15.5,15.4
c-4.1,2.4-10.2,4.1-18.4,5.1c-10.9,1.4-18.1,2.1-21.7,2.1h-83.6L784.4,773.1L784.4,773.1z M833.3,834.6h21.1
c7.6,0,12.8-1.3,15.8-3.9c3-2.6,4.4-6.4,4.4-11.3c0-4.6-1.5-8.1-4.4-10.7c-3-2.6-8.1-3.8-15.5-3.8h-21.4L833.3,834.6L833.3,834.6z
M833.3,896.2H858c8.3,0,14.2-1.5,17.6-4.4c3.4-3,5.1-6.9,5.1-11.9c0-4.6-1.7-8.4-5.1-11.2c-3.4-2.8-9.3-4.2-17.8-4.2h-24.6
L833.3,896.2L833.3,896.2z"/>
</g>
<g>
<path class="st2" d="M511,263c0,0-136.3-4.5-164.4,146.7v103c0,0,1.2,11,32.2,33.4c31,22.4,111.2,85.4,132.3,85.4
c21,0,101.3-63,132.3-85.4c31-22.4,32.2-33.4,32.2-33.4v-103C647.3,258.5,511,263,511,263z M604.3,465.9H511h-93.3v-56.1
c18.9-75.1,93.3-76.1,93.3-76.1s74.4,1,93.3,76.1V465.9z"/>
<path class="st2" d="M511,654.7c0,0-21.1-2.1-37.7-13.5C456.8,629.7,346.6,551,346.6,551v155.9c0,0,0.9,18.1,20.9,18.1
s143.5,0,143.5,0s123.5,0,143.5,0s20.9-18.1,20.9-18.1V551c0,0-110.2,78.8-126.8,90.2C532.1,652.7,511,654.7,511,654.7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -349,6 +349,7 @@ func (u *User) CheckBridgeLogin(password string) error {
func (u *User) UpdateUser(ctx context.Context) error { func (u *User) UpdateUser(ctx context.Context) error {
u.lock.Lock() u.lock.Lock()
defer u.lock.Unlock() defer u.lock.Unlock()
defer u.listener.Emit(events.UserRefreshEvent, u.userID)
_, err := u.client.UpdateUser(ctx) _, err := u.client.UpdateUser(ctx)
if err != nil { if err != nil {
@ -376,6 +377,7 @@ func (u *User) SwitchAddressMode() error {
u.lock.Lock() u.lock.Lock()
defer u.lock.Unlock() defer u.lock.Unlock()
defer u.listener.Emit(events.UserRefreshEvent, u.userID)
u.CloseAllConnections() u.CloseAllConnections()
@ -414,7 +416,6 @@ func (u *User) logout() error {
if wasConnected { if wasConnected {
u.listener.Emit(events.LogoutEvent, u.userID) u.listener.Emit(events.LogoutEvent, u.userID)
u.listener.Emit(events.UserRefreshEvent, u.userID)
} }
return err return err
@ -425,6 +426,7 @@ func (u *User) logout() error {
func (u *User) Logout() error { func (u *User) Logout() error {
u.lock.Lock() u.lock.Lock()
defer u.lock.Unlock() defer u.lock.Unlock()
defer u.listener.Emit(events.UserRefreshEvent, u.userID)
u.log.Debug("Logging out user") u.log.Debug("Logging out user")

View File

@ -41,6 +41,7 @@ func TestUpdateUser(t *testing.T) {
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}), m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}),
m.credentialsStore.EXPECT().UpdateEmails("user", []string{testPMAPIAddress.Email}).Return(testCredentials, nil), m.credentialsStore.EXPECT().UpdateEmails("user", []string{testPMAPIAddress.Email}).Return(testCredentials, nil),
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"),
) )
r.NoError(t, user.UpdateUser(context.Background())) r.NoError(t, user.UpdateUser(context.Background()))
@ -68,6 +69,7 @@ func TestUserSwitchAddressMode(t *testing.T) {
m.pmapiClient.EXPECT().CountMessages(gomock.Any(), "").Return([]*pmapi.MessagesCount{}, nil), m.pmapiClient.EXPECT().CountMessages(gomock.Any(), "").Return([]*pmapi.MessagesCount{}, nil),
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}), m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}),
m.credentialsStore.EXPECT().SwitchAddressMode("user").Return(testCredentialsSplit, nil), m.credentialsStore.EXPECT().SwitchAddressMode("user").Return(testCredentialsSplit, nil),
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"),
) )
// Check switch to split mode. // Check switch to split mode.
@ -85,6 +87,7 @@ func TestUserSwitchAddressMode(t *testing.T) {
m.pmapiClient.EXPECT().CountMessages(gomock.Any(), "").Return([]*pmapi.MessagesCount{}, nil), m.pmapiClient.EXPECT().CountMessages(gomock.Any(), "").Return([]*pmapi.MessagesCount{}, nil),
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}), m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}),
m.credentialsStore.EXPECT().SwitchAddressMode("user").Return(testCredentials, nil), m.credentialsStore.EXPECT().SwitchAddressMode("user").Return(testCredentials, nil),
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"),
) )
// Check switch to combined mode. // Check switch to combined mode.
@ -105,6 +108,7 @@ func TestLogoutUser(t *testing.T) {
m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil), m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil),
m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil), m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil),
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me"), m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me"),
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"),
) )
err := user.Logout() err := user.Logout()
@ -123,6 +127,7 @@ func TestLogoutUserFailsLogout(t *testing.T) {
m.credentialsStore.EXPECT().Logout("user").Return(nil, errors.New("logout failed")), m.credentialsStore.EXPECT().Logout("user").Return(nil, errors.New("logout failed")),
m.credentialsStore.EXPECT().Delete("user").Return(nil), m.credentialsStore.EXPECT().Delete("user").Return(nil),
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me"), m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me"),
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"),
) )
err := user.Logout() err := user.Logout()

View File

@ -52,8 +52,8 @@ func TestNewUserUnlockFails(t *testing.T) {
m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil), m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil),
m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil), m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil),
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me"), m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me"),
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user"),
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"), m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"),
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user"),
) )
checkNewUserHasCredentials(m, "failed to unlock user: bad password", testCredentialsDisconnected) checkNewUserHasCredentials(m, "failed to unlock user: bad password", testCredentialsDisconnected)

View File

@ -351,6 +351,7 @@ func (u *Users) ClearData() error {
func (u *Users) DeleteUser(userID string, clearStore bool) error { func (u *Users) DeleteUser(userID string, clearStore bool) error {
u.lock.Lock() u.lock.Lock()
defer u.lock.Unlock() defer u.lock.Unlock()
defer u.events.Emit(events.UserRefreshEvent, userID)
log := log.WithField("user", userID) log := log.WithField("user", userID)

View File

@ -32,6 +32,9 @@ func TestClearData(t *testing.T) {
users := testNewUsersWithUsers(t, m) users := testNewUsersWithUsers(t, m)
defer cleanUpUsersData(users) defer cleanUpUsersData(users)
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "users")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "users@pm.me") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "users@pm.me")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "anotheruser@pm.me") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "anotheruser@pm.me")

View File

@ -38,7 +38,9 @@ func TestDeleteUser(t *testing.T) {
m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil), m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil),
m.credentialsStore.EXPECT().Delete("user").Return(nil), m.credentialsStore.EXPECT().Delete("user").Return(nil),
) )
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
err := users.DeleteUser("user", true) err := users.DeleteUser("user", true)
r.NoError(t, err) r.NoError(t, err)
@ -61,7 +63,9 @@ func TestDeleteUserWithFailingLogout(t *testing.T) {
m.credentialsStore.EXPECT().Delete("user").Return(nil), m.credentialsStore.EXPECT().Delete("user").Return(nil),
) )
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
err := users.DeleteUser("user", true) err := users.DeleteUser("user", true)
r.NoError(t, err) r.NoError(t, err)

View File

@ -91,6 +91,7 @@ func TestNewUsersWithConnectedUserWithBadToken(t *testing.T) {
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil) m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil) m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil)
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user") m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user") m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")