GoToSocial/internal/processing/account/themes.go
tobi 8953f57d88
[feature] User-selectable preset CSS themes for accounts (#2777)
* [feature] User-selectable preset themes

* docs, more theme stuff

* lint, tests

* fix css name

* correct some little issues

* add another theme

* fix poll background

* okay last theme i swear

* make retrieval of apimodel themes more conventional

* preallocate stylesheet slices
2024-03-25 17:32:24 +00:00

152 lines
4.1 KiB
Go

// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package account
import (
"cmp"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"codeberg.org/gruf/go-bytesize"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
)
var (
themeTitleRegex = regexp.MustCompile(`(?m)^\ *theme-title:(.*)$`)
themeDescriptionRegex = regexp.MustCompile(`(?m)^\ *theme-description:(.*)$`)
)
// GetThemes returns available account css themes.
func (p *Processor) ThemesGet() []apimodel.Theme {
return p.converter.ThemesToAPIThemes(p.themes.SortedByTitle)
}
// Themes represents an in-memory
// storage structure for themes.
type Themes struct {
// Themes sorted alphabetically
// by title (case insensitive).
SortedByTitle []*gtsmodel.Theme
// ByFileName contains themes retrievable
// by their filename eg., `light-blurple.css`.
ByFileName map[string]*gtsmodel.Theme
}
// PopulateThemes parses available account CSS
// themes from the web assets themes directory.
func PopulateThemes() *Themes {
webAssetsAbsFilePath, err := filepath.Abs(config.GetWebAssetBaseDir())
if err != nil {
log.Panicf(nil, "error getting abs path for web assets: %v", err)
}
themesAbsFilePath := filepath.Join(webAssetsAbsFilePath, "themes")
themesFiles, err := os.ReadDir(themesAbsFilePath)
if err != nil {
log.Warnf(nil, "error reading themes at %s: %v", themesAbsFilePath, err)
return nil
}
themes := &Themes{
ByFileName: make(map[string]*gtsmodel.Theme),
}
for _, f := range themesFiles {
// Ignore nested directories.
if f.IsDir() {
continue
}
// Ignore weird files.
info, err := f.Info()
if err != nil {
continue
}
// Ignore really big files.
if info.Size() > int64(bytesize.MiB) {
continue
}
// Get just the name of the
// file, eg `blurple-light.css`.
fileName := f.Name()
// Get just the `.css` part.
extensionWithDot := filepath.Ext(fileName)
// Remove any leading `.`
extension := strings.TrimPrefix(extensionWithDot, ".")
// Ignore non-css files.
if extension != "css" {
continue
}
// Load the file contents.
path := filepath.Join(themesAbsFilePath, fileName)
contents, err := os.ReadFile(path)
if err != nil {
log.Warnf(nil, "error reading css theme at %s: %v", path, err)
continue
}
// Try to parse a title and description
// for this theme from the file itself.
var themeTitle string
titleMatches := themeTitleRegex.FindSubmatch(contents)
if len(titleMatches) == 2 {
themeTitle = strings.TrimSpace(string(titleMatches[1]))
} else {
// Fall back to file name
// without `.css` suffix.
themeTitle = strings.TrimSuffix(fileName, ".css")
}
var themeDescription string
descMatches := themeDescriptionRegex.FindSubmatch(contents)
if len(descMatches) == 2 {
themeDescription = strings.TrimSpace(string(descMatches[1]))
}
theme := &gtsmodel.Theme{
Title: themeTitle,
Description: themeDescription,
FileName: fileName,
}
themes.SortedByTitle = append(themes.SortedByTitle, theme)
themes.ByFileName[fileName] = theme
}
// Sort themes alphabetically
// by title (case insensitive).
slices.SortFunc(themes.SortedByTitle, func(a, b *gtsmodel.Theme) int {
return cmp.Compare(strings.ToLower(a.Title), strings.ToLower(b.Title))
})
return themes
}