diff --git a/internal/config/settings/settings.go b/internal/config/settings/settings.go index feb76b29..be2a85a9 100644 --- a/internal/config/settings/settings.go +++ b/internal/config/settings/settings.go @@ -53,6 +53,7 @@ const ( IMAPWorkers = "imap_workers" FetchWorkers = "fetch_workers" AttachmentWorkers = "attachment_workers" + ColorScheme = "color_scheme" ) type Settings struct { @@ -100,6 +101,7 @@ func (s *Settings) setDefaultValues() { s.setDefault(IMAPWorkers, "16") s.setDefault(FetchWorkers, "16") s.setDefault(AttachmentWorkers, "16") + s.setDefault(ColorScheme, "") s.setDefault(APIPortKey, DefaultAPIPort) s.setDefault(IMAPPortKey, DefaultIMAPPort) diff --git a/internal/frontend/qml/Bridge.qml b/internal/frontend/qml/Bridge.qml index 36c605ea..0f61c094 100644 --- a/internal/frontend/qml/Bridge.qml +++ b/internal/frontend/qml/Bridge.qml @@ -20,6 +20,7 @@ import QtQuick 2.13 import QtQuick.Window 2.13 import Qt.labs.platform 1.1 +import Proton 4.0 import Notifications 1.0 QtObject { @@ -58,6 +59,7 @@ QtObject { onCacheUnavailable: { mainWindow.showAndRise() } + onColorSchemeNameChanged: root.setColorScheme() } } @@ -206,15 +208,15 @@ QtObject { switch (reason) { case SystemTrayIcon.Unknown: - break; + break; case SystemTrayIcon.Context: case SystemTrayIcon.Trigger: case SystemTrayIcon.DoubleClick: case SystemTrayIcon.MiddleClick: - calcStatusWindowPosition() - toggleWindow(statusWindow) + calcStatusWindowPosition() + toggleWindow(statusWindow) break; - default: + default: break; } } @@ -225,6 +227,9 @@ QtObject { console.log("backend not loaded") } + root.setColorScheme() + + if (!root.backend.users) { console.log("users not loaded") } @@ -253,4 +258,9 @@ QtObject { root.backend.guiReady() } + + function setColorScheme() { + if (root.backend.colorSchemeName == "light") ProtonStyle.currentStyle = ProtonStyle.lightStyle + if (root.backend.colorSchemeName == "dark") ProtonStyle.currentStyle = ProtonStyle.darkStyle + } } diff --git a/internal/frontend/qml/Bridge_test.qml b/internal/frontend/qml/Bridge_test.qml index 4890c540..41b62bc0 100644 --- a/internal/frontend/qml/Bridge_test.qml +++ b/internal/frontend/qml/Bridge_test.qml @@ -321,7 +321,7 @@ Window { onCheckedChanged: { if (checked && ProtonStyle.currentStyle !== ProtonStyle.lightStyle) { - ProtonStyle.currentStyle = ProtonStyle.lightStyle + root.colorSchemeName = "light" } } } @@ -336,7 +336,7 @@ Window { onCheckedChanged: { if (checked && ProtonStyle.currentStyle !== ProtonStyle.darkStyle) { - ProtonStyle.currentStyle = ProtonStyle.darkStyle + root.colorSchemeName = "dark" } } } @@ -777,6 +777,12 @@ Window { property url releaseNotesLink: Qt.resolvedUrl("https://protonmail.com/download/bridge/early_releases.html") property url landingPageLink: Qt.resolvedUrl("https://protonmail.com/bridge") + property string colorSchemeName: "light" + function changeColorScheme(newScheme){ + root.colorSchemeName = newScheme + } + + property string currentEmailClient: "" // "Apple Mail 14.0" function updateCurrentMailClient(){ currentEmailClient = "Apple Mail 14.0" diff --git a/internal/frontend/qml/GeneralSettings.qml b/internal/frontend/qml/GeneralSettings.qml index 9549b7ea..c52122db 100644 --- a/internal/frontend/qml/GeneralSettings.qml +++ b/internal/frontend/qml/GeneralSettings.qml @@ -129,6 +129,19 @@ SettingsView { Layout.fillWidth: true } + SettingsItem { + id: darkMode + visible: root._isAdvancedShown + colorScheme: root.colorScheme + text: qsTr("Dark mode") + description: qsTr("Choose dark color theme.") + type: SettingsItem.Toggle + checked: root.backend.colorSchemeName == "dark" + onClicked: root.backend.changeColorScheme( darkMode.checked ? "light" : "dark") + + Layout.fillWidth: true + } + SettingsItem { id: ports visible: root._isAdvancedShown diff --git a/internal/frontend/qml/Proton/Style.qml b/internal/frontend/qml/Proton/Style.qml index 49a48239..083e2cf8 100644 --- a/internal/frontend/qml/Proton/Style.qml +++ b/internal/frontend/qml/Proton/Style.qml @@ -36,7 +36,7 @@ QtObject { property ColorScheme lightStyle: ColorScheme { id: _lightStyle - prominent: prominentStyle + prominent: lightProminentStyle // Primary primay_norm: "#657EE4" @@ -107,8 +107,8 @@ QtObject { welcome_img: "icons/img-welcome.png" } - property ColorScheme prominentStyle: ColorScheme { - id: _prominentStyle + property ColorScheme lightProminentStyle: ColorScheme { + id: _lightProminentStyle prominent: this @@ -184,7 +184,7 @@ QtObject { property ColorScheme darkStyle: ColorScheme { id: _darkStyle - prominent: prominentStyle + prominent: darkProminentStyle // Primary primay_norm: "#657EE4" @@ -245,8 +245,82 @@ QtObject { signal_info_active: "#3D99EB" // Shadows - shadow_norm: "#262A33" - shadow_lifted: "#262A33" + shadow_norm: "#262A33" // #000000 32% x+0 y+1 blur:4 + shadow_lifted: "#262A33" // #000000 40% x+0 y+8 blur:24 + + // Backdrop + backdrop_norm: "#52000000" + + // Images + welcome_img: "icons/img-welcome-dark.png" + } + + property ColorScheme darkProminentStyle: ColorScheme { + id: _darkProminentStyle + + prominent: this + + // Primary + primay_norm: "#657EE4" + + // Interaction-norm + interaction_norm: "#657EE4" + interaction_norm_hover: "#7D92E8" + interaction_norm_active: "#98A9EE" + + // Text + text_norm: "#FFFFFF" + text_weak: "#A4A9B5" + text_hint: "#696F7D" + text_disabled: "#575D6B" + text_invert: "#262A33" + + // Field + field_norm: "#575D6B" + field_hover: "#696F7D" + field_disabled: "#464B58" + + // Border + border_norm: "#464B58" + border_weak: "#363A46" + + // Background + background_norm: "#1A1D24" + background_weak: "#2E323C" + background_strong: "#363A46" + background_avatar: "#575D6B" + + // Interaction-weak + interaction_weak: "#464B58" + interaction_weak_hover: "#575D6B" + interaction_weak_active: "#696F7D" + + // Interaction-default + interaction_default: "#00000000" + interaction_default_hover: "#33575D6B" + interaction_default_active: "#4D575D6B" + + // Scrollbar + scrollbar_norm: "#464B58" + scrollbar_hover: "#575D6B" + + // Signal + signal_danger: "#ED4C51" + signal_danger_hover: "#F7595E" + signal_danger_active: "#FF666B" + signal_warning: "#F5930A" + signal_warning_hover: "#F5A716" + signal_warning_active: "#F5B922" + signal_success: "#349172" + signal_success_hover: "#339C79" + signal_success_active: "#31A67F" + signal_info: "#2C89DB" + signal_info_hover: "#3491E3" + signal_info_active: "#3D99EB" + + // Shadows + shadow_norm: "#262A33" // #000000 32% x+0 y+1 blur:4 + shadow_lifted: "#262A33" // #000000 40% x+0 y+8 blur:24 // Backdrop backdrop_norm: "#52000000" @@ -255,8 +329,6 @@ QtObject { welcome_img: "icons/img-welcome-dark.png" } - // TODO: if default style should be loaded from somewhere - // (i.e. from preferencies file) - it should be loaded here property ColorScheme currentStyle: lightStyle property string font_family: { diff --git a/internal/frontend/qt/frontend_settings.go b/internal/frontend/qt/frontend_settings.go index ab56970b..2e25a255 100644 --- a/internal/frontend/qt/frontend_settings.go +++ b/internal/frontend/qt/frontend_settings.go @@ -26,6 +26,7 @@ import ( "github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/frontend/clientconfig" + "github.com/ProtonMail/proton-bridge/internal/frontend/theme" "github.com/ProtonMail/proton-bridge/pkg/keychain" "github.com/ProtonMail/proton-bridge/pkg/ports" "github.com/therecipe/qt/core" @@ -184,3 +185,21 @@ func (f *FrontendQt) quit() { func (f *FrontendQt) guiReady() { f.initializationDone.Do(f.initializing.Done) } + +func (f *FrontendQt) setColorScheme() { + current := f.settings.Get(settings.ColorScheme) + if !theme.IsAvailable(theme.Theme(current)) { + current = string(theme.DefaultTheme()) + f.settings.Set(settings.ColorScheme, current) + } + f.qml.SetColorSchemeName(current) +} + +func (f *FrontendQt) changeColorScheme(newScheme string) { + if !theme.IsAvailable(theme.Theme(newScheme)) { + f.log.WithField("scheme", newScheme).Warn("Color scheme not available") + return + } + f.settings.Set(settings.ColorScheme, newScheme) + f.setColorScheme() +} diff --git a/internal/frontend/qt/qml_backend.go b/internal/frontend/qt/qml_backend.go index 62b229b7..41c67956 100644 --- a/internal/frontend/qt/qml_backend.go +++ b/internal/frontend/qt/qml_backend.go @@ -128,6 +128,8 @@ type QMLBackend struct { _ core.QUrl `property:"releaseNotesLink"` _ core.QUrl `property:"landingPageLink"` + _ string `property:"colorSchemeName"` + _ func(string) `slot:"changeColorScheme"` _ string `property:"currentEmailClient"` _ func() `slot:"updateCurrentMailClient"` _ func(description, address, emailClient string, includeLogs bool) `slot:"reportBug"` @@ -262,6 +264,14 @@ func (q *QMLBackend) setup(f *FrontendQt) { // release notes link is set by update f.setLicensePath() + f.setColorScheme() + q.ConnectChangeColorScheme(func(newScheme string) { + go func() { + defer f.panicHandler.HandlePanic() + f.changeColorScheme(newScheme) + }() + }) + f.setCurrentEmailClient() q.ConnectUpdateCurrentMailClient(func() { go func() { diff --git a/internal/frontend/theme/detect_darwin.go b/internal/frontend/theme/detect_darwin.go new file mode 100644 index 00000000..fabf10fc --- /dev/null +++ b/internal/frontend/theme/detect_darwin.go @@ -0,0 +1,34 @@ +// 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 . + +//go:build darwin +// +build darwin + +package theme + +import ( + "os/exec" + "strings" +) + +func detectSystemTheme() Theme { + out, err := exec.Command("defaults", "read", "-g", "AppleInterfaceStyle").Output() //nolint[gosec] + if err == nil && strings.TrimSpace(string(out)) == "Dark" { + return Dark + } + return Light +} diff --git a/internal/frontend/theme/detect_default.go b/internal/frontend/theme/detect_default.go new file mode 100644 index 00000000..834df4c9 --- /dev/null +++ b/internal/frontend/theme/detect_default.go @@ -0,0 +1,25 @@ +// 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 . + +//go:build !windows && !darwin +// +build !windows,!darwin + +package theme + +func detectSystemTheme() Theme { + return Light +} diff --git a/internal/frontend/theme/detect_windows.go b/internal/frontend/theme/detect_windows.go new file mode 100644 index 00000000..0157dcb7 --- /dev/null +++ b/internal/frontend/theme/detect_windows.go @@ -0,0 +1,53 @@ +// 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 . + +//go:build windows +// +build windows + +package theme + +import ( + "github.com/sirupsen/logrus" + "golang.org/x/sys/windows/registry" +) + +func detectSystemTheme() Theme { + log := logrus.WithField("pkg", "theme") + k, err := registry.OpenKey( + registry.CURRENT_USER, + `SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize`, + registry.QUERY_VALUE, + ) + + if err != nil { + log.WithError(err).Error("Not able to open register") + return Light + } + defer k.Close() + + i, _, err := k.GetIntegerValue("AppsUseLightTheme") + if err != nil { + log.WithError(err).Error("Cannot get value") + return Light + } + + if i == 0 { + return Dark + } + + return Light +} diff --git a/internal/frontend/theme/theme.go b/internal/frontend/theme/theme.go new file mode 100644 index 00000000..e416aef6 --- /dev/null +++ b/internal/frontend/theme/theme.go @@ -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 . + +package theme + +import ( + "runtime" +) + +type Theme string + +const ( + Light = Theme("light") + Dark = Theme("dark") +) + +func IsAvailable(have Theme) bool { + return have == Light || have == Dark +} + +func DefaultTheme() Theme { + switch runtime.GOOS { + case "darwin", "windows": + return detectSystemTheme() + default: + return Light + } +} diff --git a/internal/frontend/theme/theme_test.go b/internal/frontend/theme/theme_test.go new file mode 100644 index 00000000..69e0b160 --- /dev/null +++ b/internal/frontend/theme/theme_test.go @@ -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 . + +// Package settings provides access to persistent user settings. +package theme + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIsAvailable(t *testing.T) { + r := require.New(t) + + want := "dark" + + r.True(IsAvailable("dark")) + r.True(IsAvailable(Dark)) + r.True(IsAvailable(Theme(want))) + + want = "light" + r.True(IsAvailable("light")) + r.True(IsAvailable(Light)) + r.True(IsAvailable(Theme(want))) + + want = "molokai" + r.False(IsAvailable("")) + r.False(IsAvailable("molokai")) + r.False(IsAvailable(Theme(want))) +}