mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-17 23:56:56 +00:00
Shared GUI for Bridge and Import/Export
This commit is contained in:
60
internal/frontend/qt-ie/Makefile.local
Normal file
60
internal/frontend/qt-ie/Makefile.local
Normal file
@ -0,0 +1,60 @@
|
||||
QMLfiles=$(shell find ../qml/ -name "*.qml") $(shell find ../qml/ -name "qmldir")
|
||||
FontAwesome=${CURDIR}/../share/fontawesome-webfont.ttf
|
||||
ImageDir=${CURDIR}/../share/icons
|
||||
Icons=$(shell find ${ImageDir} -name "*.png")
|
||||
Icons+= share/images/folder_open.png share/images/envelope_open.png
|
||||
MocDependencies= ./ui.go ./account_model.go ./folder_structure.go ./folder_functions.go
|
||||
## EnumDependecies= ../backend/errors/errors.go ../backend/progress.go ../backend/source/enum.go ../frontend/enums.go
|
||||
|
||||
all: ../qml/ImportExportUI/images moc.go ../qml/GuiIE.qml qmlcheck rcc.cpp
|
||||
|
||||
## ./qml/GuiIE.qml: enums.sh ${EnumDependecies}
|
||||
## ./enums.sh
|
||||
|
||||
../qml/ProtonUI/fontawesome.ttf:
|
||||
ln -sf ${FontAwesome} $@
|
||||
../qml/ProtonUI/images:
|
||||
ln -sf ${ImageDir} $@
|
||||
../qml/ImportExportUI/images:
|
||||
ln -sf ${ImageDir} $@
|
||||
|
||||
translate.ts: ${QMLfiles}
|
||||
lupdate -recursive qml/ -ts $@
|
||||
|
||||
rcc.cpp: ${QMLfiles} ${Icons} resources.qrc
|
||||
rm -f rcc.cpp rcc.qrc && qtrcc -o .
|
||||
|
||||
|
||||
qmltest:
|
||||
qmltestrunner -eventdelay 500 -import ../qml/
|
||||
qmlcheck: ../qml/ProtonUI/fontawesome.ttf ../qml/ImportExportUI/images ../qml/ProtonUI/images
|
||||
qmlscene -verbose -I ../qml/ -f ../qml/tst_GuiIE.qml --quit
|
||||
qmlpreview: ../qml/ProtonUI/fontawesome.ttf ../qml/ImportExportUI/images ../qml/ProtonUI/images
|
||||
rm -f ../qml/*.qmlc ../qml/ProtonUI/*.qmlc ../qml/ImportExportUI/*.qmlc
|
||||
qmlscene -verbose -I ../qml/ -f ../qml/tst_GuiIE.qml 2>&1
|
||||
|
||||
test: qmlcheck moc.go rcc.cpp
|
||||
go test -v
|
||||
|
||||
moc.go: ${MocDependencies}
|
||||
qtmoc
|
||||
|
||||
clean:
|
||||
rm -rf linux/
|
||||
rm -rf darwin/
|
||||
rm -rf windows/
|
||||
rm -rf deploy/
|
||||
rm -f moc.cpp
|
||||
rm -f moc.go
|
||||
rm -f moc.h
|
||||
rm -f moc_cgo*.go
|
||||
rm -f moc_moc.h
|
||||
rm -f rcc.cpp
|
||||
rm -f rcc.qrc
|
||||
rm -f rcc_cgo*.go
|
||||
rm -f ../rcc.cpp
|
||||
rm -f ../rcc.qrc
|
||||
rm -f ../rcc_cgo*.go
|
||||
rm -rf ../qml/ProtonUI/images
|
||||
rm -f ../qml/ProtonUI/fontawesome.ttf
|
||||
find ../qml -name *.qmlc -exec rm {} \;
|
||||
55
internal/frontend/qt-ie/README.md
Normal file
55
internal/frontend/qt-ie/README.md
Normal file
@ -0,0 +1,55 @@
|
||||
# ProtonMail Import-Export Qt interface
|
||||
Import-Export uses [Qt](https://www.qt.io) framework for creating appealing graphical
|
||||
user interface. Package [therecipe/qt](https://github.com/therecipe/qt) is used
|
||||
to implement Qt into [Go](https://www.goglang.com).
|
||||
|
||||
|
||||
# For developers
|
||||
The GUI is designed inside QML files. Communication with backend is done via
|
||||
[frontend.go](./frontend.go). The API documentation is done via `go-doc`.
|
||||
|
||||
## Setup
|
||||
* if you don't have the system wide `go-1.8.1` download, install localy (e.g.
|
||||
`~/build/go-1.8.1`) and setup:
|
||||
|
||||
export GOROOT=~/build/go-1.8.1/go
|
||||
export PATH=$GOROOT/bin:$PATH
|
||||
|
||||
* go to your working directory and export `$GOPATH`
|
||||
|
||||
export GOPATH=`Pwd`
|
||||
mkdir -p $GOPATH/bin
|
||||
export PATH=$PATH:$GOPATH/bin
|
||||
|
||||
|
||||
* if you dont have system wide `Qt-5.8.0`
|
||||
[download](https://download.qt.io/official_releases/qt/5.8/5.8.0/qt-opensource-linux-x64-5.8.0.run),
|
||||
install locally (e.g. `~/build/qt/qt-5.8.0`) and setup:
|
||||
|
||||
export QT_DIR=~/build/qt/qt-5.8.0
|
||||
export PATH=$QT_DIR/5.8/gcc_64/bin:$PATH
|
||||
|
||||
* `Go-Qt` setup (installation is system dependent see
|
||||
[therecipe/qt/README](https://github.com/therecipe/qt/blob/master/README.md)
|
||||
for details)
|
||||
|
||||
go get -u -v github.com/therecipe/qt/cmd/...
|
||||
$GOPATH/bin/qtsetup
|
||||
|
||||
## Compile
|
||||
* it is necessary to compile the Qt-C++ with go for resources and meta-objects
|
||||
|
||||
make -f Makefile.local
|
||||
|
||||
* FIXME the rcc file is implicitly generated with `package main`. This needs to
|
||||
be changed to `package qtie` manually
|
||||
* check that user interface is working
|
||||
|
||||
make -f Makefile.local test
|
||||
|
||||
## Test
|
||||
|
||||
make -f Makefile.local qmlpreview
|
||||
|
||||
## Deploy
|
||||
* before compilation of Import-Export it is necessary to run compilation of Qt-C++ part (done in makefile)
|
||||
68
internal/frontend/qt-ie/enums.go
Normal file
68
internal/frontend/qt-ie/enums.go
Normal file
@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2020 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 !nogui
|
||||
|
||||
package qtie
|
||||
|
||||
import (
|
||||
"github.com/therecipe/qt/core"
|
||||
)
|
||||
|
||||
// Folder Type
|
||||
const (
|
||||
FolderTypeSystem = ""
|
||||
FolderTypeLabel = "label"
|
||||
FolderTypeFolder = "folder"
|
||||
FolderTypeExternal = "external"
|
||||
)
|
||||
|
||||
// Status
|
||||
const (
|
||||
StatusNoInternet = "noInternet"
|
||||
StatusCheckingInternet = "internetCheck"
|
||||
StatusNewVersionAvailable = "oldVersion"
|
||||
StatusUpToDate = "upToDate"
|
||||
StatusForceUpdate = "forceupdate"
|
||||
)
|
||||
|
||||
// Constants for data map
|
||||
const (
|
||||
// Account info
|
||||
Account = int(core.Qt__UserRole) + 1<<iota
|
||||
Status
|
||||
Password
|
||||
Aliases
|
||||
IsExpanded
|
||||
// Folder info
|
||||
FolderId
|
||||
FolderName
|
||||
FolderColor
|
||||
FolderType
|
||||
FolderEntries
|
||||
IsFolderSelected
|
||||
FolderFromDate
|
||||
FolderToDate
|
||||
TargetFolderID
|
||||
TargetLabelIDs
|
||||
// Error list
|
||||
MailSubject
|
||||
MailDate
|
||||
MailFrom
|
||||
InputFolder
|
||||
ErrorMessage
|
||||
)
|
||||
129
internal/frontend/qt-ie/error_list.go
Normal file
129
internal/frontend/qt-ie/error_list.go
Normal file
@ -0,0 +1,129 @@
|
||||
// Copyright (c) 2020 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 !nogui
|
||||
|
||||
package qtie
|
||||
|
||||
import (
|
||||
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
|
||||
"github.com/therecipe/qt/core"
|
||||
)
|
||||
|
||||
// ErrorDetail stores information about email and error
|
||||
type ErrorDetail struct {
|
||||
MailSubject, MailDate, MailFrom, InputFolder, ErrorMessage string
|
||||
}
|
||||
|
||||
func init() {
|
||||
ErrorListModel_QRegisterMetaType()
|
||||
}
|
||||
|
||||
// ErrorListModel to sending error details to Qt
|
||||
type ErrorListModel struct {
|
||||
core.QAbstractListModel
|
||||
|
||||
// Qt list model
|
||||
_ func() `constructor:"init"`
|
||||
_ map[int]*core.QByteArray `property:"roles"`
|
||||
_ int `property:"count"`
|
||||
|
||||
Details []*ErrorDetail
|
||||
}
|
||||
|
||||
func (s *ErrorListModel) init() {
|
||||
s.SetRoles(map[int]*core.QByteArray{
|
||||
MailSubject: qtcommon.NewQByteArrayFromString("mailSubject"),
|
||||
MailDate: qtcommon.NewQByteArrayFromString("mailDate"),
|
||||
MailFrom: qtcommon.NewQByteArrayFromString("mailFrom"),
|
||||
InputFolder: qtcommon.NewQByteArrayFromString("inputFolder"),
|
||||
ErrorMessage: qtcommon.NewQByteArrayFromString("errorMessage"),
|
||||
})
|
||||
// basic QAbstractListModel mehods
|
||||
s.ConnectData(s.data)
|
||||
s.ConnectRowCount(s.rowCount)
|
||||
s.ConnectColumnCount(s.columnCount)
|
||||
s.ConnectRoleNames(s.roleNames)
|
||||
}
|
||||
|
||||
func (s *ErrorListModel) data(index *core.QModelIndex, role int) *core.QVariant {
|
||||
if !index.IsValid() {
|
||||
return core.NewQVariant()
|
||||
}
|
||||
|
||||
if index.Row() >= len(s.Details) {
|
||||
return core.NewQVariant()
|
||||
}
|
||||
|
||||
var p = s.Details[index.Row()]
|
||||
|
||||
switch role {
|
||||
case MailSubject:
|
||||
return qtcommon.NewQVariantString(p.MailSubject)
|
||||
case MailDate:
|
||||
return qtcommon.NewQVariantString(p.MailDate)
|
||||
case MailFrom:
|
||||
return qtcommon.NewQVariantString(p.MailFrom)
|
||||
case InputFolder:
|
||||
return qtcommon.NewQVariantString(p.InputFolder)
|
||||
case ErrorMessage:
|
||||
return qtcommon.NewQVariantString(p.ErrorMessage)
|
||||
default:
|
||||
return core.NewQVariant()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ErrorListModel) rowCount(parent *core.QModelIndex) int { return len(s.Details) }
|
||||
func (s *ErrorListModel) columnCount(parent *core.QModelIndex) int { return 1 }
|
||||
func (s *ErrorListModel) roleNames() map[int]*core.QByteArray { return s.Roles() }
|
||||
|
||||
// Add more errors to list
|
||||
func (s *ErrorListModel) Add(more []*ErrorDetail) {
|
||||
s.BeginInsertRows(core.NewQModelIndex(), len(s.Details), len(s.Details))
|
||||
s.Details = append(s.Details, more...)
|
||||
s.SetCount(len(s.Details))
|
||||
s.EndInsertRows()
|
||||
}
|
||||
|
||||
// Clear removes all items in model
|
||||
func (s *ErrorListModel) Clear() {
|
||||
s.BeginRemoveRows(core.NewQModelIndex(), 0, len(s.Details))
|
||||
s.Details = s.Details[0:0]
|
||||
s.SetCount(len(s.Details))
|
||||
s.EndRemoveRows()
|
||||
}
|
||||
|
||||
func (s *ErrorListModel) load(importLogFileName string) {
|
||||
/*
|
||||
err := backend.LoopDetailsInFile(importLogFileName, func(d *backend.MessageDetails) {
|
||||
if d.MessageID != "" { // imported ok
|
||||
return
|
||||
}
|
||||
ed := &ErrorDetail{
|
||||
MailSubject: d.Subject,
|
||||
MailDate: d.Time,
|
||||
MailFrom: d.From,
|
||||
InputFolder: d.Folder,
|
||||
ErrorMessage: d.Error,
|
||||
}
|
||||
s.Add([]*ErrorDetail{ed})
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("load import report from %q: %v", importLogFileName, err)
|
||||
}
|
||||
*/
|
||||
}
|
||||
125
internal/frontend/qt-ie/export.go
Normal file
125
internal/frontend/qt-ie/export.go
Normal file
@ -0,0 +1,125 @@
|
||||
// Copyright (c) 2020 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 !nogui
|
||||
|
||||
package qtie
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/transfer"
|
||||
)
|
||||
|
||||
const (
|
||||
TypeEML = "EML"
|
||||
TypeMBOX = "MBOX"
|
||||
)
|
||||
|
||||
func (f *FrontendQt) LoadStructureForExport(addressOrID string) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
f.showError(err)
|
||||
f.Qml.ExportStructureLoadFinished(false)
|
||||
} else {
|
||||
f.Qml.ExportStructureLoadFinished(true)
|
||||
}
|
||||
}()
|
||||
|
||||
if f.transfer, err = f.ie.GetEMLExporter(addressOrID, ""); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.PMStructure.Clear()
|
||||
sourceMailboxes, err := f.transfer.SourceMailboxes()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, mbox := range sourceMailboxes {
|
||||
rule := f.transfer.GetRule(mbox)
|
||||
f.PMStructure.addEntry(newFolderInfo(mbox, rule))
|
||||
}
|
||||
|
||||
f.PMStructure.transfer = f.transfer
|
||||
}
|
||||
|
||||
func (f *FrontendQt) StartExport(rootPath, login, fileType string, attachEncryptedBody bool) {
|
||||
var target transfer.TargetProvider
|
||||
if fileType == TypeEML {
|
||||
target = transfer.NewEMLProvider(rootPath)
|
||||
} else if fileType == TypeMBOX {
|
||||
|
||||
target = transfer.NewMBOXProvider(rootPath)
|
||||
} else {
|
||||
log.Errorln("Wrong file format:", fileType)
|
||||
return
|
||||
}
|
||||
f.transfer.ChangeTarget(target)
|
||||
f.transfer.SetSkipEncryptedMessages(!attachEncryptedBody)
|
||||
progress := f.transfer.Start()
|
||||
f.setProgressManager(progress)
|
||||
|
||||
/*
|
||||
TODO
|
||||
f.Qml.SetProgress(0.0)
|
||||
f.Qml.SetProgressDescription(backend.ProgressInit)
|
||||
f.Qml.SetTotal(0)
|
||||
|
||||
settings := backend.ExportSettings{
|
||||
FilePath: fpath,
|
||||
Login: login,
|
||||
AttachEncryptedBody: attachEncryptedBody,
|
||||
DateBegin: 0,
|
||||
DateEnd: 0,
|
||||
Labels: make(map[string]string),
|
||||
}
|
||||
|
||||
if fileType == "EML" {
|
||||
settings.FileTypeID = backend.EMLFormat
|
||||
} else if fileType == "MBOX" {
|
||||
settings.FileTypeID = backend.MBOXFormat
|
||||
} else {
|
||||
log.Errorln("Wrong file format:", fileType)
|
||||
return
|
||||
}
|
||||
|
||||
username, _, err := backend.ExtractUsername(login)
|
||||
if err != nil {
|
||||
log.Error("qtfrontend: cannot retrieve username from alias: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
settings.User, err = backend.ExtractCurrentUser(username)
|
||||
if err != nil && !errors.IsCode(err, errors.ErrUnlockUser) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, entity := range f.PMStructure.entities {
|
||||
if entity.IsFolderSelected {
|
||||
settings.Labels[entity.FolderName] = entity.FolderId
|
||||
}
|
||||
}
|
||||
|
||||
settings.DateBegin = f.PMStructure.GlobalOptions.FromDate
|
||||
settings.DateEnd = f.PMStructure.GlobalOptions.ToDate
|
||||
|
||||
settings.PM = backend.NewProcessManager()
|
||||
f.setHandlers(settings.PM)
|
||||
|
||||
log.Debugln("start export", settings.FilePath)
|
||||
go backend.Export(f.panicHandler, settings)
|
||||
*/
|
||||
}
|
||||
539
internal/frontend/qt-ie/folder_functions.go
Normal file
539
internal/frontend/qt-ie/folder_functions.go
Normal file
@ -0,0 +1,539 @@
|
||||
// Copyright (c) 2020 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 !nogui
|
||||
|
||||
package qtie
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
|
||||
"github.com/ProtonMail/proton-bridge/internal/transfer"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/therecipe/qt/core"
|
||||
)
|
||||
|
||||
const (
|
||||
GlobalOptionIndex = -1
|
||||
)
|
||||
|
||||
var AllFolderInfoRoles = []int{
|
||||
FolderId,
|
||||
FolderName,
|
||||
FolderColor,
|
||||
FolderType,
|
||||
FolderEntries,
|
||||
IsFolderSelected,
|
||||
FolderFromDate,
|
||||
FolderToDate,
|
||||
TargetFolderID,
|
||||
TargetLabelIDs,
|
||||
}
|
||||
|
||||
func getTargetHashes(mboxes []transfer.Mailbox) (targetFolderID, targetLabelIDs string) {
|
||||
for _, targetMailbox := range mboxes {
|
||||
if targetMailbox.IsExclusive {
|
||||
targetFolderID = targetMailbox.Hash()
|
||||
} else {
|
||||
targetLabelIDs += targetMailbox.Hash() + ";"
|
||||
}
|
||||
}
|
||||
|
||||
targetLabelIDs = strings.Trim(targetLabelIDs, ";")
|
||||
return
|
||||
}
|
||||
|
||||
func isSystemMailbox(mbox transfer.Mailbox) bool {
|
||||
return pmapi.IsSystemLabel(mbox.ID)
|
||||
}
|
||||
|
||||
func newFolderInfo(mbox transfer.Mailbox, rule *transfer.Rule) *FolderInfo {
|
||||
targetFolderID, targetLabelIDs := getTargetHashes(rule.TargetMailboxes)
|
||||
|
||||
entry := &FolderInfo{
|
||||
mailbox: mbox,
|
||||
FolderEntries: 1,
|
||||
FromDate: rule.FromTime,
|
||||
ToDate: rule.ToTime,
|
||||
IsFolderSelected: rule.Active,
|
||||
TargetFolderID: targetFolderID,
|
||||
TargetLabelIDs: targetLabelIDs,
|
||||
}
|
||||
|
||||
entry.FolderType = FolderTypeSystem
|
||||
if !isSystemMailbox(mbox) {
|
||||
if mbox.IsExclusive {
|
||||
entry.FolderType = FolderTypeFolder
|
||||
} else {
|
||||
entry.FolderType = FolderTypeLabel
|
||||
}
|
||||
}
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
func (s *FolderStructure) saveRule(info *FolderInfo) error {
|
||||
if s.transfer == nil {
|
||||
return errors.New("missing transfer")
|
||||
}
|
||||
sourceMbox := info.mailbox
|
||||
if !info.IsFolderSelected {
|
||||
s.transfer.UnsetRule(sourceMbox)
|
||||
return nil
|
||||
}
|
||||
allTargetMboxes, err := s.transfer.TargetMailboxes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var targetMboxes []transfer.Mailbox
|
||||
for _, target := range allTargetMboxes {
|
||||
targetHash := target.Hash()
|
||||
if info.TargetFolderID == targetHash || strings.Contains(info.TargetLabelIDs, targetHash) {
|
||||
targetMboxes = append(targetMboxes, target)
|
||||
}
|
||||
}
|
||||
|
||||
return s.transfer.SetRule(sourceMbox, targetMboxes, info.FromDate, info.ToDate)
|
||||
}
|
||||
|
||||
func (s *FolderInfo) updateTgtLblIDs(targetLabelsSet map[string]struct{}) {
|
||||
targets := []string{}
|
||||
for key := range targetLabelsSet {
|
||||
targets = append(targets, key)
|
||||
}
|
||||
s.TargetLabelIDs = strings.Join(targets, ";")
|
||||
}
|
||||
|
||||
func (s *FolderInfo) clearTgtLblIDs() {
|
||||
s.TargetLabelIDs = ""
|
||||
}
|
||||
|
||||
func (s *FolderInfo) AddTargetLabel(targetID string) {
|
||||
if targetID == "" {
|
||||
return
|
||||
}
|
||||
targetLabelsSet := s.getSetOfLabels()
|
||||
targetLabelsSet[targetID] = struct{}{}
|
||||
s.updateTgtLblIDs(targetLabelsSet)
|
||||
}
|
||||
|
||||
func (s *FolderInfo) RemoveTargetLabel(targetID string) {
|
||||
if targetID == "" {
|
||||
return
|
||||
}
|
||||
targetLabelsSet := s.getSetOfLabels()
|
||||
delete(targetLabelsSet, targetID)
|
||||
s.updateTgtLblIDs(targetLabelsSet)
|
||||
}
|
||||
|
||||
func (s *FolderInfo) IsType(askType string) bool {
|
||||
return s.FolderType == askType
|
||||
}
|
||||
|
||||
func (s *FolderInfo) getSetOfLabels() (uniqSet map[string]struct{}) {
|
||||
uniqSet = make(map[string]struct{})
|
||||
for _, label := range s.TargetLabelIDList() {
|
||||
uniqSet[label] = struct{}{}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *FolderInfo) TargetLabelIDList() []string {
|
||||
return strings.FieldsFunc(
|
||||
s.TargetLabelIDs,
|
||||
func(c rune) bool { return c == ';' },
|
||||
)
|
||||
}
|
||||
|
||||
// Get data
|
||||
func (s *FolderStructure) data(index *core.QModelIndex, role int) *core.QVariant {
|
||||
row, isValid := index.Row(), index.IsValid()
|
||||
if !isValid || row >= s.getCount() {
|
||||
log.Warnln("Wrong index", isValid, row)
|
||||
return core.NewQVariant()
|
||||
}
|
||||
|
||||
var f = s.get(row)
|
||||
|
||||
switch role {
|
||||
case FolderId:
|
||||
return qtcommon.NewQVariantString(f.mailbox.Hash())
|
||||
case FolderName, int(core.Qt__DisplayRole):
|
||||
return qtcommon.NewQVariantString(f.mailbox.Name)
|
||||
case FolderColor:
|
||||
return qtcommon.NewQVariantString(f.mailbox.Color)
|
||||
case FolderType:
|
||||
return qtcommon.NewQVariantString(f.FolderType)
|
||||
case FolderEntries:
|
||||
return qtcommon.NewQVariantInt(f.FolderEntries)
|
||||
case FolderFromDate:
|
||||
return qtcommon.NewQVariantLong(f.FromDate)
|
||||
case FolderToDate:
|
||||
return qtcommon.NewQVariantLong(f.ToDate)
|
||||
case IsFolderSelected:
|
||||
return qtcommon.NewQVariantBool(f.IsFolderSelected)
|
||||
case TargetFolderID:
|
||||
return qtcommon.NewQVariantString(f.TargetFolderID)
|
||||
case TargetLabelIDs:
|
||||
return qtcommon.NewQVariantString(f.TargetLabelIDs)
|
||||
default:
|
||||
log.Warnln("Wrong role", role)
|
||||
return core.NewQVariant()
|
||||
}
|
||||
}
|
||||
|
||||
// Get header data (table view, tree view)
|
||||
func (s *FolderStructure) headerData(section int, orientation core.Qt__Orientation, role int) *core.QVariant {
|
||||
if role != int(core.Qt__DisplayRole) {
|
||||
return core.NewQVariant()
|
||||
}
|
||||
|
||||
if orientation == core.Qt__Horizontal {
|
||||
return qtcommon.NewQVariantString("Column")
|
||||
}
|
||||
|
||||
return qtcommon.NewQVariantString("Row")
|
||||
}
|
||||
|
||||
// Flags is editable
|
||||
func (s *FolderStructure) flags(index *core.QModelIndex) core.Qt__ItemFlag {
|
||||
if !index.IsValid() {
|
||||
return core.Qt__ItemIsEnabled
|
||||
}
|
||||
|
||||
// can do here also: core.NewQAbstractItemModelFromPointer(s.Pointer()).Flags(index) | core.Qt__ItemIsEditable
|
||||
// or s.FlagsDefault(index) | core.Qt__ItemIsEditable
|
||||
return core.Qt__ItemIsEnabled | core.Qt__ItemIsSelectable | core.Qt__ItemIsEditable
|
||||
}
|
||||
|
||||
// Set data
|
||||
func (s *FolderStructure) setData(index *core.QModelIndex, value *core.QVariant, role int) bool {
|
||||
log.Debugf("SET DATA %d", role)
|
||||
if !index.IsValid() {
|
||||
return false
|
||||
}
|
||||
if index.Row() < GlobalOptionIndex || index.Row() > s.getCount() || index.Column() != 1 {
|
||||
return false
|
||||
}
|
||||
item := s.get(index.Row())
|
||||
t := true
|
||||
switch role {
|
||||
case FolderId, FolderType:
|
||||
log.
|
||||
WithField("structure", s).
|
||||
WithField("row", index.Row()).
|
||||
WithField("column", index.Column()).
|
||||
WithField("role", role).
|
||||
WithField("isEdit", role == int(core.Qt__EditRole)).
|
||||
Warn("Set constant role forbiden")
|
||||
case FolderName:
|
||||
item.mailbox.Name = value.ToString()
|
||||
case FolderColor:
|
||||
item.mailbox.Color = value.ToString()
|
||||
case FolderEntries:
|
||||
item.FolderEntries = value.ToInt(&t)
|
||||
case FolderFromDate:
|
||||
item.FromDate = value.ToLongLong(&t)
|
||||
case FolderToDate:
|
||||
item.ToDate = value.ToLongLong(&t)
|
||||
case IsFolderSelected:
|
||||
item.IsFolderSelected = value.ToBool()
|
||||
case TargetFolderID:
|
||||
item.TargetFolderID = value.ToString()
|
||||
case TargetLabelIDs:
|
||||
item.TargetLabelIDs = value.ToString()
|
||||
default:
|
||||
log.Debugln("uknown role ", s, index.Row(), index.Column(), role, role == int(core.Qt__EditRole))
|
||||
return false
|
||||
}
|
||||
s.changedEntityRole(index.Row(), index.Row(), role)
|
||||
return true
|
||||
}
|
||||
|
||||
// Dimension of model: number of rows is equivalent to number of items in list
|
||||
func (s *FolderStructure) rowCount(parent *core.QModelIndex) int {
|
||||
return s.getCount()
|
||||
}
|
||||
|
||||
func (s *FolderStructure) getCount() int {
|
||||
return len(s.entities)
|
||||
}
|
||||
|
||||
// Returns names of available item properties
|
||||
func (s *FolderStructure) roleNames() map[int]*core.QByteArray {
|
||||
return s.Roles()
|
||||
}
|
||||
|
||||
// Clear removes all items in model
|
||||
func (s *FolderStructure) Clear() {
|
||||
s.BeginResetModel()
|
||||
if s.getCount() != 0 {
|
||||
s.entities = []*FolderInfo{}
|
||||
}
|
||||
|
||||
s.GlobalOptions = FolderInfo{
|
||||
mailbox: transfer.Mailbox{
|
||||
Name: "=",
|
||||
},
|
||||
FromDate: 0,
|
||||
ToDate: 0,
|
||||
TargetFolderID: "",
|
||||
TargetLabelIDs: "",
|
||||
}
|
||||
s.EndResetModel()
|
||||
}
|
||||
|
||||
// Method connected to addEntry slot
|
||||
func (s *FolderStructure) addEntry(entry *FolderInfo) {
|
||||
s.insertEntry(entry, s.getCount())
|
||||
}
|
||||
|
||||
// NewUniqId which is not in map yet.
|
||||
func (s *FolderStructure) newUniqId() (name string) {
|
||||
name = s.GlobalOptions.mailbox.Name
|
||||
mbox := transfer.Mailbox{Name: name}
|
||||
for newVal := byte(name[0]); true; newVal++ {
|
||||
mbox.Name = string([]byte{newVal})
|
||||
if s.getRowById(mbox.Hash()) < GlobalOptionIndex {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Method connected to addEntry slot
|
||||
func (s *FolderStructure) insertEntry(entry *FolderInfo, i int) {
|
||||
s.BeginInsertRows(core.NewQModelIndex(), i, i)
|
||||
s.entities = append(s.entities[:i], append([]*FolderInfo{entry}, s.entities[i:]...)...)
|
||||
s.EndInsertRows()
|
||||
// update global if conflict
|
||||
if entry.mailbox.Hash() == s.GlobalOptions.mailbox.Hash() {
|
||||
globalName := s.newUniqId()
|
||||
s.GlobalOptions.mailbox.Name = globalName
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FolderStructure) GetInfo(row int) FolderInfo {
|
||||
return *s.get(row)
|
||||
}
|
||||
|
||||
func (s *FolderStructure) changedEntityRole(rowStart int, rowEnd int, roles ...int) {
|
||||
if rowStart < GlobalOptionIndex || rowEnd < GlobalOptionIndex {
|
||||
return
|
||||
}
|
||||
if rowStart < 0 || rowStart >= s.getCount() {
|
||||
rowStart = 0
|
||||
}
|
||||
if rowEnd < 0 || rowEnd >= s.getCount() {
|
||||
rowEnd = s.getCount()
|
||||
}
|
||||
if rowStart > rowEnd {
|
||||
tmp := rowStart
|
||||
rowStart = rowEnd
|
||||
rowEnd = tmp
|
||||
}
|
||||
indexStart := s.Index(rowStart, 0, core.NewQModelIndex())
|
||||
indexEnd := s.Index(rowEnd, 0, core.NewQModelIndex())
|
||||
s.updateSelection(indexStart, indexEnd, roles)
|
||||
s.DataChanged(indexStart, indexEnd, roles)
|
||||
}
|
||||
|
||||
func (s *FolderStructure) setFolderSelection(id string, toSelect bool) {
|
||||
log.Debugf("set folder selection %q %b", id, toSelect)
|
||||
i := s.getRowById(id)
|
||||
//
|
||||
info := s.get(i)
|
||||
before := info.IsFolderSelected
|
||||
info.IsFolderSelected = toSelect
|
||||
if err := s.saveRule(info); err != nil {
|
||||
s.get(i).IsFolderSelected = before
|
||||
log.WithError(err).WithField("id", id).WithField("toSelect", toSelect).Error("Cannot set selection")
|
||||
return
|
||||
}
|
||||
//
|
||||
s.changedEntityRole(i, i, IsFolderSelected)
|
||||
}
|
||||
|
||||
func (s *FolderStructure) setTargetFolderID(id, target string) {
|
||||
log.Debugf("set targetFolderID %q %q", id, target)
|
||||
i := s.getRowById(id)
|
||||
//
|
||||
info := s.get(i)
|
||||
//s.get(i).TargetFolderID = target
|
||||
before := info.TargetFolderID
|
||||
info.TargetFolderID = target
|
||||
if err := s.saveRule(info); err != nil {
|
||||
info.TargetFolderID = before
|
||||
log.WithError(err).WithField("id", id).WithField("target", target).Error("Cannot set target")
|
||||
return
|
||||
}
|
||||
//
|
||||
s.changedEntityRole(i, i, TargetFolderID)
|
||||
if target == "" { // do not import
|
||||
before := info.TargetLabelIDs
|
||||
info.clearTgtLblIDs()
|
||||
if err := s.saveRule(info); err != nil {
|
||||
info.TargetLabelIDs = before
|
||||
log.WithError(err).WithField("id", id).WithField("target", target).Error("Cannot set target")
|
||||
return
|
||||
}
|
||||
s.changedEntityRole(i, i, TargetLabelIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FolderStructure) addTargetLabelID(id, label string) {
|
||||
log.Debugf("add target label id %q %q", id, label)
|
||||
if label == "" {
|
||||
return
|
||||
}
|
||||
i := s.getRowById(id)
|
||||
info := s.get(i)
|
||||
before := info.TargetLabelIDs
|
||||
info.AddTargetLabel(label)
|
||||
if err := s.saveRule(info); err != nil {
|
||||
info.TargetLabelIDs = before
|
||||
log.WithError(err).WithField("id", id).WithField("label", label).Error("Cannot add label")
|
||||
return
|
||||
}
|
||||
s.changedEntityRole(i, i, TargetLabelIDs)
|
||||
}
|
||||
|
||||
func (s *FolderStructure) removeTargetLabelID(id, label string) {
|
||||
log.Debugf("remove label id %q %q", id, label)
|
||||
if label == "" {
|
||||
return
|
||||
}
|
||||
i := s.getRowById(id)
|
||||
info := s.get(i)
|
||||
before := info.TargetLabelIDs
|
||||
info.RemoveTargetLabel(label)
|
||||
if err := s.saveRule(info); err != nil {
|
||||
info.TargetLabelIDs = before
|
||||
log.WithError(err).WithField("id", id).WithField("label", label).Error("Cannot remove label")
|
||||
return
|
||||
}
|
||||
s.changedEntityRole(i, i, TargetLabelIDs)
|
||||
}
|
||||
|
||||
func (s *FolderStructure) setFromToDate(id string, from, to int64) {
|
||||
log.Debugf("set from to date %q %d %d", id, from, to)
|
||||
i := s.getRowById(id)
|
||||
info := s.get(i)
|
||||
beforeFrom := info.FromDate
|
||||
beforeTo := info.ToDate
|
||||
info.FromDate = from
|
||||
info.ToDate = to
|
||||
if err := s.saveRule(info); err != nil {
|
||||
info.FromDate = beforeFrom
|
||||
info.ToDate = beforeTo
|
||||
log.WithError(err).WithField("id", id).WithField("from", from).WithField("to", to).Error("Cannot set date")
|
||||
return
|
||||
}
|
||||
s.changedEntityRole(i, i, FolderFromDate, FolderToDate)
|
||||
}
|
||||
|
||||
func (s *FolderStructure) selectType(folderType string, toSelect bool) {
|
||||
log.Debugf("set type %q %b", folderType, toSelect)
|
||||
iFirst, iLast := -1, -1
|
||||
for i, entity := range s.entities {
|
||||
if entity.IsType(folderType) {
|
||||
if iFirst == -1 {
|
||||
iFirst = i
|
||||
}
|
||||
before := entity.IsFolderSelected
|
||||
entity.IsFolderSelected = toSelect
|
||||
if err := s.saveRule(entity); err != nil {
|
||||
entity.IsFolderSelected = before
|
||||
log.WithError(err).WithField("i", i).WithField("type", folderType).WithField("toSelect", toSelect).Error("Cannot select type")
|
||||
}
|
||||
iLast = i
|
||||
}
|
||||
}
|
||||
if iFirst != -1 {
|
||||
s.changedEntityRole(iFirst, iLast, IsFolderSelected)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FolderStructure) updateSelection(topLeft *core.QModelIndex, bottomRight *core.QModelIndex, roles []int) {
|
||||
for _, role := range roles {
|
||||
switch role {
|
||||
case IsFolderSelected:
|
||||
s.SetSelectedFolders(true)
|
||||
s.SetSelectedLabels(true)
|
||||
s.SetAtLeastOneSelected(false)
|
||||
for _, entity := range s.entities {
|
||||
if entity.IsFolderSelected {
|
||||
s.SetAtLeastOneSelected(true)
|
||||
} else {
|
||||
if entity.IsType(FolderTypeFolder) {
|
||||
s.SetSelectedFolders(false)
|
||||
}
|
||||
if entity.IsType(FolderTypeLabel) {
|
||||
s.SetSelectedLabels(false)
|
||||
}
|
||||
}
|
||||
if !s.IsSelectedFolders() && !s.IsSelectedLabels() && s.IsAtLeastOneSelected() {
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FolderStructure) hasFolderWithName(name string) bool {
|
||||
for _, entity := range s.entities {
|
||||
if entity.mailbox.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *FolderStructure) getRowById(id string) (row int) {
|
||||
for row = GlobalOptionIndex; row < s.getCount(); row++ {
|
||||
if id == s.get(row).mailbox.Hash() {
|
||||
return
|
||||
}
|
||||
}
|
||||
row = GlobalOptionIndex - 1
|
||||
return
|
||||
}
|
||||
|
||||
func (s *FolderStructure) hasTarget() bool {
|
||||
for row := 0; row < s.getCount(); row++ {
|
||||
if s.get(row).TargetFolderID != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Getter for account info pointer
|
||||
// index out of array length returns empty folder info to avoid segfault
|
||||
// index == GlobalOptionIndex is set to access global options
|
||||
func (s *FolderStructure) get(index int) *FolderInfo {
|
||||
if index < GlobalOptionIndex || index >= s.getCount() {
|
||||
return &FolderInfo{}
|
||||
}
|
||||
if index == GlobalOptionIndex {
|
||||
return &s.GlobalOptions
|
||||
}
|
||||
return s.entities[index]
|
||||
}
|
||||
196
internal/frontend/qt-ie/folder_structure.go
Normal file
196
internal/frontend/qt-ie/folder_structure.go
Normal file
@ -0,0 +1,196 @@
|
||||
// Copyright (c) 2020 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 !nogui
|
||||
|
||||
package qtie
|
||||
|
||||
// TODO:
|
||||
// Proposal for new structure
|
||||
// It will be a bit more memory but much better performance
|
||||
// * Rules:
|
||||
// * rules []Rule /QAbstracItemModel/
|
||||
// * globalFromDate int64
|
||||
// * globalToDate int64
|
||||
// * globalLabel Mbox
|
||||
// * targetPath string
|
||||
// * filterEncryptedBodies bool
|
||||
// * Rule
|
||||
// * sourceMbox: Mbox
|
||||
// * targetFolders: []Mbox /QAbstracItemModel/ (all available target folders)
|
||||
// * targetLabels: []Mbox /QAbstracItemModel/ (all available target labels)
|
||||
// * selectedLabelColors: QStringList (need reset context on change) (show label list)
|
||||
// * fromDate int64
|
||||
// * toDate int64
|
||||
// * Mbox
|
||||
// * IsActive bool (show checkox)
|
||||
// * Name string (show name)
|
||||
// * Type string (show icon)
|
||||
// * Color string (show icon)
|
||||
//
|
||||
// Biggest update: add folder or label for all roles update target models
|
||||
|
||||
import (
|
||||
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
|
||||
"github.com/ProtonMail/proton-bridge/internal/transfer"
|
||||
"github.com/therecipe/qt/core"
|
||||
)
|
||||
|
||||
// FolderStructure model providing container for items (folder info) to QML
|
||||
//
|
||||
// QML ListView connects the model from Go and it shows item (entities)
|
||||
// information.
|
||||
//
|
||||
// Copied and edited from `github.com/therecipe/qt/internal/examples/sailfish/listview`
|
||||
//
|
||||
// NOTE: When implementing a model it is important to remember that QAbstractItemModel does not store any data itself !!!!
|
||||
// see https://doc.qt.io/qt-5/model-view-programming.html#designing-a-model
|
||||
type FolderStructure struct {
|
||||
core.QAbstractListModel
|
||||
|
||||
// QtObject Constructor
|
||||
_ func() `constructor:"init"`
|
||||
|
||||
// List of item properties
|
||||
//
|
||||
// All available item properties are inside the map
|
||||
_ map[int]*core.QByteArray `property:"roles"`
|
||||
|
||||
// The data storage
|
||||
//
|
||||
// The slice with all entities. It is not accessed directly but using
|
||||
// `data(index,role)`
|
||||
entities []*FolderInfo
|
||||
GlobalOptions FolderInfo
|
||||
|
||||
transfer *transfer.Transfer
|
||||
|
||||
// Global Folders/Labels selection flag, use setter from QML
|
||||
_ bool `property:"selectedLabels"`
|
||||
_ bool `property:"selectedFolders"`
|
||||
_ bool `property:"atLeastOneSelected"`
|
||||
|
||||
// Getters (const)
|
||||
_ func() int `slot:"getCount"`
|
||||
_ func(index int) string `slot:"getID"`
|
||||
_ func(id string) string `slot:"getName"`
|
||||
_ func(id string) string `slot:"getType"`
|
||||
_ func(id string) string `slot:"getColor"`
|
||||
_ func(id string) int64 `slot:"getFrom"`
|
||||
_ func(id string) int64 `slot:"getTo"`
|
||||
_ func(id string) string `slot:"getTargetLabelIDs"`
|
||||
_ func(name string) bool `slot:"hasFolderWithName"`
|
||||
_ func() bool `slot:"hasTarget"`
|
||||
|
||||
// TODO get folders
|
||||
// TODO get labels
|
||||
// TODO get selected labels
|
||||
// TODO get selected folder
|
||||
|
||||
// Setters (emits DataChanged)
|
||||
_ func(fileType string, toSelect bool) `slot:"selectType"`
|
||||
_ func(id string, toSelect bool) `slot:"setFolderSelection"`
|
||||
_ func(id string, target string) `slot:"setTargetFolderID"`
|
||||
_ func(id string, label string) `slot:"addTargetLabelID"`
|
||||
_ func(id string, label string) `slot:"removeTargetLabelID"`
|
||||
_ func(id string, from, to int64) `slot:"setFromToDate"`
|
||||
}
|
||||
|
||||
// FolderInfo is the element of model
|
||||
//
|
||||
// It contains all data for one structure entry
|
||||
type FolderInfo struct {
|
||||
/*
|
||||
FolderId string
|
||||
FolderFullPath string
|
||||
FolderColor string
|
||||
FolderFullName string
|
||||
*/
|
||||
mailbox transfer.Mailbox // TODO how to reference from qml source mailbox to go target mailbox
|
||||
FolderType string
|
||||
FolderEntries int // todo remove
|
||||
IsFolderSelected bool
|
||||
FromDate int64 // Unix seconds
|
||||
ToDate int64 // Unix seconds
|
||||
TargetFolderID string // target ID TODO: this will be hash
|
||||
TargetLabelIDs string // semicolon separated list of label ID same here
|
||||
}
|
||||
|
||||
// Registration of new metatype before creating instance
|
||||
//
|
||||
// NOTE: check it is run once per program. write a log
|
||||
func init() {
|
||||
FolderStructure_QRegisterMetaType()
|
||||
}
|
||||
|
||||
// Constructor
|
||||
//
|
||||
// Creates the map for item properties and connects the methods
|
||||
func (s *FolderStructure) init() {
|
||||
s.SetRoles(map[int]*core.QByteArray{
|
||||
FolderId: qtcommon.NewQByteArrayFromString("folderId"),
|
||||
FolderName: qtcommon.NewQByteArrayFromString("folderName"),
|
||||
FolderColor: qtcommon.NewQByteArrayFromString("folderColor"),
|
||||
FolderType: qtcommon.NewQByteArrayFromString("folderType"),
|
||||
FolderEntries: qtcommon.NewQByteArrayFromString("folderEntries"),
|
||||
IsFolderSelected: qtcommon.NewQByteArrayFromString("isFolderSelected"),
|
||||
FolderFromDate: qtcommon.NewQByteArrayFromString("fromDate"),
|
||||
FolderToDate: qtcommon.NewQByteArrayFromString("toDate"),
|
||||
TargetFolderID: qtcommon.NewQByteArrayFromString("targetFolderID"),
|
||||
TargetLabelIDs: qtcommon.NewQByteArrayFromString("targetLabelIDs"),
|
||||
})
|
||||
|
||||
// basic QAbstractListModel mehods
|
||||
s.ConnectGetCount(s.getCount)
|
||||
s.ConnectRowCount(s.rowCount)
|
||||
s.ConnectColumnCount(func(parent *core.QModelIndex) int { return 1 }) // for list it should be always 1
|
||||
s.ConnectData(s.data)
|
||||
s.ConnectHeaderData(s.headerData)
|
||||
s.ConnectRoleNames(s.roleNames)
|
||||
// Editable QAbstractListModel needs: https://doc.qt.io/qt-5/model-view-programming.html#an-editable-model
|
||||
s.ConnectSetData(s.setData)
|
||||
s.ConnectFlags(s.flags)
|
||||
|
||||
// Custom FolderStructure slots to export
|
||||
|
||||
// Getters (const)
|
||||
s.ConnectGetID(func(row int) string { return s.get(row).mailbox.Hash() })
|
||||
s.ConnectGetType(func(id string) string { row := s.getRowById(id); return s.get(row).FolderType })
|
||||
s.ConnectGetName(func(id string) string { row := s.getRowById(id); return s.get(row).mailbox.Name })
|
||||
s.ConnectGetColor(func(id string) string { row := s.getRowById(id); return s.get(row).mailbox.Color })
|
||||
s.ConnectGetFrom(func(id string) int64 { row := s.getRowById(id); return s.get(row).FromDate })
|
||||
s.ConnectGetTo(func(id string) int64 { row := s.getRowById(id); return s.get(row).ToDate })
|
||||
s.ConnectGetTargetLabelIDs(func(id string) string { row := s.getRowById(id); return s.get(row).TargetLabelIDs })
|
||||
s.ConnectHasFolderWithName(s.hasFolderWithName)
|
||||
s.ConnectHasTarget(s.hasTarget)
|
||||
|
||||
// Setters (emits DataChanged)
|
||||
s.ConnectSelectType(s.selectType)
|
||||
s.ConnectSetFolderSelection(s.setFolderSelection)
|
||||
s.ConnectSetTargetFolderID(s.setTargetFolderID)
|
||||
s.ConnectAddTargetLabelID(s.addTargetLabelID)
|
||||
s.ConnectRemoveTargetLabelID(s.removeTargetLabelID)
|
||||
s.ConnectSetFromToDate(s.setFromToDate)
|
||||
|
||||
s.GlobalOptions = FolderInfo{
|
||||
mailbox: transfer.Mailbox{Name: "="},
|
||||
FromDate: 0,
|
||||
ToDate: 0,
|
||||
TargetFolderID: "",
|
||||
TargetLabelIDs: "",
|
||||
}
|
||||
}
|
||||
65
internal/frontend/qt-ie/folder_structure_test.go
Normal file
65
internal/frontend/qt-ie/folder_structure_test.go
Normal file
@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2020 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 !nogui
|
||||
|
||||
package qtie
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func hasNumberOfLabels(tb testing.TB, folder *FolderInfo, expected int) {
|
||||
if current := len(folder.TargetLabelIDList()); current != expected {
|
||||
tb.Error("Folder has wrong number of labels. Expected", expected, "has", current, " labels", folder.TargetLabelIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func labelStringEquals(tb testing.TB, folder *FolderInfo, expected string) {
|
||||
if current := folder.TargetLabelIDs; current != expected {
|
||||
tb.Error("Folder returned wrong labels. Expected", expected, "has", current, " labels", folder.TargetLabelIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelInfoUniqSet(t *testing.T) {
|
||||
folder := &FolderInfo{}
|
||||
labelStringEquals(t, folder, "")
|
||||
hasNumberOfLabels(t, folder, 0)
|
||||
// add label
|
||||
folder.AddTargetLabel("blah")
|
||||
hasNumberOfLabels(t, folder, 1)
|
||||
labelStringEquals(t, folder, "blah")
|
||||
//
|
||||
folder.AddTargetLabel("blah___")
|
||||
hasNumberOfLabels(t, folder, 2)
|
||||
labelStringEquals(t, folder, "blah;blah___")
|
||||
// add same label
|
||||
folder.AddTargetLabel("blah")
|
||||
hasNumberOfLabels(t, folder, 2)
|
||||
// remove label
|
||||
folder.RemoveTargetLabel("blah")
|
||||
hasNumberOfLabels(t, folder, 1)
|
||||
//
|
||||
folder.AddTargetLabel("blah___")
|
||||
hasNumberOfLabels(t, folder, 1)
|
||||
// remove same label
|
||||
folder.RemoveTargetLabel("blah")
|
||||
hasNumberOfLabels(t, folder, 1)
|
||||
// add again label
|
||||
folder.AddTargetLabel("blah")
|
||||
hasNumberOfLabels(t, folder, 2)
|
||||
}
|
||||
497
internal/frontend/qt-ie/frontend.go
Normal file
497
internal/frontend/qt-ie/frontend.go
Normal file
@ -0,0 +1,497 @@
|
||||
// Copyright (c) 2020 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 !nogui
|
||||
|
||||
package qtie
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/internal/transfer"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/updates"
|
||||
|
||||
"github.com/therecipe/qt/core"
|
||||
"github.com/therecipe/qt/gui"
|
||||
"github.com/therecipe/qt/qml"
|
||||
"github.com/therecipe/qt/widgets"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
)
|
||||
|
||||
var log = logrus.WithField("pkg", "frontend-qt-ie")
|
||||
|
||||
// FrontendQt is API between Import-Export and Qt
|
||||
//
|
||||
// With this interface it is possible to control Qt-Gui interface using pointers to
|
||||
// Qt and QML objects. QML signals and slots are connected via methods of GoQMLInterface.
|
||||
type FrontendQt struct {
|
||||
panicHandler types.PanicHandler
|
||||
config *config.Config
|
||||
eventListener listener.Listener
|
||||
updates types.Updater
|
||||
ie types.ImportExporter
|
||||
|
||||
App *widgets.QApplication // Main Application pointer
|
||||
View *qml.QQmlApplicationEngine // QML engine pointer
|
||||
MainWin *core.QObject // Pointer to main window inside QML
|
||||
Qml *GoQMLInterface // Object accessible from both Go and QML for methods and signals
|
||||
Accounts qtcommon.Accounts // Providing data for accounts ListView
|
||||
|
||||
programName string // Program name
|
||||
programVersion string // Program version
|
||||
buildVersion string // Program build version
|
||||
|
||||
PMStructure *FolderStructure // Providing data for account labels and folders for ProtonMail account
|
||||
ExternalStructure *FolderStructure // Providing data for account labels and folders for MBOX, EML or external IMAP account
|
||||
ErrorList *ErrorListModel // Providing data for error reporting
|
||||
|
||||
transfer *transfer.Transfer
|
||||
|
||||
notifyHasNoKeychain bool
|
||||
}
|
||||
|
||||
// New is constructor for Import-Export Qt-Go interface
|
||||
func New(
|
||||
version, buildVersion string,
|
||||
panicHandler types.PanicHandler,
|
||||
config *config.Config,
|
||||
eventListener listener.Listener,
|
||||
updates types.Updater,
|
||||
ie types.ImportExporter,
|
||||
) *FrontendQt {
|
||||
f := &FrontendQt{
|
||||
panicHandler: panicHandler,
|
||||
config: config,
|
||||
programName: "ProtonMail Import-Export",
|
||||
programVersion: "v" + version,
|
||||
eventListener: eventListener,
|
||||
buildVersion: buildVersion,
|
||||
updates: updates,
|
||||
ie: ie,
|
||||
}
|
||||
|
||||
// Nicer string for OS
|
||||
currentOS := core.QSysInfo_PrettyProductName()
|
||||
ie.SetCurrentOS(currentOS)
|
||||
|
||||
log.Debugf("New Qt frontend: %p", f)
|
||||
return f
|
||||
}
|
||||
|
||||
// IsAppRestarting for Import-Export is always false i.e never restarts
|
||||
func (s *FrontendQt) IsAppRestarting() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Loop function for Import-Export interface. It runs QtExecute in main thread
|
||||
// with no additional function.
|
||||
func (s *FrontendQt) Loop(setupError error) (err error) {
|
||||
if setupError != nil {
|
||||
s.notifyHasNoKeychain = true
|
||||
}
|
||||
go func() {
|
||||
defer s.panicHandler.HandlePanic()
|
||||
s.watchEvents()
|
||||
}()
|
||||
err = s.QtExecute(func(s *FrontendQt) error { return nil })
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *FrontendQt) watchEvents() {
|
||||
internetOffCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.InternetOffEvent)
|
||||
internetOnCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.InternetOnEvent)
|
||||
restartBridgeCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.RestartBridgeEvent)
|
||||
addressChangedCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.AddressChangedEvent)
|
||||
addressChangedLogoutCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.AddressChangedLogoutEvent)
|
||||
logoutCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.LogoutEvent)
|
||||
updateApplicationCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.UpgradeApplicationEvent)
|
||||
newUserCh := qtcommon.MakeAndRegisterEvent(s.eventListener, events.UserRefreshEvent)
|
||||
for {
|
||||
select {
|
||||
case <-internetOffCh:
|
||||
s.Qml.SetConnectionStatus(false)
|
||||
case <-internetOnCh:
|
||||
s.Qml.SetConnectionStatus(true)
|
||||
case <-restartBridgeCh:
|
||||
s.Qml.SetIsRestarting(true)
|
||||
s.App.Quit()
|
||||
case address := <-addressChangedCh:
|
||||
s.Qml.NotifyAddressChanged(address)
|
||||
case address := <-addressChangedLogoutCh:
|
||||
s.Qml.NotifyAddressChangedLogout(address)
|
||||
case userID := <-logoutCh:
|
||||
user, err := s.ie.GetUser(userID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.Qml.NotifyLogout(user.Username())
|
||||
case <-updateApplicationCh:
|
||||
s.Qml.ProcessFinished()
|
||||
s.Qml.NotifyUpdate()
|
||||
case <-newUserCh:
|
||||
s.Qml.LoadAccounts()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FrontendQt) qtSetupQmlAndStructures() {
|
||||
s.App = widgets.NewQApplication(len(os.Args), os.Args)
|
||||
// view
|
||||
s.View = qml.NewQQmlApplicationEngine(s.App)
|
||||
// Add Go-QML Import-Export
|
||||
s.Qml = NewGoQMLInterface(nil)
|
||||
s.Qml.SetFrontend(s) // provides access
|
||||
s.View.RootContext().SetContextProperty("go", s.Qml)
|
||||
// Add AccountsModel
|
||||
s.Accounts.SetupAccounts(s.Qml, s.ie)
|
||||
s.View.RootContext().SetContextProperty("accountsModel", s.Accounts.Model)
|
||||
|
||||
// Add ProtonMail FolderStructure
|
||||
s.PMStructure = NewFolderStructure(nil)
|
||||
s.View.RootContext().SetContextProperty("structurePM", s.PMStructure)
|
||||
|
||||
// Add external FolderStructure
|
||||
s.ExternalStructure = NewFolderStructure(nil)
|
||||
s.View.RootContext().SetContextProperty("structureExternal", s.ExternalStructure)
|
||||
|
||||
// Add error list modal
|
||||
s.ErrorList = NewErrorListModel(nil)
|
||||
s.View.RootContext().SetContextProperty("errorList", s.ErrorList)
|
||||
s.Qml.ConnectLoadImportReports(s.ErrorList.load)
|
||||
|
||||
// Import path and load QML files
|
||||
s.View.AddImportPath("qrc:///")
|
||||
s.View.Load(core.NewQUrl3("qrc:/uiie.qml", 0))
|
||||
|
||||
// TODO set the first start flag
|
||||
log.Error("Get FirstStart: Not implemented")
|
||||
//if prefs.Get(prefs.FirstStart) == "true" {
|
||||
if false {
|
||||
s.Qml.SetIsFirstStart(true)
|
||||
} else {
|
||||
s.Qml.SetIsFirstStart(false)
|
||||
}
|
||||
|
||||
// Notify user about error during initialization.
|
||||
if s.notifyHasNoKeychain {
|
||||
s.Qml.NotifyHasNoKeychain()
|
||||
}
|
||||
}
|
||||
|
||||
// QtExecute in main for starting Qt application
|
||||
//
|
||||
// It is needed to have just one Qt application per program (at least per same
|
||||
// thread). This functions reads the main user interface defined in QML files.
|
||||
// The files are appended to library by Qt-QRC.
|
||||
func (s *FrontendQt) QtExecute(Procedure func(*FrontendQt) error) error {
|
||||
qtcommon.QtSetupCoreAndControls(s.programName, s.programVersion)
|
||||
s.qtSetupQmlAndStructures()
|
||||
// Check QML is loaded properly
|
||||
if len(s.View.RootObjects()) == 0 {
|
||||
//return errors.New(errors.ErrQApplication, "QML not loaded properly")
|
||||
return errors.New("QML not loaded properly")
|
||||
}
|
||||
// Obtain main window (need for invoke method)
|
||||
s.MainWin = s.View.RootObjects()[0]
|
||||
// Injected procedure for out-of-main-thread applications
|
||||
if err := Procedure(s); err != nil {
|
||||
return err
|
||||
}
|
||||
// Loop
|
||||
if ret := gui.QGuiApplication_Exec(); ret != 0 {
|
||||
//err := errors.New(errors.ErrQApplication, "Event loop ended with return value: %v", string(ret))
|
||||
err := errors.New("Event loop ended with return value: " + string(ret))
|
||||
log.Warnln("QGuiApplication_Exec: ", err)
|
||||
return err
|
||||
}
|
||||
log.Debug("Closing...")
|
||||
log.Error("Set FirstStart: Not implemented")
|
||||
//prefs.Set(prefs.FirstStart, "false")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FrontendQt) openLogs() {
|
||||
go open.Run(s.config.GetLogDir())
|
||||
}
|
||||
|
||||
func (s *FrontendQt) openReport() {
|
||||
go open.Run(s.Qml.ImportLogFileName())
|
||||
}
|
||||
|
||||
func (s *FrontendQt) openDownloadLink() {
|
||||
go open.Run(s.updates.GetDownloadLink())
|
||||
}
|
||||
|
||||
func (s *FrontendQt) sendImportReport(address, reportFile string) (isOK bool) {
|
||||
/*
|
||||
accname := "[No account logged in]"
|
||||
if s.Accounts.Count() > 0 {
|
||||
accname = s.Accounts.get(0).Account()
|
||||
}
|
||||
|
||||
basename := filepath.Base(reportFile)
|
||||
req := pmapi.ReportReq{
|
||||
OS: core.QSysInfo_ProductType(),
|
||||
OSVersion: core.QSysInfo_PrettyProductName(),
|
||||
Title: "[Import Export] Import report: " + basename,
|
||||
Description: "Sending import report file in attachment.",
|
||||
Username: accname,
|
||||
Email: address,
|
||||
}
|
||||
|
||||
report, err := os.Open(reportFile)
|
||||
if err != nil {
|
||||
log.Errorln("report file open:", err)
|
||||
isOK = false
|
||||
}
|
||||
req.AddAttachment("log", basename, report)
|
||||
|
||||
c := pmapi.NewClient(backend.APIConfig, "import_reporter")
|
||||
err = c.Report(req)
|
||||
if err != nil {
|
||||
log.Errorln("while sendReport:", err)
|
||||
isOK = false
|
||||
return
|
||||
}
|
||||
log.Infof("Report %q send successfully", basename)
|
||||
isOK = true
|
||||
*/
|
||||
return false
|
||||
}
|
||||
|
||||
// sendBug is almost idetical to bridge
|
||||
func (s *FrontendQt) sendBug(description, emailClient, address string) (isOK bool) {
|
||||
isOK = true
|
||||
var accname = "No account logged in"
|
||||
if s.Accounts.Model.Count() > 0 {
|
||||
accname = s.Accounts.Model.Get(0).Account()
|
||||
}
|
||||
if err := s.ie.ReportBug(
|
||||
core.QSysInfo_ProductType(),
|
||||
core.QSysInfo_PrettyProductName(),
|
||||
description,
|
||||
accname,
|
||||
address,
|
||||
emailClient,
|
||||
); err != nil {
|
||||
log.Errorln("while sendBug:", err)
|
||||
isOK = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// checkInternet is almost idetical to bridge
|
||||
func (s *FrontendQt) checkInternet() {
|
||||
s.Qml.SetConnectionStatus(s.ie.CheckConnection() == nil)
|
||||
}
|
||||
|
||||
func (s *FrontendQt) showError(err error) {
|
||||
code := 0 // TODO err.Code()
|
||||
s.Qml.SetErrorDescription(err.Error())
|
||||
log.WithField("code", code).Errorln(err.Error())
|
||||
s.Qml.NotifyError(code)
|
||||
}
|
||||
|
||||
func (s *FrontendQt) emitEvent(evType, msg string) {
|
||||
s.eventListener.Emit(evType, msg)
|
||||
}
|
||||
|
||||
func (s *FrontendQt) setProgressManager(progress *transfer.Progress) {
|
||||
s.Qml.ConnectPauseProcess(func() { progress.Pause("user") })
|
||||
s.Qml.ConnectResumeProcess(progress.Resume)
|
||||
s.Qml.ConnectCancelProcess(func(clearUnfinished bool) {
|
||||
// TODO clear unfinished
|
||||
progress.Stop()
|
||||
})
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
s.Qml.DisconnectPauseProcess()
|
||||
s.Qml.DisconnectResumeProcess()
|
||||
s.Qml.DisconnectCancelProcess()
|
||||
s.Qml.SetProgress(1)
|
||||
}()
|
||||
|
||||
//TODO get log file (in old code it was here, but this is ugly place probably somewhere else)
|
||||
updates := progress.GetUpdateChannel()
|
||||
for range updates {
|
||||
if progress.IsStopped() {
|
||||
break
|
||||
}
|
||||
failed, imported, _, _, total := progress.GetCounts()
|
||||
if total != 0 { // udate total
|
||||
s.Qml.SetTotal(int(total))
|
||||
}
|
||||
s.Qml.SetProgressFails(int(failed))
|
||||
s.Qml.SetProgressDescription(progress.PauseReason()) // TODO add description when changing folders?
|
||||
if total > 0 {
|
||||
newProgress := float32(imported+failed) / float32(total)
|
||||
if newProgress >= 0 && newProgress != s.Qml.Progress() {
|
||||
s.Qml.SetProgress(newProgress)
|
||||
s.Qml.ProgressChanged(newProgress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO fatal error?
|
||||
}()
|
||||
}
|
||||
|
||||
// StartUpdate is identical to bridge
|
||||
func (s *FrontendQt) StartUpdate() {
|
||||
progress := make(chan updates.Progress)
|
||||
go func() { // Update progress in QML.
|
||||
defer s.panicHandler.HandlePanic()
|
||||
for current := range progress {
|
||||
s.Qml.SetProgress(current.Processed)
|
||||
s.Qml.SetProgressDescription(strconv.Itoa(current.Description))
|
||||
// Error happend
|
||||
if current.Err != nil {
|
||||
log.Error("update progress: ", current.Err)
|
||||
s.Qml.UpdateFinished(true)
|
||||
return
|
||||
}
|
||||
// Finished everything OK.
|
||||
if current.Description >= updates.InfoQuitApp {
|
||||
s.Qml.UpdateFinished(false)
|
||||
time.Sleep(3 * time.Second) // Just notify.
|
||||
s.Qml.SetIsRestarting(current.Description == updates.InfoRestartApp)
|
||||
s.App.Quit()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer s.panicHandler.HandlePanic()
|
||||
s.updates.StartUpgrade(progress)
|
||||
}()
|
||||
}
|
||||
|
||||
// isNewVersionAvailable is identical to bridge
|
||||
// return 0 when local version is fine
|
||||
// return 1 when new version is available
|
||||
func (s *FrontendQt) isNewVersionAvailable(showMessage bool) {
|
||||
go func() {
|
||||
defer s.Qml.ProcessFinished()
|
||||
isUpToDate, latestVersionInfo, err := s.updates.CheckIsUpToDate()
|
||||
if err != nil {
|
||||
log.Warnln("Cannot retrieve version info: ", err)
|
||||
s.checkInternet()
|
||||
return
|
||||
}
|
||||
s.Qml.SetConnectionStatus(true) // if we are here connection is ok
|
||||
if isUpToDate {
|
||||
s.Qml.SetUpdateState(StatusUpToDate)
|
||||
if showMessage {
|
||||
s.Qml.NotifyVersionIsTheLatest()
|
||||
}
|
||||
return
|
||||
}
|
||||
s.Qml.SetNewversion(latestVersionInfo.Version)
|
||||
s.Qml.SetChangelog(latestVersionInfo.ReleaseNotes)
|
||||
s.Qml.SetBugfixes(latestVersionInfo.ReleaseFixedBugs)
|
||||
s.Qml.SetLandingPage(latestVersionInfo.LandingPage)
|
||||
s.Qml.SetDownloadLink(latestVersionInfo.GetDownloadLink())
|
||||
s.Qml.SetUpdateState(StatusNewVersionAvailable)
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *FrontendQt) resetSource() {
|
||||
if s.transfer != nil {
|
||||
s.transfer.ResetRules()
|
||||
if err := s.loadStructuresForImport(); err != nil {
|
||||
log.WithError(err).Error("Cannot reload structures after reseting rules.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getLocalVersionInfo is identical to bridge.
|
||||
func (s *FrontendQt) getLocalVersionInfo() {
|
||||
defer s.Qml.ProcessFinished()
|
||||
localVersion := s.updates.GetLocalVersion()
|
||||
s.Qml.SetNewversion(localVersion.Version)
|
||||
s.Qml.SetChangelog(localVersion.ReleaseNotes)
|
||||
s.Qml.SetBugfixes(localVersion.ReleaseFixedBugs)
|
||||
}
|
||||
|
||||
// LeastUsedColor is intended to return color for creating a new inbox or label.
|
||||
func (s *FrontendQt) leastUsedColor() string {
|
||||
if s.transfer == nil {
|
||||
log.Errorln("Getting least used color before transfer exist.")
|
||||
return "#7272a7"
|
||||
}
|
||||
|
||||
m, err := s.transfer.TargetMailboxes()
|
||||
|
||||
if err != nil {
|
||||
log.Errorln("Getting least used color:", err)
|
||||
s.showError(err)
|
||||
}
|
||||
|
||||
return transfer.LeastUsedColor(m)
|
||||
}
|
||||
|
||||
// createLabelOrFolder performs an IE target mailbox creation.
|
||||
func (s *FrontendQt) createLabelOrFolder(email, name, color string, isLabel bool, sourceID string) bool {
|
||||
// Prepare new mailbox.
|
||||
m := transfer.Mailbox{
|
||||
Name: name,
|
||||
Color: color,
|
||||
IsExclusive: !isLabel,
|
||||
}
|
||||
|
||||
// Select least used color if no color given.
|
||||
if m.Color == "" {
|
||||
m.Color = s.leastUsedColor()
|
||||
}
|
||||
|
||||
// Create mailbox.
|
||||
newLabel, err := s.transfer.CreateTargetMailbox(m)
|
||||
|
||||
if err != nil {
|
||||
log.Errorln("Folder/Label creating:", err)
|
||||
s.showError(err)
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO: notify UI of newly added folders/labels
|
||||
/*errc := s.PMStructure.Load(email, false)
|
||||
if errc != nil {
|
||||
s.showError(errc)
|
||||
return false
|
||||
}*/
|
||||
|
||||
if sourceID != "" {
|
||||
if isLabel {
|
||||
s.ExternalStructure.addTargetLabelID(sourceID, newLabel.ID)
|
||||
} else {
|
||||
s.ExternalStructure.setTargetFolderID(sourceID, newLabel.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
55
internal/frontend/qt-ie/frontend_nogui.go
Normal file
55
internal/frontend/qt-ie/frontend_nogui.go
Normal file
@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2020 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 nogui
|
||||
|
||||
package qtie
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var log = logrus.WithField("pkg", "frontend-nogui") //nolint[gochecknoglobals]
|
||||
|
||||
type FrontendHeadless struct{}
|
||||
|
||||
func (s *FrontendHeadless) Loop(credentialsError error) error {
|
||||
log.Info("Check status on localhost:8081")
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "IE is running")
|
||||
})
|
||||
return http.ListenAndServe(":8081", nil)
|
||||
}
|
||||
|
||||
func (s *FrontendHeadless) IsAppRestarting() bool { return false }
|
||||
|
||||
func New(
|
||||
version, buildVersion string,
|
||||
panicHandler types.PanicHandler,
|
||||
config *config.Config,
|
||||
eventListener listener.Listener,
|
||||
updates types.Updater,
|
||||
ie types.ImportExporter,
|
||||
) *FrontendHeadless {
|
||||
return &FrontendHeadless{}
|
||||
}
|
||||
89
internal/frontend/qt-ie/import.go
Normal file
89
internal/frontend/qt-ie/import.go
Normal file
@ -0,0 +1,89 @@
|
||||
// Copyright (c) 2020 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 !nogui
|
||||
|
||||
package qtie
|
||||
|
||||
import "github.com/ProtonMail/proton-bridge/internal/transfer"
|
||||
|
||||
// wrapper for QML
|
||||
func (f *FrontendQt) setupAndLoadForImport(isFromIMAP bool, sourcePath, sourceEmail, sourcePassword, sourceServer, sourcePort, targetAddress string) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
f.showError(err)
|
||||
f.Qml.ImportStructuresLoadFinished(false)
|
||||
} else {
|
||||
f.Qml.ImportStructuresLoadFinished(true)
|
||||
}
|
||||
}()
|
||||
|
||||
if isFromIMAP {
|
||||
f.transfer, err = f.ie.GetRemoteImporter(targetAddress, sourceEmail, sourcePassword, sourceServer, sourcePort)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
f.transfer, err = f.ie.GetLocalImporter(targetAddress, sourcePath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := f.loadStructuresForImport(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FrontendQt) loadStructuresForImport() error {
|
||||
f.PMStructure.Clear()
|
||||
targetMboxes, err := f.transfer.TargetMailboxes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, mbox := range targetMboxes {
|
||||
rule := &transfer.Rule{}
|
||||
f.PMStructure.addEntry(newFolderInfo(mbox, rule))
|
||||
}
|
||||
|
||||
f.ExternalStructure.Clear()
|
||||
sourceMboxes, err := f.transfer.SourceMailboxes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, mbox := range sourceMboxes {
|
||||
rule := f.transfer.GetRule(mbox)
|
||||
f.ExternalStructure.addEntry(newFolderInfo(mbox, rule))
|
||||
}
|
||||
|
||||
f.ExternalStructure.transfer = f.transfer
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FrontendQt) StartImport(email string) { // TODO email not needed
|
||||
f.Qml.SetProgressDescription("init") // TODO use const
|
||||
f.Qml.SetProgressFails(0)
|
||||
f.Qml.SetProgress(0.0)
|
||||
f.Qml.SetTotal(1)
|
||||
f.Qml.SetImportLogFileName("")
|
||||
f.ErrorList.Clear()
|
||||
|
||||
progress := f.transfer.Start()
|
||||
f.setProgressManager(progress)
|
||||
}
|
||||
32
internal/frontend/qt-ie/notification.go
Normal file
32
internal/frontend/qt-ie/notification.go
Normal file
@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2020 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 !nogui
|
||||
|
||||
package qtie
|
||||
|
||||
const (
|
||||
TabGlobal = 0
|
||||
TabSettings = 1
|
||||
TabHelp = 2
|
||||
TabQuit = 4
|
||||
TabAddAccount = -1
|
||||
)
|
||||
|
||||
func (s *FrontendQt) SendNotification(tabIndex int, msg string) {
|
||||
s.Qml.NotifyBubble(tabIndex, msg)
|
||||
}
|
||||
25
internal/frontend/qt-ie/types.go
Normal file
25
internal/frontend/qt-ie/types.go
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2020 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 !nogui
|
||||
|
||||
package qtie
|
||||
|
||||
type panicHandler interface {
|
||||
HandlePanic()
|
||||
SendReport(interface{})
|
||||
}
|
||||
189
internal/frontend/qt-ie/ui.go
Normal file
189
internal/frontend/qt-ie/ui.go
Normal file
@ -0,0 +1,189 @@
|
||||
// Copyright (c) 2020 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 !nogui
|
||||
|
||||
package qtie
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
|
||||
"github.com/therecipe/qt/core"
|
||||
)
|
||||
|
||||
// GoQMLInterface between go and qml
|
||||
//
|
||||
// Here we implements all the signals / methods.
|
||||
type GoQMLInterface struct {
|
||||
core.QObject
|
||||
|
||||
_ func() `constructor:"init"`
|
||||
|
||||
_ string `property:"currentAddress"`
|
||||
_ string `property:"goos"`
|
||||
_ bool `property:"isFirstStart"`
|
||||
_ bool `property:"isRestarting"`
|
||||
_ bool `property:"isConnectionOK"`
|
||||
|
||||
_ string `property:lastError`
|
||||
_ float32 `property:progress`
|
||||
_ string `property:progressDescription`
|
||||
_ int `property:progressFails`
|
||||
_ int `property:total`
|
||||
_ string `property:importLogFileName`
|
||||
|
||||
_ string `property:"programTitle"`
|
||||
_ string `property:"newversion"`
|
||||
_ string `property:"downloadLink"`
|
||||
_ string `property:"landingPage"`
|
||||
_ string `property:"changelog"`
|
||||
_ string `property:"bugfixes"`
|
||||
|
||||
// translations
|
||||
_ string `property:"wrongCredentials"`
|
||||
_ string `property:"wrongMailboxPassword"`
|
||||
_ string `property:"canNotReachAPI"`
|
||||
_ string `property:"credentialsNotRemoved"`
|
||||
_ string `property:"versionCheckFailed"`
|
||||
//
|
||||
_ func(isAvailable bool) `signal:"setConnectionStatus"`
|
||||
_ func(updateState string) `signal:"setUpdateState"`
|
||||
_ func() `slot:"checkInternet"`
|
||||
|
||||
_ func() `signal:"processFinished"`
|
||||
_ func(okay bool) `signal:"exportStructureLoadFinished"`
|
||||
_ func(okay bool) `signal:"importStructuresLoadFinished"`
|
||||
_ func() `signal:"openManual"`
|
||||
_ func(showMessage bool) `signal:"runCheckVersion"`
|
||||
_ func() `slot:"getLocalVersionInfo"`
|
||||
_ func(fname string) `slot:"loadImportReports"`
|
||||
|
||||
_ func() `slot:"quit"`
|
||||
_ func() `slot:"loadAccounts"`
|
||||
_ func() `slot:"openLogs"`
|
||||
_ func() `slot:"openDownloadLink"`
|
||||
_ func() `slot:"openReport"`
|
||||
_ func() `slot:"clearCache"`
|
||||
_ func() `slot:"clearKeychain"`
|
||||
_ func() `signal:"highlightSystray"`
|
||||
_ func() `signal:"normalSystray"`
|
||||
|
||||
_ func(showMessage bool) `slot:"isNewVersionAvailable"`
|
||||
_ func() string `slot:"getBackendVersion"`
|
||||
|
||||
_ func(description, client, address string) bool `slot:"sendBug"`
|
||||
_ func(address, fname string) bool `slot:"sendImportReport"`
|
||||
_ func(address string) `slot:"loadStructureForExport"`
|
||||
_ func() string `slot:"leastUsedColor"`
|
||||
_ func(username string, name string, color string, isLabel bool, sourceID string) bool `slot:"createLabelOrFolder"`
|
||||
_ func(fpath, address, fileType string, attachEncryptedBody bool) `slot:"startExport"`
|
||||
_ func(email string) `slot:"startImport"`
|
||||
_ func() `slot:"resetSource"`
|
||||
|
||||
_ func(isFromIMAP bool, sourcePath, sourceEmail, sourcePassword, sourceServe, sourcePort, targetAddress string) `slot:"setupAndLoadForImport"`
|
||||
|
||||
_ string `property:"progressInit"`
|
||||
|
||||
_ func(path string) int `slot:"checkPathStatus"`
|
||||
|
||||
_ func(evType string, msg string) `signal:"emitEvent"`
|
||||
_ func(tabIndex int, message string) `signal:"notifyBubble"`
|
||||
|
||||
_ func() `signal:"bubbleClosed"`
|
||||
_ func() `signal:"simpleErrorHappen"`
|
||||
_ func() `signal:"askErrorHappen"`
|
||||
_ func() `signal:"retryErrorHappen"`
|
||||
_ func() `signal:"pauseProcess"`
|
||||
_ func() `signal:"resumeProcess"`
|
||||
_ func(clearUnfinished bool) `signal:"cancelProcess"`
|
||||
|
||||
_ func(iAccount int, prefRem bool) `slot:"deleteAccount"`
|
||||
_ func(iAccount int) `slot:"logoutAccount"`
|
||||
_ func(login, password string) int `slot:"login"`
|
||||
_ func(twoFacAuth string) int `slot:"auth2FA"`
|
||||
_ func(mailboxPassword string) int `slot:"addAccount"`
|
||||
_ func(message string, changeIndex int) `signal:"setAddAccountWarning"`
|
||||
|
||||
_ func() `signal:"notifyVersionIsTheLatest"`
|
||||
_ func() `signal:"notifyKeychainRebuild"`
|
||||
_ func() `signal:"notifyHasNoKeychain"`
|
||||
_ func() `signal:"notifyUpdate"`
|
||||
_ func(accname string) `signal:"notifyLogout"`
|
||||
_ func(accname string) `signal:"notifyAddressChanged"`
|
||||
_ func(accname string) `signal:"notifyAddressChangedLogout"`
|
||||
|
||||
_ func() `slot:"startUpdate"`
|
||||
_ func(hasError bool) `signal:"updateFinished"`
|
||||
|
||||
// errors
|
||||
_ func() `signal:"answerRetry"`
|
||||
_ func(all bool) `signal:"answerSkip"`
|
||||
_ func(errCode int) `signal:"notifyError"`
|
||||
_ string `property:"errorDescription"`
|
||||
}
|
||||
|
||||
// Constructor
|
||||
func (s *GoQMLInterface) init() {}
|
||||
|
||||
// SetFrontend connects all slots and signals from Go to QML
|
||||
func (s *GoQMLInterface) SetFrontend(f *FrontendQt) {
|
||||
s.ConnectQuit(f.App.Quit)
|
||||
|
||||
s.ConnectLoadAccounts(f.Accounts.LoadAccounts)
|
||||
s.ConnectOpenLogs(f.openLogs)
|
||||
s.ConnectOpenDownloadLink(f.openDownloadLink)
|
||||
s.ConnectOpenReport(f.openReport)
|
||||
s.ConnectClearCache(f.Accounts.ClearCache)
|
||||
s.ConnectClearKeychain(f.Accounts.ClearKeychain)
|
||||
|
||||
s.ConnectSendBug(f.sendBug)
|
||||
s.ConnectSendImportReport(f.sendImportReport)
|
||||
|
||||
s.ConnectDeleteAccount(f.Accounts.DeleteAccount)
|
||||
s.ConnectLogoutAccount(f.Accounts.LogoutAccount)
|
||||
s.ConnectLogin(f.Accounts.Login)
|
||||
s.ConnectAuth2FA(f.Accounts.Auth2FA)
|
||||
s.ConnectAddAccount(f.Accounts.AddAccount)
|
||||
|
||||
s.SetGoos(runtime.GOOS)
|
||||
s.SetIsRestarting(false)
|
||||
s.SetProgramTitle(f.programName)
|
||||
|
||||
s.ConnectGetLocalVersionInfo(f.getLocalVersionInfo)
|
||||
s.ConnectIsNewVersionAvailable(f.isNewVersionAvailable)
|
||||
s.ConnectGetBackendVersion(func() string {
|
||||
return f.programVersion
|
||||
})
|
||||
|
||||
s.ConnectCheckInternet(f.checkInternet)
|
||||
|
||||
s.ConnectLoadStructureForExport(f.LoadStructureForExport)
|
||||
s.ConnectSetupAndLoadForImport(f.setupAndLoadForImport)
|
||||
s.ConnectResetSource(f.resetSource)
|
||||
s.ConnectLeastUsedColor(f.leastUsedColor)
|
||||
s.ConnectCreateLabelOrFolder(f.createLabelOrFolder)
|
||||
|
||||
s.ConnectStartExport(f.StartExport)
|
||||
s.ConnectStartImport(f.StartImport)
|
||||
|
||||
s.ConnectCheckPathStatus(qtcommon.CheckPathStatus)
|
||||
|
||||
s.ConnectStartUpdate(f.StartUpdate)
|
||||
|
||||
s.ConnectEmitEvent(f.emitEvent)
|
||||
}
|
||||
Reference in New Issue
Block a user