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?
*~
# Compiled Object files, Static and Dynamic libs (Shared Objects)
vendor
# Test files
godog.test
debug.test
@ -17,17 +14,12 @@ coverage.html
# Run files
mem.pprof
# Auto generated frontend
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).
# Auto generated
internal/**/credits.go
vendor
vendor-cache
/main.go
# Build files
/launcher-*
@ -37,18 +29,3 @@ internal/**/credits.go
/hasher
cmd/Desktop-Bridge/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
only:
- web
- branches
script:
- BUILD_TAGS="build_qa" make build
artifacts:
@ -161,6 +162,7 @@ build-darwin-qa:
extends: .build-darwin-base
only:
- web
- branches
script:
- BUILD_TAGS="build_qa" make build
artifacts:

View File

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

View File

@ -88,7 +88,7 @@ ${TGZ_TARGET}: ${DEPLOY_DIR}/${TARGET_OS}
cd ${DEPLOY_DIR}/${TARGET_OS} && tar czf ../../../../$@ .
${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 ./Changelog.md ${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}}; \
perl -i -pe"s/>${DIRNAME}/>${EXE_NAME}/g" ${EXE_TARGET}/Contents/Info.plist; \
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/
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngine.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}"
${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/
QT_BUILD_TARGET:=build desktop
@ -127,9 +127,9 @@ ${EXE_TARGET}: check-has-go gofiles ${RESOURCE_FILE} ${VENDOR_TARGET}
WINDRES_YEAR:=$(shell date +%Y)
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
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
.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-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}
PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} -c
@ -296,9 +296,7 @@ run-qml-preview:
clean-frontend-qt:
# TODO: $(MAKE) -C internal/frontend/qt -f Makefile.local clean
clean-frontend-qt-common:
# TODO: $(MAKE) -C internal/frontend/qt-common -f Makefile.local clean
$(MAKE) -C internal/frontend -f Makefile.local clean
clean-vendor: clean-frontend-qt clean-frontend-qt-common
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/sirupsen/logrus v1.7.0
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/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/vmihailenco/msgpack/v5 v5.1.3
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-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-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/go.mod h1:Uvv5cqSGCs8MTZ8sbKiCkBnaB6/OA3eq2mc77tl2VVA=
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.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-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.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
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/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.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/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
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/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
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.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
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/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
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/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
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-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-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-20190605123033-f99c8df09eb5/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-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-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-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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-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-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-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-20190502145724-3ef323f4f1fd/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-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-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-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
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/useragent"
"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/locations"
"github.com/ProtonMail/proton-bridge/internal/updater"
@ -59,6 +60,23 @@ func New(
) Frontend {
bridgeWrap := types.NewBridgeWrap(bridge)
switch frontendType {
case "qt":
return qt.New(
version,
buildVersion,
programName,
showWindowOnStart,
panicHandler,
locations,
settings,
eventListener,
updater,
userAgent,
bridgeWrap,
noEncConfirmator,
autostart,
restarter,
)
case "cli":
return cli.New(
panicHandler,

View File

@ -27,12 +27,61 @@ Item {
property ColorScheme colorScheme
property var user
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
property var _spacing: 12
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 {
anchors.fill: parent
spacing: 12
spacing: root._spacing
anchors {
top: root.top
left: root.left
right: root.rigth
leftMargin : root._leftRightMargins
rightMargin : root._leftRightMargins
topMargin : root._topBottomMargins
bottomMargin : root._topBottomMargins
}
Rectangle {
id: avatar
@ -48,8 +97,19 @@ Item {
colorScheme: root.colorScheme
anchors.fill: parent
text: root.user.avatarText.toUpperCase()
type: Label.LabelType.Body
color: root.colorScheme.text_invert
type: {
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
verticalAlignment: Qt.AlignVCenter
}
@ -63,16 +123,78 @@ Item {
spacing: 0
Label {
Layout.maximumWidth: root.width - (
root._spacing + avatar.width + 2*root._leftRightMargins
)
colorScheme: root.colorScheme
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
}
Label {
colorScheme: root.colorScheme
text: user.captionText
type: Label.LabelType.Caption
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 {
colorScheme: root.colorScheme
text: " / " + root.totalSpace
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
Item {
ScrollView {
id: root
property ColorScheme colorScheme
property var backend
property var notifications
property var user
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
clip: true
contentWidth: pane.width
contentHeight: pane.height
property int _leftRightMargins: 64
property int _topBottomMargins: 68
property int _spacing: 22
Rectangle {
anchors {
bottom: pane.bottom
}
color: root.colorScheme.background_weak
width: root.width
height: configuration.height + root._topBottomMargins
}
signal showSignIn()
signal showSetupGuide(var user, string address)
ColumnLayout {
anchors.fill: parent
spacing: 0
id: pane
Rectangle {
Layout.fillWidth: true
Layout.minimumHeight: 277
Layout.maximumHeight: 277
width: root.width
color: root.colorScheme.background_norm
ColumnLayout {
spacing: root._spacing
Layout.topMargin: root._topBottomMargins
Layout.leftMargin: root._leftRightMargins
Layout.rightMargin: root._leftRightMargins
Layout.maximumWidth: root.width - 2*root._leftRightMargins
ColumnLayout {
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}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
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
color: root.colorScheme.background_weak
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 Notification notification
property var mainWindow
topMargin: 37
leftMargin: (mainWindow.width - root.implicitWidth)/2
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
@ -74,13 +78,13 @@ Popup {
}
switch (root.notification.type) {
case Notification.NotificationType.Info:
case Notification.NotificationType.Info:
return root.colorScheme.signal_info
case Notification.NotificationType.Success:
case Notification.NotificationType.Success:
return root.colorScheme.signal_success
case Notification.NotificationType.Warning:
case Notification.NotificationType.Warning:
return root.colorScheme.signal_warning
case Notification.NotificationType.Danger:
case Notification.NotificationType.Danger:
return root.colorScheme.signal_danger
}
}
@ -109,13 +113,13 @@ Popup {
}
switch (root.notification.type) {
case Notification.NotificationType.Info:
case Notification.NotificationType.Info:
return "./icons/ic-info-circle-filled.svg"
case Notification.NotificationType.Success:
case Notification.NotificationType.Success:
return "./icons/ic-info-circle-filled.svg"
case Notification.NotificationType.Warning:
case Notification.NotificationType.Warning:
return "./icons/ic-exclamation-circle-filled.svg"
case Notification.NotificationType.Danger:
case Notification.NotificationType.Danger:
return "./icons/ic-exclamation-circle-filled.svg"
}
}
@ -145,13 +149,13 @@ Popup {
}
switch (root.notification.type) {
case Notification.NotificationType.Info:
case Notification.NotificationType.Info:
return root.colorScheme.signal_info_active
case Notification.NotificationType.Success:
case Notification.NotificationType.Success:
return root.colorScheme.signal_success_active
case Notification.NotificationType.Warning:
case Notification.NotificationType.Warning:
return root.colorScheme.signal_warning_active
case Notification.NotificationType.Danger:
case Notification.NotificationType.Danger:
return root.colorScheme.signal_danger_active
}
}
@ -183,22 +187,22 @@ Popup {
var active
switch (root.notification.type) {
case Notification.NotificationType.Info:
case Notification.NotificationType.Info:
norm = root.colorScheme.signal_info
hover = root.colorScheme.signal_info_hover
active = root.colorScheme.signal_info_active
break;
case Notification.NotificationType.Success:
case Notification.NotificationType.Success:
norm = root.colorScheme.signal_success
hover = root.colorScheme.signal_success_hover
active = root.colorScheme.signal_success_active
break;
case Notification.NotificationType.Warning:
case Notification.NotificationType.Warning:
norm = root.colorScheme.signal_warning
hover = root.colorScheme.signal_warning_hover
active = root.colorScheme.signal_warning_active
break;
case Notification.NotificationType.Danger:
case Notification.NotificationType.Danger:
norm = root.colorScheme.signal_danger
hover = root.colorScheme.signal_danger_hover
active = root.colorScheme.signal_danger_active

View File

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

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 {
Layout.fillHeight: true

View File

@ -33,8 +33,10 @@ import Notifications 1.0
Window {
id: root
width: 640
height: 480
x: 10
y: 10
width: 800
height: 600
property ColorScheme colorScheme: ProtonStyle.darkStyle
@ -103,12 +105,21 @@ Window {
QtObject {
property string username: ""
property bool loggedIn: false
property bool splitMode: false
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 password: "SMj975NnEYYsqu55GGmlpv"
property var addresses: [
"janedoe@protonmail.com",
"jane@pm.me",
"jdoe@pm.me"
]
signal loginUsernamePasswordError()
signal loginFreeUserError()
signal loginConnectionError()
@ -130,6 +141,30 @@ Window {
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: {
userSignal("loginUsernamePasswordError")
}
@ -193,6 +228,17 @@ Window {
newLoginUser.login2PasswordRequested.connect(root.login2PasswordRequested)
newLoginUser.login2PasswordError.connect(root.login2PasswordError)
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 {
text: "Log"
}
TabButton {
text: "Settings signals"
}
}
StackLayout {
@ -284,6 +334,7 @@ Window {
enabled: bridge === undefined || bridge === null
onClicked: {
bridge = bridgeComponent.createObject()
if (true) bridge._mainWindow.show()
}
}
@ -368,9 +419,8 @@ Window {
spacing: 5
Switch {
colorScheme: root.colorScheme
text: "Internet connection"
colorScheme: root.colorScheme
checked: true
onCheckedChanged: {
checked ? root.internetOn() : root.internetOff()
@ -378,115 +428,124 @@ Window {
}
Button {
colorScheme: root.colorScheme
text: "Update manual ready"
colorScheme: root.colorScheme
onClicked: {
root.updateManualReady("3.14.1592")
}
}
Button {
colorScheme: root.colorScheme
Button {
text: "Update manual done"
colorScheme: root.colorScheme
onClicked: {
root.updateManualRestartNeeded()
}
}
Button {
colorScheme: root.colorScheme
Button {
text: "Update manual error"
colorScheme: root.colorScheme
onClicked: {
root.updateManualError()
}
}
Button {
colorScheme: root.colorScheme
Button {
text: "Update force"
colorScheme: root.colorScheme
onClicked: {
root.updateForce("3.14.1592")
}
}
Button {
colorScheme: root.colorScheme
Button {
text: "Update force error"
colorScheme: root.colorScheme
onClicked: {
root.updateForceError()
}
}
Button {
colorScheme: root.colorScheme
text: "Update silent done"
colorScheme: root.colorScheme
onClicked: {
root.updateSilentRestartNeeded()
}
}
Button {
text: "Update silent error"
colorScheme: root.colorScheme
text: "Update solent error"
onClicked: {
root.updateSilentError()
}
}
Button {
text: "Update is latest version"
colorScheme: root.colorScheme
text: "Bug report send OK"
onClicked: {
root.updateIsLatestVersion()
}
}
Button {
text: "Bug report send OK"
colorScheme: root.colorScheme
onClicked: {
root.reportBugFinished()
root.bugReportSendSuccess()
}
}
Button {
colorScheme: root.colorScheme
text: "Bug report send error"
colorScheme: root.colorScheme
onClicked: {
root.reportBugFinished()
root.bugReportSendError()
}
}
Button {
colorScheme: root.colorScheme
text: "Cache anavailable"
colorScheme: root.colorScheme
onClicked: {
root.cacheAnavailable()
root.cacheUnavailable()
}
}
Button {
colorScheme: root.colorScheme
Button {
text: "Cache can't move"
colorScheme: root.colorScheme
onClicked: {
root.cacheCantMove()
}
}
Button {
text: "Cache location change success"
onClicked: {
root.cacheLocationChangeSuccess()
}
colorScheme: root.colorScheme
}
Button {
text: "Disk full"
colorScheme: root.colorScheme
onClicked: {
root.diskFull()
}
}
}
}
TextArea {
colorScheme: root.colorScheme
id: logTextArea
colorScheme: root.colorScheme
Layout.fillHeight: true
Layout.fillWidth: true
@ -496,20 +555,90 @@ Window {
textFormat: TextEdit.RichText
//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 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)
signal loginUsernamePasswordError()
signal loginFreeUserError()
signal loginConnectionError()
signal loginUsernamePasswordError(string errorMsg)
signal loginFreeUserError(string errorMsg)
signal loginConnectionError(string errorMsg)
signal login2FARequested()
signal login2FAError()
signal login2FAErrorAbort()
signal login2FAError(string errorMsg)
signal login2FAErrorAbort(string errorMsg)
signal login2PasswordRequested()
signal login2PasswordError()
signal login2PasswordErrorAbort()
signal login2PasswordError(string errorMsg)
signal login2PasswordErrorAbort(string errorMsg)
signal loginFinished()
signal internetOff()
signal internetOn()
@ -521,14 +650,140 @@ Window {
signal updateForceError()
signal updateSilentRestartNeeded()
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 bugReportSendError()
signal cacheAnavailable()
signal cacheCantMove()
property var availableKeychain: ["gnome-keyring", "pass"]
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: {
console.debug("<- loginUsernamePasswordError")
@ -557,6 +812,9 @@ Window {
onLogin2PasswordErrorAbort: {
console.debug("<- login2PasswordErrorAbort")
}
onLoginFinished: {
console.debug("<- loginFinished")
}
onInternetOff: {
console.debug("<- internetOff")
@ -571,30 +829,6 @@ Window {
Bridge {
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 var backend
property var notifications
signal login(string username, string password)
signal login2FA(string username, string code)
signal login2Password(string username, string password)
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 {
anchors.fill: parent
spacing: 0
@ -91,6 +105,8 @@ Item {
horizontalPadding: 0
icon.source: "./icons/ic-question-circle.svg"
onClicked: rightContent.showHelpView()
}
Button {
@ -109,10 +125,14 @@ Item {
horizontalPadding: 0
icon.source: "./icons/ic-cog-wheel.svg"
onClicked: rightContent.showGeneralSettings()
}
}
// Separator
Item {implicitHeight:10}
// Separator line
Rectangle {
Layout.fillWidth: true
Layout.minimumHeight: 1
@ -122,14 +142,20 @@ Item {
ListView {
id: accounts
property var _topBottomMargins: 24
property var _leftRightMargins: 16
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 24
Layout.bottomMargin: 24
Layout.leftMargin: accounts._leftRightMargins
Layout.rightMargin: accounts._leftRightMargins
Layout.topMargin: accounts._topBottomMargins
Layout.bottomMargin: accounts._topBottomMargins
spacing: 12
clip: true
boundsBehavior: Flickable.StopAtBounds
header: Rectangle {
height: headerLabel.height+16
@ -142,11 +168,28 @@ Item {
}
}
highlight: Rectangle {
color: leftBar.colorScheme.interaction_default_active
radius: 4
}
model: root.backend.users
delegate: AccountDelegate{
width: leftBar.width - 2*accounts._leftRightMargins
id: accountDelegate
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"
onClicked: root.showSignIn()
onClicked: {
signIn.username = ""
rightContent.showSignIn()
}
}
}
}
}
Rectangle {
id: rightPlane
Rectangle { // right content background
Layout.fillWidth: true
Layout.fillHeight: true
@ -199,14 +243,44 @@ Item {
id: rightContent
anchors.fill: parent
AccountView {
AccountView { // 0
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 {
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
}
SignIn {
id: signIn
Layout.topMargin: 68
Layout.leftMargin: 80
Layout.leftMargin: 80 - backButton.width - 18
Layout.rightMargin: 80
Layout.bottomMargin: 68
Layout.preferredWidth: 320
@ -214,21 +288,70 @@ Item {
Layout.fillHeight: true
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
onLogin : { root.login ( username , password ) }
onLogin2FA : { root.login2FA ( username , code ) }
onLogin2Password : { root.login2Password ( username , password ) }
onLoginAbort : { root.loginAbort ( username ) }
onLogin : { root.backend.login ( username , password ) }
onLogin2FA : { root.backend.login2FA ( username , code ) }
onLogin2Password : { root.backend.login2Password ( username , password ) }
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 showSignIn() {
rightContent.currentIndex = 1
function showLocalCacheSettings(){rightContent.showLocalCacheSettings() }
function showSettings(){rightContent.showGeneralSettings() }
function showHelp(){rightContent.showHelpView() }
function showSignIn(username){
signIn.username = username
rightContent.showSignIn()
}
}

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

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

View File

@ -28,6 +28,7 @@ Item {
property ColorScheme colorScheme
property var notifications
property var mainWindow
property int notificationWhitelist: NotificationFilter.FilterConsts.All
property int notificationBlacklist: NotificationFilter.FilterConsts.None
@ -42,6 +43,7 @@ Item {
Banner {
colorScheme: root.colorScheme
notification: bannerNotificationFilter.topmost
mainWindow: root.mainWindow
}
NotificationDialog {
@ -66,17 +68,17 @@ Item {
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.bugReportSendSuccess
notification: root.notifications.disableBeta
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.bugReportSendError
notification: root.notifications.enableBeta
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.cacheAnavailable
notification: root.notifications.cacheUnavailable
}
NotificationDialog {
@ -88,4 +90,24 @@ Item {
colorScheme: root.colorScheme
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 active: false
property bool loading: false
readonly property var occurred: active ? new Date() : undefined
property var data

View File

@ -29,6 +29,13 @@ QtObject {
property StatusWindow frontendStatus
property SystemTrayIcon frontendTray
signal askDisableBeta()
signal askEnableBeta()
signal askEnableSplitMode(var user)
signal askDisableLocalCache()
signal askEnableLocalCache(var path)
signal askResetBridge()
enum Group {
Connection = 1,
Update = 2,
@ -48,12 +55,20 @@ QtObject {
root.updateForceError,
root.updateSilentRestartNeeded,
root.updateSilentError,
root.updateIsLatestVersion,
root.disableBeta,
root.enableBeta,
root.bugReportSendSuccess,
root.bugReportSendError,
root.cacheAnavailable,
root.cacheUnavailable,
root.cacheCantMove,
root.accountChanged,
root.diskFull
root.diskFull,
root.cacheLocationChangeSuccess,
root.enableSplitMode,
root.disableLocalCache,
root.enableLocalCache,
root.resetBridge
]
// Connection
@ -93,10 +108,18 @@ QtObject {
action: [
Action {
text: qsTr("Update")
text: qsTr("Install update")
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
}
},
@ -104,7 +127,6 @@ QtObject {
text: qsTr("Remind me later")
onTriggered: {
// TODO: start timer here
root.updateManualReady.active = false
}
}
@ -128,14 +150,14 @@ QtObject {
text: qsTr("Restart Bridge")
onTriggered: {
// TODO
root.backend.restart()
root.updateManualRestartNeeded.active = false
}
}
}
property Notification updateManualError: Notification {
text: qsTr("Bridge couldnt update")
text: qsTr("Bridge couldnt update. Please update manually.")
icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Warning
group: Notifications.Group.Update
@ -147,19 +169,28 @@ QtObject {
}
}
action: Action {
text: qsTr("Update manually")
action: [
Action {
text: qsTr("Update manually")
onTriggered: {
// TODO
root.updateManualError.active = false
onTriggered: {
Qt.openUrlExternally(root.backend.getLandingPage())
root.updateManualError.active = false
}
},
Action {
text: qsTr("Remind me later")
onTriggered: {
root.updateManualReady.active = false
}
}
}
]
}
property Notification updateForce: Notification {
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"
type: Notification.NotificationType.Danger
group: Notifications.Group.Update | Notifications.Group.Dialogs
@ -175,18 +206,26 @@ QtObject {
action: [
Action {
text: qsTr("Update")
text: qsTr("Install update")
onTriggered: {
// TODO: trigger update here
root.backend.installUpdate()
root.updateForce.active = false
}
},
Action {
text: qsTr("Quite Bridge")
text: qsTr("Update manually")
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
}
}
@ -195,7 +234,7 @@ QtObject {
property Notification updateForceError: Notification {
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"
type: Notification.NotificationType.Danger
group: Notifications.Group.Update | Notifications.Group.Dialogs
@ -213,15 +252,15 @@ QtObject {
text: qsTr("Update manually")
onTriggered: {
// TODO: trigger update here
Qt.openUrlExternally(root.backend.getLandingPage())
root.updateForceError.active = false
}
},
Action {
text: qsTr("Quite Bridge")
text: qsTr("Quit Bridge")
onTriggered: {
// TODO: quit Bridge here
root.backend.quit()
root.updateForce.active = false
}
}
@ -245,7 +284,7 @@ QtObject {
text: qsTr("Restart Bridge")
onTriggered: {
// TODO
root.backend.restart()
root.updateSilentRestartNeeded.active = false
}
}
@ -268,18 +307,105 @@ QtObject {
text: qsTr("Update manually")
onTriggered: {
// TODO
Qt.openUrlExternally(root.backend.getLandingPage())
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
property Notification bugReportSendSuccess: Notification {
text: qsTr("Bug report sent")
description: qsTr("Weve received your report, thank you! Our team will get back to you as soon as we can.")
text: qsTr("Thank you for the report. We'll get back to you as soon as we can.")
icon: "./icons/ic-info-circle-filled.svg"
type: Notification.NotificationType.Success
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
group: Notifications.Group.Configuration
Connections {
target: root.backend
@ -302,10 +428,10 @@ QtObject {
}
property Notification bugReportSendError: Notification {
text: qsTr("There was a problem")
description: qsTr("There was a problem with sending your report. Please try again later or contact us directly at security@protonmail.com")
type: Notification.NotificationType.Warning
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
text: qsTr("Report could not be sent. Try again or email us directly.")
icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger
group: Notifications.Group.Configuration
Connections {
target: root.backend
@ -323,7 +449,7 @@ QtObject {
}
// Cache
property Notification cacheAnavailable: Notification {
property Notification cacheUnavailable: Notification {
text: qsTr("Cache location is unavailable")
description: qsTr("Check the directory or change it in your settings.")
type: Notification.NotificationType.Warning
@ -331,8 +457,8 @@ QtObject {
Connections {
target: root.backend
onCacheAnavailable: {
root.cacheAnavailable.active = true
onCacheUnavailable: {
root.cacheUnavailable.active = true
}
}
@ -340,13 +466,15 @@ QtObject {
Action {
text: qsTr("Quit Bridge")
onTriggered: {
root.cacheAnavailable.active = false
root.backend.quit()
root.cacheUnavailable.active = false
}
},
Action {
text: qsTr("Change location")
onTriggered: {
root.cacheAnavailable.active = false
root.cacheUnavailable.active = false
root.frontendMain.showLocalCacheSettings()
}
}
]
@ -376,6 +504,31 @@ QtObject {
text: qsTr("Change location")
onTriggered: {
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 {
text: qsTr("Quit Bridge")
onTriggered: {
root.backend.quit()
root.diskFull.active = false
}
},
@ -421,6 +575,171 @@ QtObject {
text: qsTr("Settings")
onTriggered: {
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
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 {
id: root
color: Style.transparent
color: "transparent"
property color fillColor : Style.currentStyle.background_norm
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.impl 2.12
import QtQuick.Templates 2.12 as T
import "."
Item {
id: root
@ -86,9 +87,10 @@ Item {
property alias wrapMode: control.wrapMode
implicitWidth: background.width
implicitHeight: control.implicitHeight +
Math.max(label.implicitHeight + label.anchors.topMargin + label.anchors.bottomMargin, hint.implicitHeight + hint.anchors.topMargin + hint.anchors.bottomMargin) +
assistiveText.implicitHeight
implicitHeight: control.implicitHeight + Math.max(
label.implicitHeight + label.anchors.topMargin + label.anchors.bottomMargin,
hint.implicitHeight + hint.anchors.topMargin + hint.anchors.bottomMargin
) + assistiveText.implicitHeight
property alias label: label.text
property alias hint: hint.text
@ -96,6 +98,8 @@ Item {
property bool error: false
signal editingFinished()
// Backgroud is moved away from within control as it will be clipped with scrollview
Rectangle {
id: background
@ -200,12 +204,16 @@ Item {
T.TextArea {
id: control
implicitWidth: Math.max(contentWidth + leftPadding + rightPadding,
implicitBackgroundWidth + leftInset + rightInset,
placeholder.implicitWidth + leftPadding + rightPadding)
implicitHeight: Math.max(contentHeight + topPadding + bottomPadding,
implicitBackgroundHeight + topInset + bottomInset,
placeholder.implicitHeight + topPadding + bottomPadding)
implicitWidth: Math.max(
contentWidth + leftPadding + rightPadding,
implicitBackgroundWidth + leftInset + rightInset,
placeholder.implicitWidth + leftPadding + rightPadding
)
implicitHeight: Math.max(
contentHeight + topPadding + bottomPadding,
implicitBackgroundHeight + topInset + bottomInset,
placeholder.implicitHeight + topPadding + bottomPadding
)
padding: 8
leftPadding: 12
@ -216,6 +224,8 @@ Item {
selectionColor: control.palette.highlight
selectedTextColor: control.palette.highlightedText
onEditingFinished: root.editingFinished()
cursorDelegate: Rectangle {
id: cursor
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
TextArea 4.0 TextArea.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 user
property string address
signal dismissed()
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
RowLayout {
anchors.fill: parent
spacing: 0
@ -56,7 +58,7 @@ Item {
Label {
colorScheme: root.colorScheme
text: user ? user.username : ""
text: address
color: root.colorScheme.text_weak
type: Label.LabelType.Lead
}
@ -80,30 +82,50 @@ Item {
Repeater {
model: clients
ColumnLayout {
RowLayout {
Layout.topMargin: 12
Layout.bottomMargin: 12
Layout.leftMargin: 16
Layout.rightMargin: 16
Rectangle {
implicitWidth: clientRow.width
implicitHeight: clientRow.height
ColorImage {
source: model.iconSource
height: 36
ColumnLayout {
id: clientRow
RowLayout {
Layout.topMargin: 12
Layout.bottomMargin: 12
Layout.leftMargin: 16
Layout.rightMargin: 16
ColorImage {
source: model.iconSource
height: 36
}
Label {
colorScheme: root.colorScheme
Layout.leftMargin: 12
text: model.name
type: Label.LabelType.Body
}
}
Label {
colorScheme: root.colorScheme
Layout.leftMargin: 12
text: model.name
type: Label.LabelType.Body
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: root.colorScheme.border_weak
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
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()
}
}
}
}

View File

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

View File

@ -22,20 +22,16 @@ import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13
import Proton 4.0
import ProtonBackend 1.0
import Notifications 1.0
// Because of https://bugreports.qt.io/browse/QTBUG-69777 and other bugs alike it is impossible
// to use Window with flags: Qt.Popup here since it won't close by it's own on click outside.
PopupWindow {
Window {
id: root
title: "ProtonMail Bridge"
height: contentLayout.implicitHeight
width: contentLayout.implicitWidth
minimumHeight: 201
minimumWidth: 448
flags: Qt.FramelessWindowHint
property ColorScheme colorScheme: ProtonStyle.currentStyle
@ -47,15 +43,19 @@ PopupWindow {
signal showMainWindow()
signal showHelp()
signal showSettings()
signal showSignIn(string username)
signal quit()
ColumnLayout {
id: contentLayout
Layout.minimumHeight: 201
anchors.fill: parent
spacing: 0
ColumnLayout {
Layout.minimumWidth: 448
Layout.fillWidth: true
spacing: 0
@ -76,13 +76,13 @@ PopupWindow {
}
switch (statusItem.activeNotification.type) {
case Notification.NotificationType.Danger:
case Notification.NotificationType.Danger:
return root.colorScheme.signal_danger
case Notification.NotificationType.Warning:
case Notification.NotificationType.Warning:
return root.colorScheme.signal_warning
case Notification.NotificationType.Success:
case Notification.NotificationType.Success:
return root.colorScheme.signal_success
case Notification.NotificationType.Info:
case Notification.NotificationType.Info:
return root.colorScheme.signal_info
}
}
@ -149,8 +149,8 @@ PopupWindow {
Layout.fillHeight: true
Layout.maximumHeight: accountListView.count ?
accountListView.contentHeight / accountListView.count * 3 + accountListView.anchors.topMargin + accountListView.anchors.bottomMargin :
Number.POSITIVE_INFINITY
accountListView.contentHeight / accountListView.count * 3 + accountListView.anchors.topMargin + accountListView.anchors.bottomMargin :
Number.POSITIVE_INFINITY
color: root.colorScheme.background_norm
clip: true
@ -171,13 +171,17 @@ PopupWindow {
interactive: contentHeight > parent.height
snapMode: ListView.SnapToItem
boundsBehavior: Flickable.StopAtBounds
delegate: Item {
id: viewItem
width: ListView.view.width
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
property var user: root.backend.users.get(index)
RowLayout {
spacing: 0
anchors.fill: parent
@ -187,15 +191,19 @@ PopupWindow {
Layout.margins: 12
user: modelData
user: viewItem.user
colorScheme: root.colorScheme
}
Button {
Layout.margins: 12
colorScheme: root.colorScheme
visible: true
text: "test"
visible: !viewItem.user.loggedIn
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)
}
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
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 {
u.lock.Lock()
defer u.lock.Unlock()
defer u.listener.Emit(events.UserRefreshEvent, u.userID)
_, err := u.client.UpdateUser(ctx)
if err != nil {
@ -376,6 +377,7 @@ func (u *User) SwitchAddressMode() error {
u.lock.Lock()
defer u.lock.Unlock()
defer u.listener.Emit(events.UserRefreshEvent, u.userID)
u.CloseAllConnections()
@ -414,7 +416,6 @@ func (u *User) logout() error {
if wasConnected {
u.listener.Emit(events.LogoutEvent, u.userID)
u.listener.Emit(events.UserRefreshEvent, u.userID)
}
return err
@ -425,6 +426,7 @@ func (u *User) logout() error {
func (u *User) Logout() error {
u.lock.Lock()
defer u.lock.Unlock()
defer u.listener.Emit(events.UserRefreshEvent, u.userID)
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.credentialsStore.EXPECT().UpdateEmails("user", []string{testPMAPIAddress.Email}).Return(testCredentials, nil),
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"),
)
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().Addresses().Return([]*pmapi.Address{testPMAPIAddress}),
m.credentialsStore.EXPECT().SwitchAddressMode("user").Return(testCredentialsSplit, nil),
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"),
)
// 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().Addresses().Return([]*pmapi.Address{testPMAPIAddress}),
m.credentialsStore.EXPECT().SwitchAddressMode("user").Return(testCredentials, nil),
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"),
)
// Check switch to combined mode.
@ -105,6 +108,7 @@ func TestLogoutUser(t *testing.T) {
m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil),
m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil),
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me"),
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"),
)
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().Delete("user").Return(nil),
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me"),
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"),
)
err := user.Logout()

View File

@ -52,8 +52,8 @@ func TestNewUserUnlockFails(t *testing.T) {
m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil),
m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil),
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.LogoutEvent, "user"),
)
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 {
u.lock.Lock()
defer u.lock.Unlock()
defer u.events.Emit(events.UserRefreshEvent, userID)
log := log.WithField("user", userID)

View File

@ -32,6 +32,9 @@ func TestClearData(t *testing.T) {
users := testNewUsersWithUsers(t, m)
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, "users@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().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.UserRefreshEvent, "user")
err := users.DeleteUser("user", true)
r.NoError(t, err)
@ -61,7 +63,9 @@ func TestDeleteUserWithFailingLogout(t *testing.T) {
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.UserRefreshEvent, "user")
err := users.DeleteUser("user", true)
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().Logout("user").Return(testCredentialsDisconnected, nil)
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")