Merge pull request #203 from dweymouth/feature/custom-themes

Add support for custom themes
This commit is contained in:
Drew Weymouth
2023-06-20 18:42:32 -07:00
committed by GitHub
11 changed files with 318 additions and 65 deletions
+1
View File
@@ -89,6 +89,7 @@ type ReplayGainConfig struct {
} }
type ThemeConfig struct { type ThemeConfig struct {
ThemeFile string
Appearance string Appearance string
} }
+1 -1
View File
@@ -37,7 +37,7 @@ func main() {
if h <= 1 { if h <= 1 {
h = 800 h = 800
} }
mainWindow := ui.NewMainWindow(fyneApp, displayName, appVersion, myApp, fyne.NewSize(w, h)) mainWindow := ui.NewMainWindow(fyneApp, appname, displayName, appVersion, myApp, fyne.NewSize(w, h))
myApp.OnReactivate = mainWindow.Show myApp.OnReactivate = mainWindow.Show
go func() { go func() {
+5
View File
@@ -140,6 +140,11 @@ var ResFilterSvg = &fyne.StaticResource{
StaticContent: []byte( StaticContent: []byte(
"<svg fill=\"#000000\" version=\"1.1\" id=\"Capa_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" \r\n\t width=\"800px\" height=\"800px\" viewBox=\"0 0 971.986 971.986\"\r\n\t xml:space=\"preserve\">\r\n<g>\r\n\t<path d=\"M370.216,459.3c10.2,11.1,15.8,25.6,15.8,40.6v442c0,26.601,32.1,40.101,51.1,21.4l123.3-141.3\r\n\t\tc16.5-19.8,25.6-29.601,25.6-49.2V500c0-15,5.7-29.5,15.8-40.601L955.615,75.5c26.5-28.8,6.101-75.5-33.1-75.5h-873\r\n\t\tc-39.2,0-59.7,46.6-33.1,75.5L370.216,459.3z\"/>\r\n</g>\r\n</svg>\r\n"), "<svg fill=\"#000000\" version=\"1.1\" id=\"Capa_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" \r\n\t width=\"800px\" height=\"800px\" viewBox=\"0 0 971.986 971.986\"\r\n\t xml:space=\"preserve\">\r\n<g>\r\n\t<path d=\"M370.216,459.3c10.2,11.1,15.8,25.6,15.8,40.6v442c0,26.601,32.1,40.101,51.1,21.4l123.3-141.3\r\n\t\tc16.5-19.8,25.6-29.601,25.6-49.2V500c0-15,5.7-29.5,15.8-40.601L955.615,75.5c26.5-28.8,6.101-75.5-33.1-75.5h-873\r\n\t\tc-39.2,0-59.7,46.6-33.1,75.5L370.216,459.3z\"/>\r\n</g>\r\n</svg>\r\n"),
} }
var ResDefaultToml = &fyne.StaticResource{
StaticName: "default.toml",
StaticContent: []byte(
"[SupersonicTheme]\nName = \"Default\"\nVersion = \"0.1\"\nSupportsDark = true\nSupportsLight = true\n\n[DarkColors]\nPageBackground = \"#0F0F0F\"\nListHeader = \"#232323\"\nBackground = \"#232323\"\nScrollBar = \"#F3F3F3\"\nButton = \"#14141432\"\nInputBackground = \"#14141432\"\n\n[LightColors]\nPageBackground = \"#FAFAFA\"\nListHeader = \"#E1DFE1\"\nBackground = \"#E1DFE1\"\nScrollBar = \"#565656\"\nButton = \"#C8C8C8F0\"\nDisabledButton = \"#CDCDCDF0\"\nForeground = \"#0A0A0A\"\nPrimary = \"#1919FA\""),
}
var ResLICENSE = &fyne.StaticResource{ var ResLICENSE = &fyne.StaticResource{
StaticName: "LICENSE", StaticName: "LICENSE",
StaticContent: []byte( StaticContent: []byte(
+2
View File
@@ -28,6 +28,8 @@ fyne bundle -append -prefix Res icons/publicdomain/grid.svg >> bundled.go
fyne bundle -append -prefix Res icons/publicdomain/list.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/list.svg >> bundled.go
fyne bundle -append -prefix Res icons/publicdomain/filter.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/filter.svg >> bundled.go
fyne bundle -append -prefix Res themes/default.toml >> bundled.go
fyne bundle -append -prefix Res ../LICENSE >> bundled.go fyne bundle -append -prefix Res ../LICENSE >> bundled.go
fyne bundle -append -prefix Res licenses/BSDLICENSE >> bundled.go fyne bundle -append -prefix Res licenses/BSDLICENSE >> bundled.go
fyne bundle -append -prefix Res licenses/MITLICENSE >> bundled.go fyne bundle -append -prefix Res licenses/MITLICENSE >> bundled.go
+23
View File
@@ -0,0 +1,23 @@
[SupersonicTheme]
Name = "Default"
Version = "0.1"
SupportsDark = true
SupportsLight = true
[DarkColors]
PageBackground = "#0F0F0F"
ListHeader = "#232323"
Background = "#232323"
ScrollBar = "#F3F3F3"
Button = "#14141432"
InputBackground = "#14141432"
[LightColors]
PageBackground = "#FAFAFA"
ListHeader = "#E1DFE1"
Background = "#E1DFE1"
ScrollBar = "#565656"
Button = "#C8C8C8F0"
DisabledButton = "#CDCDCDF0"
Foreground = "#0A0A0A"
Primary = "#1919FA"
+2 -2
View File
@@ -441,14 +441,14 @@ func (c *Controller) ShowAboutDialog() {
pop.Show() pop.Show()
} }
func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func()) { func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map[string]string) {
devs, err := c.App.Player.ListAudioDevices() devs, err := c.App.Player.ListAudioDevices()
if err != nil { if err != nil {
log.Printf("error listing audio devices: %v", err) log.Printf("error listing audio devices: %v", err)
devs = []player.AudioDevice{{Name: "auto", Description: "Autoselect device"}} devs = []player.AudioDevice{{Name: "auto", Description: "Autoselect device"}}
} }
dlg := dialogs.NewSettingsDialog(c.App.Config, devs, c.MainWindow) dlg := dialogs.NewSettingsDialog(c.App.Config, devs, themeFiles, c.MainWindow)
dlg.OnReplayGainSettingsChanged = func() { dlg.OnReplayGainSettingsChanged = func() {
c.App.PlaybackManager.SetReplayGainOptions(c.App.Config.ReplayGain) c.App.PlaybackManager.SetReplayGainOptions(c.App.Config.ReplayGain)
} }
+37 -13
View File
@@ -38,14 +38,15 @@ type SettingsDialog struct {
config *backend.Config config *backend.Config
audioDevices []player.AudioDevice audioDevices []player.AudioDevice
themeFiles map[string]string // filename -> displayName
promptText *widget.RichText promptText *widget.RichText
content fyne.CanvasObject content fyne.CanvasObject
} }
// TODO: having this depend on the player package for the AudioDevice type is kinda gross. Refactor. // TODO: having this depend on the player package for the AudioDevice type is kinda gross. Refactor.
func NewSettingsDialog(config *backend.Config, audioDeviceList []player.AudioDevice, window fyne.Window) *SettingsDialog { func NewSettingsDialog(config *backend.Config, audioDeviceList []player.AudioDevice, themeFileList map[string]string, window fyne.Window) *SettingsDialog {
s := &SettingsDialog{config: config, audioDevices: audioDeviceList} s := &SettingsDialog{config: config, audioDevices: audioDeviceList, themeFiles: themeFileList}
s.ExtendBaseWidget(s) s.ExtendBaseWidget(s)
tabs := container.NewAppTabs( tabs := container.NewAppTabs(
@@ -69,19 +70,39 @@ func NewSettingsDialog(config *backend.Config, audioDeviceList []player.AudioDev
} }
func (s *SettingsDialog) createGeneralTab() *container.TabItem { func (s *SettingsDialog) createGeneralTab() *container.TabItem {
themeSelect := widget.NewSelect([]string{ themeNames := []string{"Default"}
string(myTheme.AppearanceDark), themeFileNames := []string{""}
string(myTheme.AppearanceLight), i, selIndex := 1, 0
string(myTheme.AppearanceAuto)}, nil) for filename, displayname := range s.themeFiles {
themeSelect.OnChanged = func(_ string) { themeFileNames = append(themeFileNames, filename)
s.config.Theme.Appearance = themeSelect.Options[themeSelect.SelectedIndex()] themeNames = append(themeNames, displayname)
if strings.EqualFold(filename, s.config.Theme.ThemeFile) {
selIndex = i
}
i++
}
themeFileSelect := widget.NewSelect(themeNames, nil)
themeFileSelect.SetSelectedIndex(selIndex)
themeFileSelect.OnChanged = func(_ string) {
s.config.Theme.ThemeFile = themeFileNames[themeFileSelect.SelectedIndex()]
if s.OnThemeSettingChanged != nil { if s.OnThemeSettingChanged != nil {
s.OnThemeSettingChanged() s.OnThemeSettingChanged()
} }
} }
themeSelect.SetSelected(s.config.Theme.Appearance) themeModeSelect := widget.NewSelect([]string{
if themeSelect.Selected == "" { string(myTheme.AppearanceDark),
themeSelect.SetSelectedIndex(0) string(myTheme.AppearanceLight),
string(myTheme.AppearanceAuto)}, nil)
themeModeSelect.OnChanged = func(_ string) {
s.config.Theme.Appearance = themeModeSelect.Options[themeModeSelect.SelectedIndex()]
if s.OnThemeSettingChanged != nil {
s.OnThemeSettingChanged()
}
}
themeModeSelect.SetSelected(s.config.Theme.Appearance)
if themeModeSelect.Selected == "" {
themeModeSelect.SetSelectedIndex(0)
} }
startupPage := widget.NewSelect(backend.SupportedStartupPages, func(choice string) { startupPage := widget.NewSelect(backend.SupportedStartupPages, func(choice string) {
@@ -183,8 +204,11 @@ func (s *SettingsDialog) createGeneralTab() *container.TabItem {
scrobbleEnabled.Checked = s.config.Scrobbling.Enabled scrobbleEnabled.Checked = s.config.Scrobbling.Enabled
return container.NewTabItem("General", container.NewVBox( return container.NewTabItem("General", container.NewVBox(
container.New(layout.NewFormLayout(), container.NewBorder(nil, nil, widget.NewLabel("Theme"), /*left*/
widget.NewLabel("Appearance"), container.NewGridWithColumns(2, themeSelect), container.NewHBox(widget.NewLabel("Mode"), themeModeSelect, util.NewHSpace(5)), // right
themeFileSelect, // center
),
container.NewHBox(
widget.NewLabel("Startup page"), container.NewGridWithColumns(2, startupPage), widget.NewLabel("Startup page"), container.NewGridWithColumns(2, startupPage),
), ),
container.NewHBox(systemTrayEnable, closeToTray), container.NewHBox(systemTrayEnable, closeToTray),
+12 -11
View File
@@ -3,6 +3,7 @@ package ui
import ( import (
"fmt" "fmt"
"github.com/20after4/configdir"
"github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/res" "github.com/dweymouth/supersonic/res"
@@ -49,12 +50,12 @@ type MainWindow struct {
container *fyne.Container container *fyne.Container
} }
func NewMainWindow(fyneApp fyne.App, appName, appVersion string, app *backend.App, size fyne.Size) MainWindow { func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, app *backend.App, size fyne.Size) MainWindow {
m := MainWindow{ m := MainWindow{
App: app, App: app,
Window: fyneApp.NewWindow(appName), Window: fyneApp.NewWindow(displayAppName),
BrowsingPane: browsing.NewBrowsingPane(app), BrowsingPane: browsing.NewBrowsingPane(app),
theme: theme.NewMyTheme(&app.Config.Theme), theme: theme.NewMyTheme(&app.Config.Theme, configdir.LocalConfig(appName, "themes")),
} }
m.theme.NormalFont = app.Config.Application.FontNormalTTF m.theme.NormalFont = app.Config.Application.FontNormalTTF
@@ -62,7 +63,7 @@ func NewMainWindow(fyneApp fyne.App, appName, appVersion string, app *backend.Ap
fyneApp.Settings().SetTheme(m.theme) fyneApp.Settings().SetTheme(m.theme)
if app.Config.Application.EnableSystemTray { if app.Config.Application.EnableSystemTray {
m.SetupSystemTrayMenu(appName, fyneApp) m.SetupSystemTrayMenu(displayAppName, fyneApp)
} }
m.Controller = &controller.Controller{ m.Controller = &controller.Controller{
AppVersion: appVersion, AppVersion: appVersion,
@@ -82,10 +83,10 @@ func NewMainWindow(fyneApp fyne.App, appName, appVersion string, app *backend.Ap
m.Window.Resize(size) m.Window.Resize(size)
app.PlaybackManager.OnSongChange(func(song, _ *mediaprovider.Track) { app.PlaybackManager.OnSongChange(func(song, _ *mediaprovider.Track) {
if song == nil { if song == nil {
m.Window.SetTitle(appName) m.Window.SetTitle(displayAppName)
return return
} }
m.Window.SetTitle(fmt.Sprintf("%s %s · %s", song.Name, song.ArtistNames[0], appName)) m.Window.SetTitle(fmt.Sprintf("%s %s · %s", song.Name, song.ArtistNames[0], displayAppName))
}) })
app.ServerManager.OnServerConnected(func() { app.ServerManager.OnServerConnected(func() {
m.BrowsingPane.EnableNavigationButtons() m.BrowsingPane.EnableNavigationButtons()
@@ -93,7 +94,7 @@ func NewMainWindow(fyneApp fyne.App, appName, appVersion string, app *backend.Ap
// check if found new version on startup // check if found new version on startup
if t := app.UpdateChecker.VersionTagFound(); t != "" && t != app.Config.Application.LastCheckedVersion { if t := app.UpdateChecker.VersionTagFound(); t != "" && t != app.Config.Application.LastCheckedVersion {
if t != app.VersionTag() { if t != app.VersionTag() {
m.ShowNewVersionDialog(appName, t) m.ShowNewVersionDialog(displayAppName, t)
} }
m.App.Config.Application.LastCheckedVersion = t m.App.Config.Application.LastCheckedVersion = t
} }
@@ -101,7 +102,7 @@ func NewMainWindow(fyneApp fyne.App, appName, appVersion string, app *backend.Ap
m.App.UpdateChecker.OnUpdatedVersionFound = func() { m.App.UpdateChecker.OnUpdatedVersionFound = func() {
t := m.App.UpdateChecker.VersionTagFound() t := m.App.UpdateChecker.VersionTagFound()
if t != app.VersionTag() { if t != app.VersionTag() {
m.ShowNewVersionDialog(appName, t) m.ShowNewVersionDialog(displayAppName, t)
} }
m.App.Config.Application.LastCheckedVersion = t m.App.Config.Application.LastCheckedVersion = t
} }
@@ -117,10 +118,10 @@ func NewMainWindow(fyneApp fyne.App, appName, appVersion string, app *backend.Ap
m.BrowsingPane.AddSettingsMenuItem("Check for Updates", func() { m.BrowsingPane.AddSettingsMenuItem("Check for Updates", func() {
go func() { go func() {
if t := app.UpdateChecker.CheckLatestVersionTag(); t != "" && t != app.VersionTag() { if t := app.UpdateChecker.CheckLatestVersionTag(); t != "" && t != app.VersionTag() {
m.ShowNewVersionDialog(appName, t) m.ShowNewVersionDialog(displayAppName, t)
} else { } else {
dialog.ShowInformation("No new version found", dialog.ShowInformation("No new version found",
"You are running the latest version of "+appName, "You are running the latest version of "+displayAppName,
m.Window) m.Window)
} }
}() }()
@@ -128,7 +129,7 @@ func NewMainWindow(fyneApp fyne.App, appName, appVersion string, app *backend.Ap
m.BrowsingPane.AddSettingsMenuItem("Settings...", func() { m.BrowsingPane.AddSettingsMenuItem("Settings...", func() {
m.Controller.ShowSettingsDialog(func() { m.Controller.ShowSettingsDialog(func() {
fyneApp.Settings().SetTheme(m.theme) fyneApp.Settings().SetTheme(m.theme)
}) }, m.theme.ListThemeFiles())
}) })
m.BrowsingPane.AddSettingsMenuItem("About...", m.Controller.ShowAboutDialog) m.BrowsingPane.AddSettingsMenuItem("About...", m.Controller.ShowAboutDialog)
m.addNavigationButtons() m.addNavigationButtons()
+103 -37
View File
@@ -1,10 +1,13 @@
package theme package theme
import ( import (
"bytes"
"errors" "errors"
"image/color" "image/color"
"io/ioutil" "io/ioutil"
"log" "log"
"path"
"path/filepath"
"strings" "strings"
"github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend"
@@ -15,7 +18,10 @@ import (
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
) )
const ColorNamePageBackground fyne.ThemeColorName = "PageBackground" const (
ColorNameListHeader fyne.ThemeColorName = "ListHeader"
ColorNamePageBackground fyne.ThemeColorName = "PageBackground"
)
type AppearanceMode string type AppearanceMode string
@@ -33,59 +39,107 @@ var (
) )
type MyTheme struct { type MyTheme struct {
NormalFont string NormalFont string
BoldFont string BoldFont string
config *backend.ThemeConfig config *backend.ThemeConfig
themeFileDir string
loadedThemeFilename string
loadedThemeFile *ThemeFile
defaultThemeFile *ThemeFile
} }
var _ fyne.Theme = (*MyTheme)(nil) var _ fyne.Theme = (*MyTheme)(nil)
func NewMyTheme(config *backend.ThemeConfig) *MyTheme { func NewMyTheme(config *backend.ThemeConfig, themeFileDir string) *MyTheme {
m := &MyTheme{config: config} m := &MyTheme{config: config, themeFileDir: themeFileDir}
m.defaultThemeFile, _ = DecodeThemeFile(bytes.NewReader(res.ResDefaultToml.StaticContent))
m.createThemeIcons() m.createThemeIcons()
return m return m
} }
func (m *MyTheme) Color(name fyne.ThemeColorName, _ fyne.ThemeVariant) color.Color { func (m *MyTheme) Color(name fyne.ThemeColorName, _ fyne.ThemeVariant) color.Color {
// load theme file if necessary
if m.loadedThemeFile == nil || m.config.ThemeFile != m.loadedThemeFilename {
t, err := ReadThemeFile(path.Join(m.themeFileDir, m.config.ThemeFile))
if err == nil {
m.loadedThemeFile = t
} else {
log.Printf("failed to load theme file %q: %s", m.config.ThemeFile, err.Error())
m.loadedThemeFile = m.defaultThemeFile
}
m.loadedThemeFilename = m.config.ThemeFile
}
variant := m.getVariant() variant := m.getVariant()
thFile := m.loadedThemeFile
if !thFile.SupportsVariant(variant) {
thFile = m.defaultThemeFile
}
colors := thFile.DarkColors
defColors := m.defaultThemeFile.DarkColors
if variant == theme.VariantLight {
colors = thFile.LightColors
defColors = m.defaultThemeFile.LightColors
}
switch name { switch name {
case ColorNameListHeader:
return colorOrDefault(colors.ListHeader, defColors.ListHeader, name, variant)
case ColorNamePageBackground: case ColorNamePageBackground:
if variant == theme.VariantDark { return colorOrDefault(colors.PageBackground, defColors.PageBackground, name, variant)
return color.RGBA{R: 15, G: 15, B: 15, A: 255}
}
return color.RGBA{R: 250, G: 250, B: 250, A: 255}
case theme.ColorNameBackground: case theme.ColorNameBackground:
if variant == theme.VariantDark { return colorOrDefault(colors.Background, defColors.Background, name, variant)
return color.RGBA{R: 35, G: 35, B: 35, A: 255}
}
return color.RGBA{R: 225, G: 223, B: 225, A: 255}
case theme.ColorNameScrollBar:
if variant == theme.VariantDark {
return theme.DarkTheme().Color(theme.ColorNameForeground, variant)
}
return theme.LightTheme().Color(theme.ColorNameForeground, variant)
case theme.ColorNameButton: case theme.ColorNameButton:
if variant == theme.VariantDark { return colorOrDefault(colors.Button, defColors.Button, name, variant)
return color.RGBA{R: 20, G: 20, B: 20, A: 50} case theme.ColorNameDisabled:
} return colorOrDefault(colors.Disabled, defColors.Disabled, name, variant)
return color.RGBA{R: 200, G: 200, B: 200, A: 240}
case theme.ColorNameDisabledButton: case theme.ColorNameDisabledButton:
if variant == theme.VariantLight { return colorOrDefault(colors.DisabledButton, defColors.DisabledButton, name, variant)
return color.RGBA{R: 205, G: 205, B: 205, A: 240} case theme.ColorNameError:
} return colorOrDefault(colors.Error, defColors.Error, name, variant)
return theme.DefaultTheme().Color(theme.ColorNameDisabledButton, variant) case theme.ColorNameFocus:
case theme.ColorNameInputBackground: return colorOrDefault(colors.Focus, defColors.Focus, name, variant)
if variant == theme.VariantDark {
return color.RGBA{R: 20, G: 20, B: 20, A: 50}
}
case theme.ColorNameForeground: case theme.ColorNameForeground:
if variant == theme.VariantLight { return colorOrDefault(colors.Foreground, defColors.Foreground, name, variant)
return color.RGBA{R: 10, G: 10, B: 10, A: 255} case theme.ColorNameHover:
} return colorOrDefault(colors.Hover, defColors.Hover, name, variant)
case theme.ColorNameInputBackground:
return colorOrDefault(colors.InputBackground, defColors.InputBackground, name, variant)
case theme.ColorNameInputBorder:
return colorOrDefault(colors.InputBorder, defColors.InputBorder, name, variant)
case theme.ColorNameMenuBackground:
return colorOrDefault(colors.MenuBackground, defColors.MenuBackground, name, variant)
case theme.ColorNameOverlayBackground:
return colorOrDefault(colors.OverlayBackground, defColors.OverlayBackground, name, variant)
case theme.ColorNamePlaceHolder:
return colorOrDefault(colors.Placeholder, defColors.Placeholder, name, variant)
case theme.ColorNamePressed:
return colorOrDefault(colors.Pressed, defColors.Pressed, name, variant)
case theme.ColorNamePrimary: case theme.ColorNamePrimary:
if variant == theme.VariantLight { return colorOrDefault(colors.Primary, defColors.Primary, name, variant)
return color.RGBA{R: 25, G: 25, B: 250, A: 255} case theme.ColorNameScrollBar:
} return colorOrDefault(colors.ScrollBar, defColors.ScrollBar, name, variant)
case theme.ColorNameSelection:
return colorOrDefault(colors.Selection, defColors.Selection, name, variant)
case theme.ColorNameSeparator:
return colorOrDefault(colors.Separator, defColors.Separator, name, variant)
case theme.ColorNameShadow:
return colorOrDefault(colors.Shadow, defColors.Shadow, name, variant)
case theme.ColorNameSuccess:
return colorOrDefault(colors.Success, defColors.Success, name, variant)
case theme.ColorNameWarning:
return colorOrDefault(colors.Warning, defColors.Warning, name, variant)
default:
return colorOrDefault("", "", name, variant)
}
}
func colorOrDefault(colorStr, defColorStr string, name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
if c, err := ColorStringToColor(colorStr); err == nil {
return c
}
if c, err := ColorStringToColor(defColorStr); err == nil {
return c
} }
return theme.DefaultTheme().Color(name, variant) return theme.DefaultTheme().Color(name, variant)
} }
@@ -94,6 +148,18 @@ func (m *MyTheme) Icon(name fyne.ThemeIconName) fyne.Resource {
return theme.DefaultTheme().Icon(name) return theme.DefaultTheme().Icon(name)
} }
// Returns a map [themeFileName] -> displayName
func (m *MyTheme) ListThemeFiles() map[string]string {
files, _ := filepath.Glob(m.themeFileDir + "/*.toml")
result := make(map[string]string)
for _, filepath := range files {
if themeFile, err := ReadThemeFile(filepath); err == nil {
result[path.Base(filepath)] = themeFile.SupersonicTheme.Name
}
}
return result
}
type myThemedResource struct { type myThemedResource struct {
myTheme MyTheme myTheme MyTheme
darkVariant *fyne.StaticResource darkVariant *fyne.StaticResource
+131
View File
@@ -0,0 +1,131 @@
package theme
import (
"encoding/hex"
"errors"
"fmt"
"image/color"
"io"
"os"
"strings"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/theme"
"github.com/dweymouth/supersonic/sharedutil"
"github.com/pelletier/go-toml/v2"
)
type ThemeFileHeader struct {
Name string
Version string
SupportsDark bool
SupportsLight bool
}
type ThemeFile struct {
SupersonicTheme ThemeFileHeader
DarkColors ThemeColors
LightColors ThemeColors
}
type ThemeColors struct {
// Supersonic-specific colors
ListHeader string
PageBackground string
// Fyne colors
Background string
Button string
DisabledButton string
Disabled string
Error string
Focus string
Foreground string
Hover string
InputBackground string
InputBorder string
MenuBackground string
OverlayBackground string
Placeholder string
Pressed string
Primary string
ScrollBar string
Selection string
Separator string
Shadow string
Success string
Warning string
}
func ReadThemeFile(filePath string) (*ThemeFile, error) {
f, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer f.Close()
return DecodeThemeFile(f)
}
func DecodeThemeFile(reader io.Reader) (*ThemeFile, error) {
theme := &ThemeFile{}
if err := toml.NewDecoder(reader).Decode(theme); err != nil {
return nil, err
}
if theme.SupersonicTheme.Name == "" || theme.SupersonicTheme.Version != "0.1" {
return nil, errors.New("invalid theme file name or version")
}
if !(theme.SupersonicTheme.SupportsDark || theme.SupersonicTheme.SupportsLight) {
return nil, errors.New("invalid theme file: must support one or both of light/dark")
}
return theme, nil
}
func (t *ThemeFile) SupportsVariant(v fyne.ThemeVariant) bool {
if v == theme.VariantDark {
return t.SupersonicTheme.SupportsDark
}
return t.SupersonicTheme.SupportsLight
}
// Parses a CSS-style #RRGGBB or #RRGGBBAA string
func ColorStringToColor(colorStr string) (color.Color, error) {
if !strings.HasPrefix(colorStr, "#") || !sharedutil.SliceContains([]int{7, 9}, len(colorStr)) {
return color.Black, errors.New("invalid color string")
}
colorBytes := make([]byte, 4)
n, err := hex.Decode(colorBytes, []byte(colorStr[1:]))
if err != nil {
return color.Black, fmt.Errorf("invalid color string: %s", err.Error())
}
if n == 3 {
colorBytes[3] = 255 // opaque alpha
}
return color.RGBA{R: colorBytes[0], G: colorBytes[1], B: colorBytes[2], A: colorBytes[3]}, nil
}
+1 -1
View File
@@ -62,7 +62,7 @@ func NewListHeader(cols []ListColumn, layout *layouts.ColumnsLayout) *ListHeader
for i := range l.columnVisible { for i := range l.columnVisible {
l.columnVisible[i] = true l.columnVisible[i] = true
} }
l.container = container.NewMax(myTheme.NewThemedRectangle(theme.ColorNameBackground), l.columnsContainer) l.container = container.NewMax(myTheme.NewThemedRectangle(myTheme.ColorNameListHeader), l.columnsContainer)
l.ExtendBaseWidget(l) l.ExtendBaseWidget(l)
l.buildColumns() l.buildColumns()
return l return l