diff --git a/backend/config.go b/backend/config.go index 891a3fe..c58a7bd 100644 --- a/backend/config.go +++ b/backend/config.go @@ -89,6 +89,7 @@ type ReplayGainConfig struct { } type ThemeConfig struct { + ThemeFile string Appearance string } diff --git a/res/bundled.go b/res/bundled.go index e7f4497..da4fd5d 100644 --- a/res/bundled.go +++ b/res/bundled.go @@ -140,6 +140,11 @@ var ResFilterSvg = &fyne.StaticResource{ StaticContent: []byte( "\r\n\r\n\t\r\n\r\n\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\"\nBackground = \"#232323\"\nScrollBar = \"#F3F3F3\"\nButton = \"#14141432\"\nInputBackground = \"#14141432\"\n\n[LightColors]\nPageBackground = \"#FAFAFA\"\nBackground = \"#E1DFE1\"\nScrollBar = \"#565656\"\nButton = \"#C8C8C8F0\"\nDisabledButton = \"#CDCDCDF0\"\nForeground = \"#0A0A0A\"\nPrimary = \"#1919FA\""), +} var ResLICENSE = &fyne.StaticResource{ StaticName: "LICENSE", StaticContent: []byte( diff --git a/res/bundled_gen.sh b/res/bundled_gen.sh index cc2ee27..91d8f88 100755 --- a/res/bundled_gen.sh +++ b/res/bundled_gen.sh @@ -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/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 licenses/BSDLICENSE >> bundled.go fyne bundle -append -prefix Res licenses/MITLICENSE >> bundled.go diff --git a/res/themes/default.toml b/res/themes/default.toml new file mode 100644 index 0000000..7903deb --- /dev/null +++ b/res/themes/default.toml @@ -0,0 +1,21 @@ +[SupersonicTheme] +Name = "Default" +Version = "0.1" +SupportsDark = true +SupportsLight = true + +[DarkColors] +PageBackground = "#0F0F0F" +Background = "#232323" +ScrollBar = "#F3F3F3" +Button = "#14141432" +InputBackground = "#14141432" + +[LightColors] +PageBackground = "#FAFAFA" +Background = "#E1DFE1" +ScrollBar = "#565656" +Button = "#C8C8C8F0" +DisabledButton = "#CDCDCDF0" +Foreground = "#0A0A0A" +Primary = "#1919FA" \ No newline at end of file diff --git a/ui/theme/theme.go b/ui/theme/theme.go index c739156..49c311a 100644 --- a/ui/theme/theme.go +++ b/ui/theme/theme.go @@ -1,10 +1,13 @@ package theme import ( + "bytes" "errors" "image/color" "io/ioutil" "log" + "path" + "path/filepath" "strings" "github.com/dweymouth/supersonic/backend" @@ -33,59 +36,100 @@ var ( ) type MyTheme struct { - NormalFont string - BoldFont string - config *backend.ThemeConfig + NormalFont string + BoldFont string + config *backend.ThemeConfig + themeFileDir string + + loadedThemeFilename string + loadedThemeFile *ThemeFile + defaultThemeFile *ThemeFile } var _ fyne.Theme = (*MyTheme)(nil) func NewMyTheme(config *backend.ThemeConfig) *MyTheme { m := &MyTheme{config: config} + m.defaultThemeFile, _ = DecodeThemeFile(bytes.NewReader(res.ResDefaultToml.StaticContent)) m.createThemeIcons() return m } 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() + thFile := m.loadedThemeFile + if !thFile.SupportsVariant(variant) { + thFile = m.defaultThemeFile + } + colors := thFile.DarkColors + if variant == theme.VariantLight { + colors = thFile.LightColors + } switch name { case ColorNamePageBackground: - if variant == theme.VariantDark { - return color.RGBA{R: 15, G: 15, B: 15, A: 255} - } - return color.RGBA{R: 250, G: 250, B: 250, A: 255} + return colorOrDefault(colors.PageBackground, name, variant) case theme.ColorNameBackground: - if variant == theme.VariantDark { - 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) + return colorOrDefault(colors.Background, name, variant) case theme.ColorNameButton: - if variant == theme.VariantDark { - return color.RGBA{R: 20, G: 20, B: 20, A: 50} - } - return color.RGBA{R: 200, G: 200, B: 200, A: 240} + return colorOrDefault(colors.Button, name, variant) + case theme.ColorNameDisabled: + return colorOrDefault(colors.Disabled, name, variant) case theme.ColorNameDisabledButton: - if variant == theme.VariantLight { - return color.RGBA{R: 205, G: 205, B: 205, A: 240} - } - return theme.DefaultTheme().Color(theme.ColorNameDisabledButton, variant) - case theme.ColorNameInputBackground: - if variant == theme.VariantDark { - return color.RGBA{R: 20, G: 20, B: 20, A: 50} - } + return colorOrDefault(colors.DisabledButton, name, variant) + case theme.ColorNameError: + return colorOrDefault(colors.Error, name, variant) + case theme.ColorNameFocus: + return colorOrDefault(colors.Focus, name, variant) case theme.ColorNameForeground: - if variant == theme.VariantLight { - return color.RGBA{R: 10, G: 10, B: 10, A: 255} - } + return colorOrDefault(colors.Foreground, name, variant) + case theme.ColorNameHover: + return colorOrDefault(colors.Hover, name, variant) + case theme.ColorNameInputBackground: + return colorOrDefault(colors.InputBackground, name, variant) + case theme.ColorNameInputBorder: + return colorOrDefault(colors.InputBorder, name, variant) + case theme.ColorNameMenuBackground: + return colorOrDefault(colors.MenuBackground, name, variant) + case theme.ColorNameOverlayBackground: + return colorOrDefault(colors.OverlayBackground, name, variant) + case theme.ColorNamePlaceHolder: + return colorOrDefault(colors.Placeholder, name, variant) + case theme.ColorNamePressed: + return colorOrDefault(colors.Pressed, name, variant) case theme.ColorNamePrimary: - if variant == theme.VariantLight { - return color.RGBA{R: 25, G: 25, B: 250, A: 255} - } + return colorOrDefault(colors.Primary, name, variant) + case theme.ColorNameScrollBar: + return colorOrDefault(colors.ScrollBar, name, variant) + case theme.ColorNameSelection: + return colorOrDefault(colors.Selection, name, variant) + case theme.ColorNameSeparator: + return colorOrDefault(colors.Separator, name, variant) + case theme.ColorNameShadow: + return colorOrDefault(colors.Shadow, name, variant) + case theme.ColorNameSuccess: + return colorOrDefault(colors.Success, name, variant) + case theme.ColorNameWarning: + return colorOrDefault(colors.Warning, name, variant) + default: + return colorOrDefault("", name, variant) + } +} + +func colorOrDefault(colorStr string, name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color { + if c, err := ColorStringToColor(colorStr); err == nil { + return c } return theme.DefaultTheme().Color(name, variant) } @@ -94,6 +138,18 @@ func (m *MyTheme) Icon(name fyne.ThemeIconName) fyne.Resource { 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 _, filename := range files { + if themeFile, err := ReadThemeFile(path.Join(m.themeFileDir, filename)); err == nil { + result[filename] = themeFile.SupersonicTheme.Name + } + } + return result +} + type myThemedResource struct { myTheme MyTheme darkVariant *fyne.StaticResource diff --git a/ui/theme/themefile.go b/ui/theme/themefile.go index 950df01..a9c6850 100644 --- a/ui/theme/themefile.go +++ b/ui/theme/themefile.go @@ -5,9 +5,12 @@ import ( "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" ) @@ -83,8 +86,12 @@ func ReadThemeFile(filePath string) (*ThemeFile, error) { } defer f.Close() + return DecodeThemeFile(f) +} + +func DecodeThemeFile(reader io.Reader) (*ThemeFile, error) { theme := &ThemeFile{} - if err := toml.NewDecoder(f).Decode(theme); err != nil { + if err := toml.NewDecoder(reader).Decode(theme); err != nil { return nil, err } @@ -98,6 +105,13 @@ func ReadThemeFile(filePath string) (*ThemeFile, error) { 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)) {