customization page

This commit is contained in:
Timon Ringwald 2022-09-05 14:17:07 +02:00
parent fd40eb276c
commit f5d5d208fe
18 changed files with 1080 additions and 386 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
homepage
.env

View File

@ -5,8 +5,8 @@ import (
"strings"
)
var (
Bookmarks = []Bookmark{
func DefaultBookmarks() []Bookmark {
return []Bookmark{
{
Title: "DuckDuckGo",
Image: "https://duckduckgo.com/assets/logo_homepage.alt.v108.svg",
@ -14,130 +14,22 @@ var (
Color: "#e37151",
},
{
Title: "Cloud",
Image: "https://cloud.tordarus.net/svg/core/logo/logo?color=ffffff&v=1",
Link: "https://cloud.tordarus.net/",
Color: "#007ec2",
Title: "Google Dark",
Image: "https://www.google.com/images/branding/googlelogo/2x/googlelogo_light_color_272x92dp.png",
Link: "https://google.com/",
Color: "#202124",
},
{
Title: "ProtonMail",
Image: "https://protonmail.com/images/pm-logo-white.svg",
Link: "https://protonmail.com/",
Color: "#253163",
},
{
Title: "Anilist",
Image: "https://anilist.co/img/icons/icon.svg",
Link: "https://anilist.co/",
Color: "#2b2d42",
IconPadding: "0.5em",
},
{
Title: "Torrents",
Image: "/transmission.png",
Link: "https://torrents.tordarus.net/",
Color: "#e7402c",
},
{
Title: "Nyaa.si",
Image: "https://progsoft.net/images/nyaa-v2-icon-a591b5ae622eb38f31c00297cf9b53fd88c058ef.png",
Link: "https://nyaa.si/",
Color: "#e7402c",
ImageSize: "111%",
IconPadding: "0em",
},
{
Title: "Adblock",
Image: "/pihole.svg",
Link: "https://adblock.tordarus.net/",
Color: "#272c30",
},
{
Title: "Postbank",
Image: "https://wintouch.de/wp-content/uploads/2014/06/Postbank-Icon.png",
Link: "https://meine.postbank.de/",
Color: "#ffcc00",
},
{
Title: "DKB",
Image: "https://vdiv-sa.de/wp-content/uploads/2016/08/DKB_AG_web_RGB.jpg",
Link: "https://www.dkb.de/",
Title: "Google Light",
Image: "https://www.google.de/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png",
Link: "https://google.com/",
Color: "#ffffff",
},
{
Title: "Finanzen",
Image: "https://docs.firefly-iii.org/img/logo.png",
Link: "https://finance.tordarus.net",
Color: "#ffffff",
ImageSize: "cover",
IconPadding: "0em",
},
{
Title: "YouTube",
Image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/42/YouTube_light_icon_%282017%29.svg/1280px-YouTube_light_icon_%282017%29.svg.png",
Color: "#ff0000",
Link: "https://www.youtube.com/",
IconPadding: "1.5em",
},
{
Title: "Netflix",
Image: "https://1000logos.net/wp-content/uploads/2017/05/emblem-Netflix.jpg",
Color: "#000000",
Link: "https://www.netflix.com/",
},
{
Title: "Jisho",
Image: "https://assets.jisho.org/assets/jisho-logo-v4@2x-7330091c079b9dd59601401b052b52e103978221c8fb6f5e22406d871fcc746a.png",
Link: "https://jisho.org/",
Color: "#ffffff",
},
{
Title: "Wadoku",
Image: "https://www.wadoku.de/img/wadoku_logo.svg",
Link: "https://www.wadoku.de/",
Color: "#ffffff",
},
{
Title: "Übersetzer",
Image: "https://tools.avans.nl/tools/image/wZkdyaMblN.jpg",
Link: "https://www.deepl.com/translator",
Color: "#042b48",
ImageSize: "cover",
IconPadding: "0em",
},
{
Title: "WaniKani",
Image: "https://assets.wanikani.com/assets/logo--retro-colors-b79775af8773b5a416e4ec3fae02e62d391b7e88e57f9fe0c2e4997a3383b002.png",
Link: "https://www.wanikani.com/",
Color: "#ffffff",
},
{
Title: "Passwörter",
Image: "https://raw.githubusercontent.com/dani-garcia/vaultwarden/main/resources/vaultwarden-logo-white.svg",
Link: "https://pw.tordarus.net",
Color: "#175ddc",
},
{
Title: "Projekte",
Image: "/gitea.png", //"https://git.tordarus.net/assets/img/logo.svg",
Link: "https://git.tordarus.net",
Color: "#609926",
ImageSize: "cover",
IconPadding: "0em",
},
{
Title: "Regexr",
Image: "https://raw.githubusercontent.com/gskinner/regexr/master/dev/icons/RegExr.svg",
Link: "https://regex.tordarus.net",
Color: "#101112",
ImageSize: "contain",
IconPadding: "0em",
},
{
Title: "Wikipedia",
Image: "https://upload.wikimedia.org/wikipedia/commons/8/80/Wikipedia-logo-v2.svg",
Link: "https://www.wikipedia.org/",
Color: "#ffffff",
Link: "https://www.wikipedia.org/",
},
{
Title: "Reddit",
@ -148,67 +40,64 @@ var (
IconPadding: "0em",
},
{
Title: "Tagesschau",
Image: "https://m47ch.com/wp-content/uploads/2010/12/Tagesschau.png",
Link: "https://www.tagesschau.de/",
Color: "#003f8c",
Title: "YouTube",
Image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/42/YouTube_light_icon_%282017%29.svg/1280px-YouTube_light_icon_%282017%29.svg.png",
Link: "https://www.youtube.com/",
Color: "#ff0000",
IconPadding: "1.5em",
},
{
Title: "Golem",
Image: "https://www.golem.de/staticrl/images/golem-logo-opt2.svg",
Link: "https://www.golem.de/",
Title: "Netflix",
Image: "https://1000logos.net/wp-content/uploads/2017/05/emblem-Netflix.jpg",
Link: "https://www.netflix.com/",
Color: "#000000",
},
{
Title: "Comico",
Image: "https://play-lh.googleusercontent.com/xZ5bfg-XfyEmM1641bXduE27w44OmyWeVspg626wRYF0ejAgZAvsRKP4sECblfmnpg=w512",
Link: "https://comico.jp/",
Color: "#f40000",
Title: "Übersetzer",
Image: "https://tools.avans.nl/tools/image/wZkdyaMblN.jpg",
Link: "https://www.deepl.com/translator",
Color: "#042b48",
ImageSize: "cover",
IconPadding: "0em",
},
{
Title: "Manganelo",
Image: "https://manganato.com/themes/hm/images/logo.png",
Link: "https://manganato.com/",
Color: "#ffffff",
ImageSize: "contain",
IconPadding: "0em",
Title: "Cloud",
Image: "https://cloud.tordarus.net/svg/core/logo/logo?color=ffffff\u0026v=1",
Link: "https://cloud.tordarus.net/",
Color: "#007ec2",
},
{
Title: "Manga List",
Image: "https://www.firstcomicsnews.com/wp-content/uploads/2020/09/manga-logo.png",
Link: "https://manga.tordarus.net/",
Color: "#be151b",
Title: "Passwörter",
Image: "https://raw.githubusercontent.com/dani-garcia/vaultwarden/main/resources/vaultwarden-logo-white.svg",
Link: "https://pw.tordarus.net",
Color: "#175ddc",
},
}
)
type Bookmark struct {
Title string
Image string
ImageSize string
IconPadding string
Color string
Link string
HideBorder bool
Order int
}
func ParseBookmarks() {
// set default values
for i := range Bookmarks {
if Bookmarks[i].Color == "" {
Bookmarks[i].Color = DefaultSettings.Background
}
if Bookmarks[i].ImageSize == "" {
Bookmarks[i].ImageSize = "contain"
}
}
type Bookmark struct {
Title string `json:"title"`
Image string `json:"image"`
ImageSize string `json:"image_size"`
IconPadding string `json:"icon_padding"`
Color string `json:"color"`
Link string `json:"link"`
HideBorder bool `json:"hide_border"`
Order int `json:"order"`
}
sort.SliceStable(Bookmarks, func(i, j int) bool {
return Bookmarks[i].Order < Bookmarks[j].Order
})
func (b *Bookmark) GetColor() string {
if b.Color == "" {
b.Color = "transparent"
}
return b.Color
}
func (b *Bookmark) GetImageSize() string {
if b.ImageSize == "" {
b.ImageSize = "contain"
}
return b.ImageSize
}
func (b *Bookmark) SmallLink() string {
@ -218,3 +107,13 @@ func (b *Bookmark) SmallLink() string {
lnk = strings.TrimSuffix(lnk, "/")
return lnk
}
func Reorder(bookmarks []Bookmark) {
sort.SliceStable(bookmarks, func(i, j int) bool {
return bookmarks[i].Order < bookmarks[j].Order
})
for index := range bookmarks {
bookmarks[index].Order = index * 10
}
}

262
bookmarks.json Normal file
View File

@ -0,0 +1,262 @@
[
{
"title": "DuckDuckGo",
"image": "https://duckduckgo.com/assets/logo_homepage.alt.v108.svg",
"image_size": "",
"icon_padding": "",
"color": "#e37151",
"link": "https://duckduckgo.com/",
"hide_border": false,
"order": 0
},
{
"title": "Cloud",
"image": "https://cloud.tordarus.net/svg/core/logo/logo?color=ffffff\u0026v=1",
"image_size": "",
"icon_padding": "",
"color": "#007ec2",
"link": "https://cloud.tordarus.net/",
"hide_border": false,
"order": 0
},
{
"title": "ProtonMail",
"image": "https://protonmail.com/images/pm-logo-white.svg",
"image_size": "",
"icon_padding": "",
"color": "#253163",
"link": "https://protonmail.com/",
"hide_border": false,
"order": 0
},
{
"title": "Anilist",
"image": "https://anilist.co/img/icons/icon.svg",
"image_size": "",
"icon_padding": "0.5em",
"color": "#2b2d42",
"link": "https://anilist.co/",
"hide_border": false,
"order": 0
},
{
"title": "Torrents",
"image": "/transmission.png",
"image_size": "",
"icon_padding": "",
"color": "#e7402c",
"link": "https://torrents.tordarus.net/",
"hide_border": false,
"order": 0
},
{
"title": "Nyaa.si",
"image": "https://progsoft.net/images/nyaa-v2-icon-a591b5ae622eb38f31c00297cf9b53fd88c058ef.png",
"image_size": "111%",
"icon_padding": "0em",
"color": "#e7402c",
"link": "https://nyaa.si/",
"hide_border": false,
"order": 0
},
{
"title": "Adblock",
"image": "/pihole.svg",
"image_size": "",
"icon_padding": "",
"color": "#272c30",
"link": "https://adblock.tordarus.net/",
"hide_border": false,
"order": 0
},
{
"title": "Postbank",
"image": "https://wintouch.de/wp-content/uploads/2014/06/Postbank-Icon.png",
"image_size": "",
"icon_padding": "",
"color": "#ffcc00",
"link": "https://meine.postbank.de/",
"hide_border": false,
"order": 0
},
{
"title": "DKB",
"image": "https://vdiv-sa.de/wp-content/uploads/2016/08/DKB_AG_web_RGB.jpg",
"image_size": "",
"icon_padding": "",
"color": "#ffffff",
"link": "https://www.dkb.de/",
"hide_border": false,
"order": 0
},
{
"title": "Finanzen",
"image": "https://docs.firefly-iii.org/img/logo.png",
"image_size": "cover",
"icon_padding": "0em",
"color": "#ffffff",
"link": "https://finance.tordarus.net",
"hide_border": false,
"order": 0
},
{
"title": "YouTube",
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/42/YouTube_light_icon_%282017%29.svg/1280px-YouTube_light_icon_%282017%29.svg.png",
"image_size": "",
"icon_padding": "1.5em",
"color": "#ff0000",
"link": "https://www.youtube.com/",
"hide_border": false,
"order": 0
},
{
"title": "Netflix",
"image": "https://1000logos.net/wp-content/uploads/2017/05/emblem-Netflix.jpg",
"image_size": "",
"icon_padding": "",
"color": "#000000",
"link": "https://www.netflix.com/",
"hide_border": false,
"order": 0
},
{
"title": "Jisho",
"image": "https://assets.jisho.org/assets/jisho-logo-v4@2x-7330091c079b9dd59601401b052b52e103978221c8fb6f5e22406d871fcc746a.png",
"image_size": "",
"icon_padding": "",
"color": "#ffffff",
"link": "https://jisho.org/",
"hide_border": false,
"order": 0
},
{
"title": "Wadoku",
"image": "https://www.wadoku.de/img/wadoku_logo.svg",
"image_size": "",
"icon_padding": "",
"color": "#ffffff",
"link": "https://www.wadoku.de/",
"hide_border": false,
"order": 0
},
{
"title": "Übersetzer",
"image": "https://tools.avans.nl/tools/image/wZkdyaMblN.jpg",
"image_size": "cover",
"icon_padding": "0em",
"color": "#042b48",
"link": "https://www.deepl.com/translator",
"hide_border": false,
"order": 0
},
{
"title": "WaniKani",
"image": "https://assets.wanikani.com/assets/logo--retro-colors-b79775af8773b5a416e4ec3fae02e62d391b7e88e57f9fe0c2e4997a3383b002.png",
"image_size": "",
"icon_padding": "",
"color": "#ffffff",
"link": "https://www.wanikani.com/",
"hide_border": false,
"order": 0
},
{
"title": "Passwörter",
"image": "https://raw.githubusercontent.com/dani-garcia/vaultwarden/main/resources/vaultwarden-logo-white.svg",
"image_size": "",
"icon_padding": "",
"color": "#175ddc",
"link": "https://pw.tordarus.net",
"hide_border": false,
"order": 0
},
{
"title": "Projekte",
"image": "/gitea.png",
"image_size": "cover",
"icon_padding": "0em",
"color": "#609926",
"link": "https://git.tordarus.net",
"hide_border": false,
"order": 0
},
{
"title": "Regexr",
"image": "https://raw.githubusercontent.com/gskinner/regexr/master/dev/icons/RegExr.svg",
"image_size": "contain",
"icon_padding": "0em",
"color": "#101112",
"link": "https://regex.tordarus.net",
"hide_border": false,
"order": 0
},
{
"title": "Wikipedia",
"image": "https://upload.wikimedia.org/wikipedia/commons/8/80/Wikipedia-logo-v2.svg",
"image_size": "",
"icon_padding": "",
"color": "#ffffff",
"link": "https://www.wikipedia.org/",
"hide_border": false,
"order": 0
},
{
"title": "Reddit",
"image": "https://www.redditinc.com/assets/images/site/reddit-logo.png",
"image_size": "cover",
"icon_padding": "0em",
"color": "#ff4300",
"link": "https://www.reddit.com/",
"hide_border": false,
"order": 0
},
{
"title": "Tagesschau",
"image": "https://m47ch.com/wp-content/uploads/2010/12/Tagesschau.png",
"image_size": "",
"icon_padding": "",
"color": "#003f8c",
"link": "https://www.tagesschau.de/",
"hide_border": false,
"order": 0
},
{
"title": "Golem",
"image": "https://www.golem.de/staticrl/images/golem-logo-opt2.svg",
"image_size": "",
"icon_padding": "",
"color": "#000000",
"link": "https://www.golem.de/",
"hide_border": false,
"order": 0
},
{
"title": "Comico",
"image": "https://play-lh.googleusercontent.com/xZ5bfg-XfyEmM1641bXduE27w44OmyWeVspg626wRYF0ejAgZAvsRKP4sECblfmnpg=w512",
"image_size": "cover",
"icon_padding": "0em",
"color": "#f40000",
"link": "https://comico.jp/",
"hide_border": false,
"order": 0
},
{
"title": "Manganelo",
"image": "https://manganato.com/themes/hm/images/logo.png",
"image_size": "contain",
"icon_padding": "0em",
"color": "#ffffff",
"link": "https://manganato.com/",
"hide_border": false,
"order": 0
},
{
"title": "Manga List",
"image": "https://www.firstcomicsnews.com/wp-content/uploads/2020/09/manga-logo.png",
"image_size": "",
"icon_padding": "",
"color": "#be151b",
"link": "https://manga.tordarus.net/",
"hide_border": false,
"order": 0
}
]

8
go.mod
View File

@ -2,4 +2,10 @@ module git.tordarus.net/Tordarus/homepage
go 1.18
require golang.org/x/text v0.3.7 // indirect
require (
git.milar.in/milarin/slices v0.0.4
github.com/gorilla/sessions v1.2.1
golang.org/x/text v0.3.7
)
require github.com/gorilla/securecookie v1.1.1 // indirect

6
go.sum
View File

@ -1,2 +1,8 @@
git.milar.in/milarin/slices v0.0.4 h1:z92jgsKcnLPLfgXkTVCzH2XXesfXzhe0Osx+PkfCHVI=
git.milar.in/milarin/slices v0.0.4/go.mod h1:NOr53AOeur/qscu/FBj3lsFR262PNYBccLYSTCAXRk4=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=

96
main.go
View File

@ -2,6 +2,9 @@ package main
import (
"embed"
"encoding/gob"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
@ -9,9 +12,13 @@ import (
"io"
"net/http"
"net/url"
"os"
"strings"
_ "embed"
"git.milar.in/milarin/slices"
"github.com/gorilla/sessions"
)
var (
@ -27,12 +34,15 @@ var (
StaticFS embed.FS
Templates *template.Template
SessionStore = sessions.NewCookieStore(Must(hex.DecodeString(os.Getenv("SESSION_KEY"))))
)
func main() {
flag.Parse()
gob.Register([]Bookmark{})
gob.Register(&Settings{})
ParseBookmarks()
flag.Parse()
if tmpl, err := template.New("homepage").ParseFS(TemplateFS, "templates/*"); err == nil {
Templates = tmpl
@ -41,8 +51,11 @@ func main() {
}
http.HandleFunc("/", handler)
http.HandleFunc("/customize", customize)
http.HandleFunc("/save-changes", saveChanges)
http.HandleFunc("/search", search)
http.HandleFunc("/style.css", style)
http.HandleFunc("/style.css", ProvideFile("static/style.css", "text/css"))
http.HandleFunc("/customize.js", ProvideFile("static/customize.js", "application/javascript"))
http.HandleFunc("/icons.ttf", ProvideFile("static/icons.ttf", "font/ttf"))
http.HandleFunc("/transmission.png", ProvideFile("static/transmission.png", "image/png"))
http.HandleFunc("/pihole.svg", ProvideFile("static/pihole.svg", "image/svg+xml"))
@ -53,10 +66,12 @@ func main() {
}
func handler(w http.ResponseWriter, r *http.Request) {
session, _ := SessionStore.Get(r, "settings")
data := &TmplData{
text: GetText(r),
Bookmarks: Bookmarks,
Settings: DefaultSettings,
Bookmarks: GetValueDefault(session, "bookmarks", DefaultBookmarks()),
Settings: GetValueDefault(session, "settings", DefaultSettings()),
}
if err := Templates.ExecuteTemplate(w, "index.html", data); err != nil {
@ -64,22 +79,6 @@ func handler(w http.ResponseWriter, r *http.Request) {
}
}
func style(w http.ResponseWriter, r *http.Request) {
file, err := TemplateFS.Open("templates/style.css")
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
defer file.Close()
w.Header().Add("Content-Type", "text/css")
if err := Templates.ExecuteTemplate(w, "style.css", DefaultSettings); err != nil {
panic(err)
}
}
func ProvideFile(path, contentType string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
file, err := StaticFS.Open(path)
@ -95,6 +94,9 @@ func ProvideFile(path, contentType string) http.HandlerFunc {
}
func search(w http.ResponseWriter, r *http.Request) {
session, _ := SessionStore.Get(r, "settings")
settings := GetValueDefault(session, "settings", DefaultSettings())
if err := r.ParseForm(); err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
@ -109,12 +111,62 @@ func search(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusMovedPermanently)
} else {
// search string
w.Header().Add("Location", fmt.Sprintf("https://duckduckgo.com/?q=%s", query))
w.Header().Add("Location", fmt.Sprintf(settings.Search, query))
w.WriteHeader(http.StatusMovedPermanently)
}
}
func customize(w http.ResponseWriter, r *http.Request) {
session, _ := SessionStore.Get(r, "settings")
text := GetText(r)
bookmarks := slices.Map(GetValueDefault(session, "bookmarks", DefaultBookmarks()), func(b Bookmark) BookmarkData {
return BookmarkData{
Bookmark: &b,
text: text,
}
})
data := &CustomizeData{
text: text,
Bookmarks: bookmarks,
Settings: GetValueDefault(session, "settings", DefaultSettings()),
}
if err := Templates.ExecuteTemplate(w, "customize.html", data); err != nil {
panic(err)
}
}
func saveChanges(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
session, _ := SessionStore.Get(r, "settings")
sessionData := &SessionData{}
err := json.NewDecoder(r.Body).Decode(&sessionData)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Println(err)
return
}
Reorder(sessionData.Bookmarks)
session.Values["bookmarks"] = sessionData.Bookmarks
session.Values["settings"] = sessionData.Settings
if err := session.Save(r, w); err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func ParseURI(uri string) (*url.URL, error) {
if !strings.HasPrefix(uri, "http://") && !strings.HasPrefix(uri, "https://") {
uri = "https://" + uri

6
session_data.go Normal file
View File

@ -0,0 +1,6 @@
package main
type SessionData struct {
Bookmarks []Bookmark `json:"bookmarks"`
Settings *Settings `json:"settings"`
}

View File

@ -1,19 +1,17 @@
package main
type Settings struct {
Background string
IconMargin string
IconPadding string
IconWidth string
IconHeight string
BorderRadius string
Background string `json:"background_color"`
Foreground string `json:"foreground_color"`
Search string `json:"search_query"`
BorderRadius string `json:"border_radius"`
}
var DefaultSettings = &Settings{
Background: "#2b2a33",
IconMargin: "0.5em",
IconPadding: "1em",
IconWidth: "10em",
IconHeight: "10em",
BorderRadius: "0.15em",
func DefaultSettings() *Settings {
return &Settings{
Background: "#2b2a33",
Foreground: "#ffffff",
Search: "https://duckduckgo.com/?q=%s",
BorderRadius: "1.5%",
}
}

135
static/customize.js Normal file
View File

@ -0,0 +1,135 @@
window.addEventListener("load", main);
function main() {
const bgColor = document.querySelector("#bg-color");
const fgColor = document.querySelector("#fg-color");
const borderRadius = document.querySelector("#border-radius");
bgColor.addEventListener("change", () => document.body.style.backgroundColor = bgColor.value);
fgColor.addEventListener("change", () => document.body.style.color = fgColor.value);
borderRadius.addEventListener("change", () => document.querySelector(":root").style.setProperty("--bookmark-border-radius", borderRadius.value));
const save = document.querySelector("#button");
save.addEventListener("click", saveChanges);
const addBookmark = document.querySelector("#add-bookmark");
const firstBookmark = document.querySelector(".customizer");
addBookmark.style.width = `${firstBookmark.offsetWidth}px`;
addBookmark.style.height = `${firstBookmark.offsetHeight}px`;
addBookmark.style.lineHeight = `${firstBookmark.offsetHeight}px`;
addBookmark.addEventListener("click", () => {
const bookmark = firstBookmark.cloneNode(true);
initBookmark(bookmark);
clearBookmarkData(bookmark);
document.querySelector("#customize-container").insertBefore(bookmark, addBookmark);
});
document.querySelectorAll(".customizer").forEach(initBookmark);
}
function saveChanges() {
const bookmarks = [];
document.querySelectorAll(".customizer").forEach(element => bookmarks.push(buildDataForBoomark(element)));
const settings = {
background_color: document.querySelector("#bg-color").value,
foreground_color: document.querySelector("#fg-color").value,
search_query: document.querySelector("#search-string").value,
border_radius: document.querySelector("#border-radius").value
};
const data = { bookmarks, settings }
fetch("/save-changes", {
method: "POST",
body: JSON.stringify(data)
}).then(() => {
window.location = "/";
})
}
function buildDataForBoomark(element) {
const form = element.querySelector("form");
const title = form.querySelector("#bookmark-name");
const link = form.querySelector("#bookmark-link");
const image = form.querySelector("#bookmark-image");
const imageSize = form.querySelector("#bookmark-imagesize");
const iconPadding = form.querySelector("#bookmark-iconpadding");
const color = form.querySelector("#bookmark-color");
const hideBorder = form.querySelector("#bookmark-border");
const order = form.querySelector("#bookmark-order");
return {
title: title.value,
image: image.value,
image_size: imageSize.value,
icon_padding: iconPadding.value,
color: color.value,
link: link.value,
hide_border: hideBorder.checked,
order: parseInt(order.value)
};
}
function clearBookmarkData(bookmark) {
const a = bookmark.querySelector(".bookmark");
const form = bookmark.querySelector("form");
const bg = a.querySelector(".background");
const title = form.querySelector("#bookmark-name");
const link = form.querySelector("#bookmark-link");
const image = form.querySelector("#bookmark-image");
const imageSize = form.querySelector("#bookmark-imagesize");
const iconPadding = form.querySelector("#bookmark-iconpadding");
const color = form.querySelector("#bookmark-color");
const hideBorder = form.querySelector("#bookmark-border");
const order = form.querySelector("#bookmark-order");
let orderValue = 0;
document.querySelectorAll(".customizer").forEach(element => {
orderValue = Math.max(orderValue, element.querySelector("#bookmark-order").value);
});
title.value = "";
link.value = "";
image.value = "";
imageSize.value = "contain";
iconPadding.value = "";
color.value = "#ffffff";
hideBorder.value = false;
order.value = orderValue + 10;
bg.style.backgroundImage = "";
a.style.backgroundColor = "#ffffff";
}
function initBookmark(bookmark) {
const a = bookmark.querySelector(".bookmark");
const del = bookmark.querySelector(".delete");
const form = bookmark.querySelector("form");
const bg = a.querySelector(".background");
const title = form.querySelector("#bookmark-name");
const link = form.querySelector("#bookmark-link");
const image = form.querySelector("#bookmark-image");
const imageSize = form.querySelector("#bookmark-imagesize");
const iconPadding = form.querySelector("#bookmark-iconpadding");
const color = form.querySelector("#bookmark-color");
const hideBorder = form.querySelector("#bookmark-border");
del.addEventListener("click", () => {
const question = title.value == "" ? "Do you want to delete this bookmark?" : `Do you want to delete '${title.value}'?`
if (confirm(question)) {
bookmark.parentElement.removeChild(bookmark);
}
});
link.addEventListener("change", () => a.setAttribute("href", link.value));
image.addEventListener("change", () => bg.style.backgroundImage = `url(${image.value})`);
imageSize.addEventListener("change", () => bg.style.backgroundSize = imageSize.value);
iconPadding.addEventListener("change", () => a.style.padding = iconPadding.value);
color.addEventListener("change", () => a.style.backgroundColor = color.value);
hideBorder.addEventListener("change", () => a.style.boxShadow = hideBorder.value ? "" : "0px 0px 3px black");
}

315
static/style.css Normal file
View File

@ -0,0 +1,315 @@
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url("/icons.ttf");
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url("/icons.ttf") format('truetype');
}
.material-icons {
transform: translateY(3px);
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px; /* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: 'liga';
}
:root {
--bookmark-width: 10em;
--bookmark-height: 10em;
--bookmark-margin: 0.5em;
--bookmark-padding: 1em;
}
* {
margin: 0;
padding: 0;
}
html, body {
width: 100%;
min-height: 100vh;
font-size: 100%;
}
body {
background-color: #2b2a33;
}
main {
width: 100%;
min-height: 100vh;
}
#main-container {
display: flex;
flex-direction: column;
justify-content: center;
margin-bottom: 0.5em;
}
#bookmark-container {
width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-content: center;
}
#search {
margin: 3em auto;
max-width: calc(100% - 2em);
}
#search form {
--input-font-size: 1.2em;
--width: 30rem;
--height: 2.5rem;
position: relative;
display: block;
width: var(--width);
height: var(--height);
max-width: 100%;
background-color: rgba(0, 0, 0, 0.5);
border: 1px solid black;
border-radius: 0.25em;
}
#search form input[type="search"] {
position: absolute;
top: 0;
left: 0;
display: inline-block;
width: calc(100% - var(--height));
height: var(--height);
border: 0;
padding: 0 0.5em;
box-sizing: border-box;
outline: 0;
color: inherit;
background-color: transparent;
transition: all 0.2s ease-in-out;
font-size: var(--input-font-size);
}
#search form label[for="submit"] {
position: absolute;
top: 0;
right: 0;
display: inline-block;
width: var(--height);
height: var(--height);
margin-left: 0;
margin-right: 0;
box-sizing: border-box;
transition: all 0.2s ease-in-out;
font-size: var(--input-font-size);
cursor: pointer;
}
#search form label[for="submit"] span {
transform: translate(0.3em, 0.3em);
}
#search form input[type="submit"] {
display: none;
}
.bookmark {
display: block;
box-sizing: border-box;
transition: all 0.2s ease-in-out;
width: var(--bookmark-width);
height: var(--bookmark-height);
margin: var(--bookmark-margin);
padding: var(--bookmark-padding);
border-radius: var(--bookmark-border-radius);
}
.bookmark:hover {
transform: scale(1.1, 1.1);
transition: all 0.2s ease-in-out;
}
.background {
width: 100%;
height: 100%;
background-position: center;
background-size: contain;
background-repeat: no-repeat;
border-radius: var(--bookmark-border-radius);
}
@media screen and (max-width: 30em) {
.bookmark {
width: calc(var(--bookmark-width) * 0.5);
height: calc(var(--bookmark-height) * 0.5);
padding: calc(var(--bookmark-padding) * 0.5);
}
}
#button {
display: block;
position: absolute;
top: 0.25em;
right: 0.5em;
color: white;
}
#customize-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
justify-content: center;
}
.customizer {
margin: 1em;
position: relative;
display: flex;
flex-direction: row;
justify-content: center;
background-color: rgba(255, 255, 255, 0.15);
border-radius: 0.25em;
}
.customizer > form {
margin: 0.5em;
display: grid;
grid-template-columns: auto auto;
grid-auto-rows: max-content;
gap: 0.25em;
}
form label {
margin-right: 0.25em;
}
.customizer > form input {
background-color: rgba(0, 0, 0, 0.25);
border: 1px solid black;
color: inherit;
padding: 0.25em;
border-radius: 0.2em;
}
.customizer > form input:not([type="checkbox"]):not([type="file"]):not([type="color"]):focus {
outline-style: solid;
outline-color: #aaa;
}
#add-bookmark {
margin: 1em;
color: white;
background-color: darkgreen;
text-align: center;
border-radius: 0.25em;
cursor: pointer;
transition: all 0.25s ease-in-out;
width: 618px;
height: 259px;
line-height: 259px;
}
#add-bookmark:hover,
#add-bookmark:focus {
background-color: green;
transition: all 0.25s ease-in-out;
}
#add-bookmark .material-icons {
font-size: 8em;
line-height: inherit;
}
.delete {
background-color: #cc0000;
color: white;
position: absolute;
bottom: 0.5em;
left: 0.5em;
width: 2em;
height: 2em;
text-align: center;
border-radius: 0.25em;
box-shadow: 0px 0px 3px #333;
cursor: pointer;
transition: all 0.25s ease-in-out;
}
.delete:hover,
.delete:focus {
transform: scale(1.25);
transition: all 0.25s ease-in-out;
}
.fixed {
position: fixed !important;
}
#top-panel {
background-color: rgba(255, 255, 255, 0.15);
padding: 1em;
border-bottom: 1px solid black;
}
#top-panel h1 {
text-align: center;
margin-bottom: 0.5em;
}
#top-panel > form {
max-width: 30em;
display: grid;
grid-template-columns: auto auto;
grid-auto-rows: max-content;
gap: 0.25em;
margin: 0 auto;
}
#top-panel > form input {
background-color: rgba(0, 0, 0, 0.25);
border: 1px solid black;
color: inherit;
padding: 0.25em;
border-radius: 0.2em;
}
#top-panel > form input:not([type="checkbox"]):not([type="file"]):not([type="color"]):focus {
outline-style: solid;
outline-color: #aaa;
}
#top-panel > form > * {
display: block;
}

View File

@ -1,3 +1,3 @@
<a href="{{ .Link }}" class="bookmark" style='background-color: {{ .Color }}; {{ if not .HideBorder }} box-shadow: 0px 0px 3px black; {{ end }}; {{ if not (eq .IconPadding "") }} padding: {{ .IconPadding }}; {{ end }}'>
<div class="background" style='background-image: url("{{ .Image }}"); background-size: {{ .ImageSize }}'></div>
</a>
<a href="{{ .Link }}" class="bookmark" style='background-color: {{ .GetColor }}; {{ if not .HideBorder }} box-shadow: 0px 0px 3px black; {{ end }}; {{ if not (eq .IconPadding "") }} padding: {{ .IconPadding }}; {{ end }}'>
<div class="background" style='background-image: url("{{ .Image }}"); background-size: {{ .GetImageSize }}'></div>
</a>

View File

@ -0,0 +1,33 @@
<div class="customizer">
<a href="{{ .Link }}" class="bookmark" style='background-color: {{ .GetColor }}; {{ if not .HideBorder }} box-shadow: 0px 0px 3px black; {{ end }}; {{ if not (eq .IconPadding "") }} padding: {{ .IconPadding }}; {{ end }}'>
<div class="background" style='background-image: url("{{ .Image }}"); background-size: {{ .GetImageSize }}'></div>
</a>
<a class="delete"><span class="material-icons">delete</span></a>
<form>
<label for="bookmark-name">{{ .Translate "Name" }}</label>
<input type="text" id="bookmark-name" name="bookmark-name" value="{{ .Title }}" />
<label for="bookmark-link">{{ .Translate "Link" }}</label>
<input type="url" id="bookmark-link" name="bookmark-link" value="{{ .Link }}" />
<label for="bookmark-image">{{ .Translate "Image" }}</label>
<input type="url" id="bookmark-image" name="bookmark-image" value="{{ .Image }}" />
<label for="bookmark-imagesize">{{ .Translate "Image size" }}</label>
<input list="image-sizes" id="bookmark-imagesize" name="bookmark-imagesize" value="{{ .ImageSize }}" />
<label for="bookmark-iconpadding">{{ .Translate "Image padding" }}</label>
<input list="image-paddings" id="bookmark-iconpadding" name="bookmark-iconpadding" value="{{ .IconPadding }}" />
<label for="bookmark-color">{{ .Translate "Color" }}</label>
<input type="color" id="bookmark-color" name="bookmark-color" value="{{ .Color }}" />
<label for="bookmark-border">{{ .Translate "Hide border" }}</label>
<input type="checkbox" id="bookmark-border" name="bookmark-border" {{ if .HideBorder }} checked {{ end }} />
<label for="bookmark-order">{{ .Translate "Order priority" }}</label>
<input type="number" id="bookmark-order" name="bookmark-order" value="{{ .Order }}" />
</form>
</div>

70
templates/customize.html Normal file
View File

@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ .Translate "Customize Homepage" }}</title>
<link rel="stylesheet" type="text/css" href="/style.css" />
<script src="/customize.js"></script>
<style>
:root {
--bookmark-border-radius: {{ .Settings.BorderRadius }};
}
body {
background-color: {{ .Settings.Background }};
color: {{ .Settings.Foreground }};
}
</style>
</head>
<body>
<datalist id="image-sizes">
<option value="cover">cover</option>
<option value="contain">contain</option>
<option value="100%">100%</option>
<option value="0px">0px</option>
</datalist>
<datalist id="image-paddings">
<option value="10%">10%</option>
<option value="0px">0px</option>
<option value="10px">10px</option>
<option value="1em">1em</option>
</datalist>
<datalist id="search-strings">
<option value="https://www.google.de/search?q=%s">Google</option>
<option value="https://duckduckgo.com/?q=%s">DuckDuckGo</option>
<option value="https://www.startpage.com/do/search?query=%s">Startpage</option>
<option value="https://www.bing.com/search?q=%s">Bing</option>
<option value="https://de.search.yahoo.com/search?p=%s">Yahoo</option>
</datalist>
<main>
<a id="button" class="fixed" href="#">
<span class="material-icons">save</span>
</a>
<div id="top-panel">
<h1>{{ .Translate "Settings" }}</h1>
<form>
<label for="search-string">{{ .Translate "Search engine" }}</label>
<input list="search-strings" id="search-string" name="search-string" value="{{ .Settings.Search }}" />
<label for="bg-color">{{ .Translate "Background color" }}</label>
<input type="color" id="bg-color" name="bg-color" value="{{ .Settings.Background }}" />
<label for="fg-color">{{ .Translate "Foreground color" }}</label>
<input type="color" id="fg-color" name="fg-color" value="{{ .Settings.Foreground }}" />
<label for="border-radius">{{ .Translate "Border radius" }}</label>
<input type="text" id="border-radius" name="border-radius" value="{{ .Settings.BorderRadius }}" />
</form>
</div>
<section id="customize-container">
{{ range .Bookmarks }}
{{ template "customize-bookmark.html" . }}
{{ end }}
<a id="add-bookmark"><span class="material-icons">add</span></a>
</section>
</main>
</body>
</html>

View File

@ -6,11 +6,24 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ .Translate "Homepage" }}</title>
<link rel="stylesheet" type="text/css" href="style.css" />
<link rel="stylesheet" type="text/css" href="/style.css" />
<style>
:root {
--bookmark-border-radius: {{ .Settings.BorderRadius }};
}
body {
background-color: {{ .Settings.Background }};
color: {{ .Settings.Foreground }};
}
</style>
</head>
<body>
<main>
<main id="main-container">
<a id="button" href="/customize">
<span class="material-icons">settings</span>
</a>
<section id="search">
<form method="post" action="/search">
<datalist id="bookmarks">
@ -31,4 +44,4 @@
</main>
</body>
</html>
</html>

View File

@ -1,175 +0,0 @@
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url("/icons.ttf");
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url("/icons.ttf") format('truetype');
}
.material-icons {
transform: translateY(3px);
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px; /* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: 'liga';
}
* {
margin: 0;
padding: 0;
}
html, body {
width: 100%;
min-height: 100vh;
font-size: 100%;
}
body {
background-color: {{ .Background }};
}
main {
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
margin-bottom: 0.5em;
}
#bookmark-container {
width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-content: center;
}
#search {
margin: 3em auto;
max-width: calc(100% - 2em);
}
#search form {
--input-font-size: 1.2em;
--width: 30rem;
--height: 2.5rem;
position: relative;
display: block;
width: var(--width);
height: var(--height);
max-width: 100%;
}
#search form input[type="search"] {
position: absolute;
top: 0;
left: 0;
display: inline-block;
width: calc(100% - var(--height));
height: var(--height);
border: 1px solid black;
padding: 0 0.5em;
box-sizing: border-box;
outline: 0;
background-color: black;
color: white;
transition: all 0.2s ease-in-out;
border-radius: 0.25em 0 0 0.25em;
font-size: var(--input-font-size);
}
#search form input[type="search"]:focus {
box-shadow: 0px 0px 3px gray;
transition: all 0.2s ease-in-out;
}
#search form input[type="search"]:focus + label[for="submit"] {
background-color: #111;
box-shadow: 0px 0px 3px gray;
transition: all 0.2s ease-in-out;
}
#search form label[for="submit"] {
position: absolute;
top: 0;
right: 0;
display: inline-block;
width: var(--height);
height: var(--height);
margin-left: 0;
border: 1px solid black;
box-sizing: border-box;
background-color: black;
color: white;
transition: all 0.2s ease-in-out;
border-radius: 0 0.25em 0.25em 0;
font-size: var(--input-font-size);
cursor: pointer;
}
#search form label[for="submit"] span {
transform: translate(0.3em, 0.3em);
}
#search form input[type="submit"] {
display: none;
}
.bookmark {
display: block;
box-sizing: border-box;
transition: all 0.2s ease-in-out;
width: {{ .IconWidth }};
height: {{ .IconHeight }};
margin: {{ .IconMargin }};
padding: {{ .IconPadding }};
border-radius: {{ .BorderRadius }};
}
.bookmark:hover {
transform: scale(1.1, 1.1);
transition: all 0.2s ease-in-out;
}
.background {
width: 100%;
height: 100%;
background-position: center;
background-size: contain;
background-repeat: no-repeat;
border-radius: {{ .BorderRadius }};
}
@media screen and (max-width: 30em) {
.bookmark {
width: calc({{ .IconWidth }} * 0.5);
height: calc({{ .IconHeight }} * 0.5);
padding: calc({{ .IconPadding }} * 0.5);
}
}

27
text.go
View File

@ -22,22 +22,37 @@ var (
Languages = map[language.Tag]Text{
language.English: {
Strings: map[string]string{
"Homepage": "Homepage",
"Search": "Search",
"Homepage": "Homepage",
"Search": "Search",
"Search engine": "Search engine",
"Settings": "Settings",
"Background color": "Background color",
"Foreground color": "Foreground color",
"Border radius": "Border radius",
},
},
language.German: {
Strings: map[string]string{
"Homepage": "Homepage",
"Search": "Suche",
"Homepage": "Homepage",
"Search": "Suche",
"Search engine": "Suchmaschine",
"Settings": "Einstellungen",
"Background color": "Hintergrundfarbe",
"Foreground color": "Textfarbe",
"Border radius": "Randradius",
},
},
language.Japanese: {
Strings: map[string]string{
"Homepage": "ホームページ",
"Search": "検索",
"Homepage": "ホームページ",
"Search": "検索",
"Search engine": "検索エンジン",
"Settings": "設定",
"Background color": "背景色",
"Foreground color": "描画色",
"Border radius": "境界半径",
},
},
}

View File

@ -13,3 +13,26 @@ func (d *TmplData) Translate(str string) string {
func (d *TmplData) Language() string {
return d.text.Language
}
type CustomizeData struct {
text *Text
Bookmarks []BookmarkData
Settings *Settings
}
func (d *CustomizeData) Translate(str string) string {
return d.text.Translate(str)
}
func (d *CustomizeData) Language() string {
return d.text.Language
}
type BookmarkData struct {
*Bookmark
text *Text
}
func (bd *BookmarkData) Translate(str string) string {
return bd.text.Translate(str)
}

35
utils.go Normal file
View File

@ -0,0 +1,35 @@
package main
import (
"github.com/gorilla/sessions"
)
func Must[T any](value T, err error) T {
if err != nil {
panic(err)
}
return value
}
func GetValue[K any, T any](session *sessions.Session, key K) (T, bool) {
value, ok := session.Values[key]
if !ok && value == nil {
return *new(T), false
}
castedValue, ok := value.(T)
if !ok {
return *new(T), false
}
return castedValue, true
}
func GetValueDefault[K any, T any](session *sessions.Session, key K, defaultValue T) T {
v, ok := GetValue[K, T](session, key)
if !ok {
return defaultValue
}
return v
}