Compare commits
11 commits
0596edc4b2
...
3da0249555
Author | SHA1 | Date | |
---|---|---|---|
3da0249555 | |||
7a354cddf5 | |||
053faeb493 | |||
375ae84ae3 | |||
581273370d | |||
9274796729 | |||
f7b3faf8e8 | |||
69e2e22e47 | |||
fe84a59326 | |||
da1cd0204e | |||
30c4252e40 |
|
@ -77,5 +77,9 @@ func handleConfigOverrides(config *model.Config) error {
|
||||||
if env, has := os.LookupEnv("ARIMELODY_DISCORD_CLIENT_ID"); has { config.Discord.ClientID = env }
|
if env, has := os.LookupEnv("ARIMELODY_DISCORD_CLIENT_ID"); has { config.Discord.ClientID = env }
|
||||||
if env, has := os.LookupEnv("ARIMELODY_DISCORD_SECRET"); has { config.Discord.Secret = env }
|
if env, has := os.LookupEnv("ARIMELODY_DISCORD_SECRET"); has { config.Discord.Secret = env }
|
||||||
|
|
||||||
|
if env, has := os.LookupEnv("ARIMELODY_TWITCH_BROADCASTER"); has { config.Twitch.Broadcaster = env }
|
||||||
|
if env, has := os.LookupEnv("ARIMELODY_TWITCH_CLIENT_ID"); has { config.Twitch.ClientID = env }
|
||||||
|
if env, has := os.LookupEnv("ARIMELODY_TWITCH_SECRET"); has { config.Twitch.Secret = env }
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"image/png"
|
|
||||||
|
|
||||||
"github.com/skip2/go-qrcode"
|
"github.com/skip2/go-qrcode"
|
||||||
)
|
)
|
||||||
|
@ -33,69 +29,6 @@ const (
|
||||||
HIGH
|
HIGH
|
||||||
)
|
)
|
||||||
|
|
||||||
func noDepsGenerateQRCode() (string, error) {
|
|
||||||
version := 1
|
|
||||||
|
|
||||||
size := 0
|
|
||||||
size = 21 + version * 4
|
|
||||||
if version > 10 {
|
|
||||||
return "", errors.New(fmt.Sprintf("QR version %d not supported", version))
|
|
||||||
}
|
|
||||||
|
|
||||||
img := image.NewGray(image.Rect(0, 0, size + margin * 2, size + margin * 2))
|
|
||||||
|
|
||||||
// fill white
|
|
||||||
for y := range size + margin * 2 {
|
|
||||||
for x := range size + margin * 2 {
|
|
||||||
img.Set(x, y, color.White)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw alignment squares
|
|
||||||
drawLargeAlignmentSquare(margin, margin, img)
|
|
||||||
drawLargeAlignmentSquare(margin, margin + size - 7, img)
|
|
||||||
drawLargeAlignmentSquare(margin + size - 7, margin, img)
|
|
||||||
drawSmallAlignmentSquare(size - 5, size - 5, img)
|
|
||||||
/*
|
|
||||||
if version > 4 {
|
|
||||||
space := version * 3 - 2
|
|
||||||
end := size / space
|
|
||||||
for y := range size / space + 1 {
|
|
||||||
for x := range size / space + 1 {
|
|
||||||
if x == 0 && y == 0 { continue }
|
|
||||||
if x == 0 && y == end { continue }
|
|
||||||
if x == end && y == 0 { continue }
|
|
||||||
if x == end && y == end { continue }
|
|
||||||
drawSmallAlignmentSquare(
|
|
||||||
x * space + margin + 4,
|
|
||||||
y * space + margin + 4,
|
|
||||||
img,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// draw timing bits
|
|
||||||
for i := margin + 6; i < size - 4; i++ {
|
|
||||||
if (i % 2 == 0) {
|
|
||||||
img.Set(i, margin + 6, color.Black)
|
|
||||||
img.Set(margin + 6, i, color.Black)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
img.Set(margin + 8, size - 4, color.Black)
|
|
||||||
|
|
||||||
var imgBuf bytes.Buffer
|
|
||||||
err := png.Encode(&imgBuf, img)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
base64Img := base64.StdEncoding.EncodeToString(imgBuf.Bytes())
|
|
||||||
|
|
||||||
return "data:image/png;base64," + base64Img, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func drawLargeAlignmentSquare(x int, y int, img *image.Gray) {
|
func drawLargeAlignmentSquare(x int, y int, img *image.Gray) {
|
||||||
for yi := range 7 {
|
for yi := range 7 {
|
||||||
for xi := range 7 {
|
for xi := range 7 {
|
||||||
|
|
94
controller/twitch.go
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"arimelody-web/model"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const TWITCH_API_BASE = "https://api.twitch.tv/helix/"
|
||||||
|
|
||||||
|
func TwitchSetup(app *model.AppState) error {
|
||||||
|
app.Twitch = &model.TwitchState{}
|
||||||
|
err := RefreshTwitchToken(app)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func RefreshTwitchToken(app *model.AppState) error {
|
||||||
|
if app.Twitch != nil && app.Twitch.Token != nil && time.Now().UTC().After(app.Twitch.Token.ExpiresAt) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
requestUrl, _ := url.Parse("https://id.twitch.tv/oauth2/token")
|
||||||
|
req, _ := http.NewRequest(http.MethodPost, requestUrl.String(), bytes.NewBuffer([]byte(url.Values{
|
||||||
|
"client_id": []string{ app.Config.Twitch.ClientID },
|
||||||
|
"client_secret": []string{ app.Config.Twitch.Secret },
|
||||||
|
"grant_type": []string{ "client_credentials" },
|
||||||
|
}.Encode())))
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type TwitchOAuthToken struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
}
|
||||||
|
oauthResponse := TwitchOAuthToken{}
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&oauthResponse)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Twitch.Token = &model.TwitchOAuthToken{
|
||||||
|
AccessToken: oauthResponse.AccessToken,
|
||||||
|
ExpiresAt: time.Now().UTC().Add(time.Second * time.Duration(oauthResponse.ExpiresIn)).UTC(),
|
||||||
|
TokenType: oauthResponse.TokenType,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastStreamState *model.TwitchStreamInfo
|
||||||
|
var lastStreamStateAt time.Time
|
||||||
|
|
||||||
|
func GetTwitchStatus(app *model.AppState, broadcaster string) (*model.TwitchStreamInfo, error) {
|
||||||
|
if lastStreamState != nil && time.Now().UTC().Before(lastStreamStateAt.Add(time.Minute)) {
|
||||||
|
return lastStreamState, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
requestUrl, _ := url.Parse(TWITCH_API_BASE + "streams")
|
||||||
|
requestUrl.RawQuery = url.Values{
|
||||||
|
"user_login": []string{ broadcaster },
|
||||||
|
}.Encode()
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, requestUrl.String(), nil)
|
||||||
|
req.Header.Set("Client-Id", app.Config.Twitch.ClientID)
|
||||||
|
req.Header.Set("Authorization", "Bearer " + app.Twitch.Token.AccessToken)
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type StreamsResponse struct {
|
||||||
|
Data []model.TwitchStreamInfo `json:"data"`
|
||||||
|
}
|
||||||
|
streamInfo := StreamsResponse{}
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&streamInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(streamInfo.Data) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lastStreamState = &streamInfo.Data[0]
|
||||||
|
lastStreamStateAt = time.Now().UTC()
|
||||||
|
return lastStreamState, nil
|
||||||
|
}
|
49
main.go
|
@ -22,7 +22,6 @@ import (
|
||||||
"arimelody-web/cursor"
|
"arimelody-web/cursor"
|
||||||
"arimelody-web/log"
|
"arimelody-web/log"
|
||||||
"arimelody-web/model"
|
"arimelody-web/model"
|
||||||
"arimelody-web/templates"
|
|
||||||
"arimelody-web/view"
|
"arimelody-web/view"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
|
@ -40,6 +39,7 @@ func main() {
|
||||||
|
|
||||||
app := model.AppState{
|
app := model.AppState{
|
||||||
Config: controller.GetConfig(),
|
Config: controller.GetConfig(),
|
||||||
|
Twitch: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialise database connection
|
// initialise database connection
|
||||||
|
@ -460,6 +460,13 @@ func main() {
|
||||||
// handle DB migrations
|
// handle DB migrations
|
||||||
controller.CheckDBVersionAndMigrate(app.DB)
|
controller.CheckDBVersionAndMigrate(app.DB)
|
||||||
|
|
||||||
|
if app.Config.Twitch != nil {
|
||||||
|
err = controller.TwitchSetup(&app)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "WARN: Failed to set up Twitch integration: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// initial invite code
|
// initial invite code
|
||||||
accountsCount := 0
|
accountsCount := 0
|
||||||
err = app.DB.Get(&accountsCount, "SELECT count(*) FROM account")
|
err = app.DB.Get(&accountsCount, "SELECT count(*) FROM account")
|
||||||
|
@ -512,49 +519,13 @@ func createServeMux(app *model.AppState) *http.ServeMux {
|
||||||
mux.Handle("/api/", http.StripPrefix("/api", api.Handler(app)))
|
mux.Handle("/api/", http.StripPrefix("/api", api.Handler(app)))
|
||||||
mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler(app)))
|
mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler(app)))
|
||||||
mux.Handle("/blog/", http.StripPrefix("/blog", view.BlogHandler(app)))
|
mux.Handle("/blog/", http.StripPrefix("/blog", view.BlogHandler(app)))
|
||||||
mux.Handle("/uploads/", http.StripPrefix("/uploads", staticHandler(filepath.Join(app.Config.DataDirectory, "uploads"))))
|
mux.Handle("/uploads/", http.StripPrefix("/uploads", view.StaticHandler(filepath.Join(app.Config.DataDirectory, "uploads"))))
|
||||||
mux.Handle("/cursor-ws", cursor.Handler(app))
|
mux.Handle("/cursor-ws", cursor.Handler(app))
|
||||||
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
mux.Handle("/", view.IndexHandler(app))
|
||||||
if r.Method == http.MethodHead {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
|
|
||||||
err := templates.IndexTemplate.Execute(w, nil)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
staticHandler("public").ServeHTTP(w, r)
|
|
||||||
}))
|
|
||||||
|
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
func staticHandler(directory string) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
info, err := os.Stat(filepath.Join(directory, filepath.Clean(r.URL.Path)))
|
|
||||||
|
|
||||||
// does the file exist?
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// is thjs a directory? (forbidden)
|
|
||||||
if info.IsDir() {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.FileServer(http.Dir(directory)).ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var PoweredByStrings = []string{
|
var PoweredByStrings = []string{
|
||||||
"nerd rage",
|
"nerd rage",
|
||||||
"estrogen",
|
"estrogen",
|
||||||
|
|
|
@ -21,6 +21,12 @@ type (
|
||||||
Secret string `toml:"secret"`
|
Secret string `toml:"secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TwitchConfig struct {
|
||||||
|
Broadcaster string `toml:"broadcaster"`
|
||||||
|
ClientID string `toml:"client_id"`
|
||||||
|
Secret string `toml:"secret"`
|
||||||
|
}
|
||||||
|
|
||||||
Config struct {
|
Config struct {
|
||||||
BaseUrl string `toml:"base_url" comment:"Used for OAuth redirects."`
|
BaseUrl string `toml:"base_url" comment:"Used for OAuth redirects."`
|
||||||
Host string `toml:"host"`
|
Host string `toml:"host"`
|
||||||
|
@ -28,12 +34,14 @@ type (
|
||||||
DataDirectory string `toml:"data_dir"`
|
DataDirectory string `toml:"data_dir"`
|
||||||
TrustedProxies []string `toml:"trusted_proxies"`
|
TrustedProxies []string `toml:"trusted_proxies"`
|
||||||
DB DBConfig `toml:"db"`
|
DB DBConfig `toml:"db"`
|
||||||
Discord DiscordConfig `toml:"discord"`
|
Discord *DiscordConfig `toml:"discord"`
|
||||||
|
Twitch *TwitchConfig `toml:"twitch"`
|
||||||
}
|
}
|
||||||
|
|
||||||
AppState struct {
|
AppState struct {
|
||||||
DB *sqlx.DB
|
DB *sqlx.DB
|
||||||
Config Config
|
Config Config
|
||||||
Log log.Logger
|
Log log.Logger
|
||||||
|
Twitch *TwitchState
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
43
model/twitch.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
TwitchOAuthToken struct {
|
||||||
|
AccessToken string
|
||||||
|
ExpiresAt time.Time
|
||||||
|
TokenType string
|
||||||
|
}
|
||||||
|
|
||||||
|
TwitchState struct {
|
||||||
|
Token *TwitchOAuthToken
|
||||||
|
}
|
||||||
|
|
||||||
|
TwitchStreamInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
UserLogin string `json:"user_login"`
|
||||||
|
UserName string `json:"user_name"`
|
||||||
|
GameID string `json:"game_id"`
|
||||||
|
GameName string `json:"game_name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
ViewerCount int `json:"viewer_count"`
|
||||||
|
StartedAt string `json:"started_at"`
|
||||||
|
Language string `json:"language"`
|
||||||
|
ThumbnailURL string `json:"thumbnail_url"`
|
||||||
|
TagIDs []string `json:"tag_ids"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
IsMature bool `json:"is_mature"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (info *TwitchStreamInfo) Thumbnail(width int, height int) string {
|
||||||
|
res := strings.Replace(info.ThumbnailURL, "{width}", fmt.Sprintf("%d", width), 1)
|
||||||
|
res = strings.Replace(res, "{height}", fmt.Sprintf("%d", height), 1)
|
||||||
|
return res
|
||||||
|
}
|
10
public/img/brand/bandcamp.svg
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g>
|
||||||
|
<path d="M256,512C397.384,512 512,397.385 512,256C512,114.616 397.384,0 256,0C114.615,0 0,114.616 0,256C0,397.385 114.615,512 256,512Z" style="fill:rgb(35,159,194);"/>
|
||||||
|
<path d="M324.857,238.405C306.507,238.405 297.131,252.847 297.131,274.605C297.131,295.171 307.269,310.609 324.857,310.609C344.746,310.609 352.202,292.407 352.202,274.605C352.188,256.015 342.817,238.405 324.851,238.405L324.857,238.405ZM276.1,184.409L297.896,184.409L297.896,236.624L298.282,236.624C304.209,226.737 316.637,220.603 327.728,220.603C358.89,220.603 374.001,245.137 374.001,275.007C374.001,302.492 360.618,328.405 331.358,328.405C317.974,328.405 303.633,325.051 297.13,311.596L296.752,311.596L296.752,325.647L276.098,325.647L276.098,184.412L276.1,184.409Z" style="fill:white;"/>
|
||||||
|
<path d="M454.389,257.598C452.667,245.136 443.874,238.406 431.827,238.406C420.54,238.406 404.674,244.541 404.674,275.598C404.674,292.61 411.938,310.613 430.87,310.613C443.488,310.613 452.281,301.899 454.389,287.262L476.185,287.262C472.169,313.768 456.302,328.405 430.87,328.405C399.893,328.405 382.876,305.663 382.876,275.598C382.876,244.742 399.129,220.609 431.635,220.609C454.579,220.609 474.089,232.476 476.185,257.6L454.425,257.6L454.389,257.598Z" style="fill:white;"/>
|
||||||
|
<path d="M199.895,325.339L36.407,325.339L112.753,184.409L276.242,184.409L199.895,325.339Z" style="fill:white;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
10
public/img/brand/bluesky.svg
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 568 501" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(1,0,0,1,-228,-281.442)">
|
||||||
|
<path d="M351.121,315.106C416.241,363.994 486.281,463.123 512,516.315C537.719,463.123 607.759,363.994 672.879,315.106C719.866,279.83 796,252.536 796,339.388C796,356.734 786.055,485.101 780.222,505.943C759.947,578.396 686.067,596.876 620.347,585.691C735.222,605.242 764.444,670.002 701.333,734.762C581.473,857.754 529.061,703.903 515.631,664.481C513.169,657.254 512.017,653.873 512,656.748C511.983,653.873 510.831,657.254 508.369,664.481C494.939,703.903 442.527,857.754 322.667,734.762C259.556,670.002 288.778,605.242 403.653,585.691C337.933,596.876 264.053,578.396 243.778,505.943C237.945,485.101 228,356.734 228,339.388C228,252.536 304.134,279.83 351.121,315.106Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.06233e-14,500.117,-500.117,3.06233e-14,512,281.442)"><stop offset="0" style="stop-color:rgb(10,122,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(89,185,255);stop-opacity:1"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
164
public/img/brand/codeberg.svg
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 4.2333332 4.2333335"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1468"
|
||||||
|
sodipodi:docname="codeberg-logo_icon_blue.svg"
|
||||||
|
inkscape:version="1.2-alpha1 (b6a15bb, 2022-02-23)"
|
||||||
|
inkscape:export-filename="/home/mray/Projects/Codeberg/logo/icon/png/codeberg-logo_icon_blue.png"
|
||||||
|
inkscape:export-xdpi="384"
|
||||||
|
inkscape:export-ydpi="384"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<title
|
||||||
|
id="title16">Codeberg logo</title>
|
||||||
|
<defs
|
||||||
|
id="defs1462">
|
||||||
|
<linearGradient
|
||||||
|
xlink:href="#linearGradient6924"
|
||||||
|
id="linearGradient6918"
|
||||||
|
x1="42519.285"
|
||||||
|
y1="-7078.7891"
|
||||||
|
x2="42575.336"
|
||||||
|
y2="-6966.9307"
|
||||||
|
gradientUnits="userSpaceOnUse" />
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient6924">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#2185d0;stop-opacity:0"
|
||||||
|
offset="0"
|
||||||
|
id="stop6920" />
|
||||||
|
<stop
|
||||||
|
id="stop6926"
|
||||||
|
offset="0.49517274"
|
||||||
|
style="stop-color:#2185d0;stop-opacity:0.48923996" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#2185d0;stop-opacity:0.63279623"
|
||||||
|
offset="1"
|
||||||
|
id="stop6922" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
xlink:href="#linearGradient6924-6"
|
||||||
|
id="linearGradient6918-3"
|
||||||
|
x1="42519.285"
|
||||||
|
y1="-7078.7891"
|
||||||
|
x2="42575.336"
|
||||||
|
y2="-6966.9307"
|
||||||
|
gradientUnits="userSpaceOnUse" />
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient6924-6">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#2185d0;stop-opacity:0;"
|
||||||
|
offset="0"
|
||||||
|
id="stop6920-7" />
|
||||||
|
<stop
|
||||||
|
id="stop6926-5"
|
||||||
|
offset="0.49517274"
|
||||||
|
style="stop-color:#2185d0;stop-opacity:0.30000001;" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#2185d0;stop-opacity:0.30000001;"
|
||||||
|
offset="1"
|
||||||
|
id="stop6922-3" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
showborder="false"
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="22.627417"
|
||||||
|
inkscape:cx="12.948893"
|
||||||
|
inkscape:cy="12.661631"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="svg1468"
|
||||||
|
inkscape:document-rotation="0"
|
||||||
|
showgrid="false"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0"
|
||||||
|
units="px"
|
||||||
|
inkscape:snap-global="false"
|
||||||
|
inkscape:snap-page="true"
|
||||||
|
showguides="false"
|
||||||
|
inkscape:window-width="1531"
|
||||||
|
inkscape:window-height="873"
|
||||||
|
inkscape:window-x="69"
|
||||||
|
inkscape:window-y="27"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid2067" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<metadata
|
||||||
|
id="metadata1465">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title>Codeberg logo</dc:title>
|
||||||
|
<cc:license
|
||||||
|
rdf:resource="http://creativecommons.org/publicdomain/zero/1.0/" />
|
||||||
|
<dc:creator>
|
||||||
|
<cc:Agent>
|
||||||
|
<dc:title>Robert Martinez</dc:title>
|
||||||
|
</cc:Agent>
|
||||||
|
</dc:creator>
|
||||||
|
<dc:rights>
|
||||||
|
<cc:Agent>
|
||||||
|
<dc:title>Codeberg and the Codeberg Logo are trademarks of Codeberg e.V.</dc:title>
|
||||||
|
</cc:Agent>
|
||||||
|
</dc:rights>
|
||||||
|
<dc:date>2020-04-09</dc:date>
|
||||||
|
<dc:publisher>
|
||||||
|
<cc:Agent>
|
||||||
|
<dc:title>Codeberg e.V.</dc:title>
|
||||||
|
</cc:Agent>
|
||||||
|
</dc:publisher>
|
||||||
|
<dc:source>codeberg.org</dc:source>
|
||||||
|
</cc:Work>
|
||||||
|
<cc:License
|
||||||
|
rdf:about="http://creativecommons.org/publicdomain/zero/1.0/">
|
||||||
|
<cc:permits
|
||||||
|
rdf:resource="http://creativecommons.org/ns#Reproduction" />
|
||||||
|
<cc:permits
|
||||||
|
rdf:resource="http://creativecommons.org/ns#Distribution" />
|
||||||
|
<cc:permits
|
||||||
|
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
|
||||||
|
</cc:License>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
id="g370484"
|
||||||
|
inkscape:label="logo"
|
||||||
|
transform="matrix(0.06551432,0,0,0.06551432,-2.232417,-1.431776)">
|
||||||
|
<path
|
||||||
|
id="path6733-5"
|
||||||
|
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:url(#linearGradient6918-3);fill-opacity:1;stroke:none;stroke-width:3.67846;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill;stop-color:#000000;stop-opacity:1"
|
||||||
|
d="m 42519.285,-7078.7891 a 0.76086879,0.56791688 0 0 0 -0.738,0.6739 l 33.586,125.8886 a 87.182358,87.182358 0 0 0 39.381,-33.7636 l -71.565,-92.5196 a 0.76086879,0.56791688 0 0 0 -0.664,-0.2793 z"
|
||||||
|
transform="matrix(0.37058478,0,0,0.37058478,-15690.065,2662.0533)"
|
||||||
|
inkscape:label="berg" />
|
||||||
|
<path
|
||||||
|
id="path360787"
|
||||||
|
style="opacity:1;fill:#2185d0;fill-opacity:1;stroke-width:17.0055;paint-order:markers fill stroke;stop-color:#000000"
|
||||||
|
d="m 11249.461,-1883.6961 c -12.74,0 -23.067,10.3275 -23.067,23.0671 0,4.3335 1.22,8.5795 3.522,12.2514 l 19.232,-24.8636 c 0.138,-0.1796 0.486,-0.1796 0.624,0 l 19.233,24.8646 c 2.302,-3.6721 3.523,-7.9185 3.523,-12.2524 0,-12.7396 -10.327,-23.0671 -23.067,-23.0671 z"
|
||||||
|
sodipodi:nodetypes="sccccccs"
|
||||||
|
inkscape:label="sky"
|
||||||
|
transform="matrix(1.4006354,0,0,1.4006354,-15690.065,2662.0533)" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 5.8 KiB |
1
public/img/brand/discord.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg id="Discord-Logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 126.644 96"><defs><style>.cls-1{fill:#5865f2;}</style></defs><path id="Discord-Symbol-Blurple" class="cls-1" d="M81.15,0c-1.2376,2.1973-2.3489,4.4704-3.3591,6.794-9.5975-1.4396-19.3718-1.4396-28.9945,0-.985-2.3236-2.1216-4.5967-3.3591-6.794-9.0166,1.5407-17.8059,4.2431-26.1405,8.0568C2.779,32.5304-1.6914,56.3725.5312,79.8863c9.6732,7.1476,20.5083,12.603,32.0505,16.0884,2.6014-3.4854,4.8998-7.1981,6.8698-11.0623-3.738-1.3891-7.3497-3.1318-10.8098-5.1523.9092-.6567,1.7932-1.3386,2.6519-1.9953,20.281,9.547,43.7696,9.547,64.0758,0,.8587.7072,1.7427,1.3891,2.6519,1.9953-3.4601,2.0457-7.0718,3.7632-10.835,5.1776,1.97,3.8642,4.2683,7.5769,6.8698,11.0623,11.5419-3.4854,22.3769-8.9156,32.0509-16.0631,2.626-27.2771-4.496-50.9172-18.817-71.8548C98.9811,4.2684,90.1918,1.5659,81.1752.0505l-.0252-.0505ZM42.2802,65.4144c-6.2383,0-11.4159-5.6575-11.4159-12.6535s4.9755-12.6788,11.3907-12.6788,11.5169,5.708,11.4159,12.6788c-.101,6.9708-5.026,12.6535-11.3907,12.6535ZM84.3576,65.4144c-6.2637,0-11.3907-5.6575-11.3907-12.6535s4.9755-12.6788,11.3907-12.6788,11.4917,5.708,11.3906,12.6788c-.101,6.9708-5.026,12.6535-11.3906,12.6535Z"/></svg>
|
After Width: | Height: | Size: 1.2 KiB |
21
public/img/brand/twitch.svg
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 23.0.6, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 2400 2800" style="enable-background:new 0 0 2400 2800;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#FFFFFF;}
|
||||||
|
.st1{fill:#9146FF;}
|
||||||
|
</style>
|
||||||
|
<title>Asset 2</title>
|
||||||
|
<g>
|
||||||
|
<polygon class="st0" points="2200,1300 1800,1700 1400,1700 1050,2050 1050,1700 600,1700 600,200 2200,200 "/>
|
||||||
|
<g>
|
||||||
|
<g id="Layer_1-2">
|
||||||
|
<path class="st1" d="M500,0L0,500v1800h600v500l500-500h400l900-900V0H500z M2200,1300l-400,400h-400l-350,350v-350H600V200h1600
|
||||||
|
V1300z"/>
|
||||||
|
<rect x="1700" y="550" class="st1" width="200" height="600"/>
|
||||||
|
<rect x="1150" y="550" class="st1" width="200" height="600"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 890 B |
10
public/img/brand/youtube.svg
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 507 355" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(4.16667,0,0,4.16667,495.608,299.004)">
|
||||||
|
<path d="M0,-58.482C-1.397,-63.709 -5.514,-67.825 -10.741,-69.222C-20.215,-71.761 -58.204,-71.761 -58.204,-71.761C-58.204,-71.761 -96.193,-71.761 -105.667,-69.222C-110.894,-67.825 -115.011,-63.709 -116.408,-58.482C-118.946,-49.008 -118.946,-29.241 -118.946,-29.241C-118.946,-29.241 -118.946,-9.474 -116.408,-0.001C-115.011,5.226 -110.894,9.343 -105.667,10.74C-96.193,13.279 -58.204,13.279 -58.204,13.279C-58.204,13.279 -20.215,13.279 -10.741,10.74C-5.514,9.343 -1.397,5.226 0,-0.001C2.539,-9.474 2.539,-29.241 2.539,-29.241C2.539,-29.241 2.539,-49.008 0,-58.482" style="fill:rgb(255,0,0);fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(4.16667,0,0,4.16667,202.472,101.237)">
|
||||||
|
<path d="M0,36.446L31.562,18.223L0,0L0,36.446Z" style="fill:white;fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/img/buttons/thermia.gif
Normal file
After Width: | Height: | Size: 9.9 KiB |
|
@ -1,3 +1,5 @@
|
||||||
|
import { hijackClickEvent } from "./main.js";
|
||||||
|
|
||||||
const hexPrimary = document.getElementById("hex-primary");
|
const hexPrimary = document.getElementById("hex-primary");
|
||||||
const hexSecondary = document.getElementById("hex-secondary");
|
const hexSecondary = document.getElementById("hex-secondary");
|
||||||
const hexTertiary = document.getElementById("hex-tertiary");
|
const hexTertiary = document.getElementById("hex-tertiary");
|
||||||
|
@ -14,3 +16,8 @@ updateHexColours();
|
||||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
|
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
|
||||||
updateHexColours();
|
updateHexColours();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll("ul#projects li.project-item").forEach(projectItem => {
|
||||||
|
const link = projectItem.querySelector('a');
|
||||||
|
hijackClickEvent(projectItem, link);
|
||||||
|
});
|
||||||
|
|
|
@ -45,6 +45,23 @@ function fill_list(list) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hijackClickEvent(container, link) {
|
||||||
|
container.addEventListener('click', event => {
|
||||||
|
if (event.target.tagName.toLowerCase() === 'a') return;
|
||||||
|
event.preventDefault();
|
||||||
|
link.dispatchEvent(new MouseEvent('click', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
view: window,
|
||||||
|
ctrlKey: event.ctrlKey,
|
||||||
|
metaKey: event.metaKey,
|
||||||
|
shiftKey: event.shiftKey,
|
||||||
|
altKey: event.altKey,
|
||||||
|
button: event.button,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
[...document.querySelectorAll(".typeout")]
|
[...document.querySelectorAll(".typeout")]
|
||||||
.filter((e) => e.innerText != "")
|
.filter((e) => e.innerText != "")
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
import "./main.js";
|
import { hijackClickEvent } from "./main.js";
|
||||||
|
|
||||||
document.querySelectorAll("div.music").forEach(container => {
|
document.querySelectorAll("div.music").forEach(container => {
|
||||||
const link = container.querySelector(".music-title a").href
|
const link = container.querySelector(".music-title a")
|
||||||
|
hijackClickEvent(container, link);
|
||||||
container.addEventListener("click", event => {
|
|
||||||
if (event.target.href) return;
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
location = link;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
--secondary: #f8e05b;
|
--secondary: #f8e05b;
|
||||||
--tertiary: #f788fe;
|
--tertiary: #f788fe;
|
||||||
--links: #5eb2ff;
|
--links: #5eb2ff;
|
||||||
|
--live: #fd3737;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
|
|
|
@ -25,7 +25,6 @@ nav {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: .5em;
|
gap: .5em;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
img#header-icon {
|
img#header-icon {
|
||||||
|
@ -36,19 +35,19 @@ img#header-icon {
|
||||||
}
|
}
|
||||||
|
|
||||||
#header-text {
|
#header-text {
|
||||||
width: 11em;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-grow: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#header-text h1 {
|
#header-text h1 {
|
||||||
|
width: fit-content;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#header-text h2 {
|
#header-text h2 {
|
||||||
|
width: fit-content;
|
||||||
height: 1.2em;
|
height: 1.2em;
|
||||||
line-height: 1.2em;
|
line-height: 1.2em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -154,7 +153,7 @@ header ul li a:hover {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
border-bottom: 1px solid #888;
|
border-bottom: 1px solid #888;
|
||||||
background: #080808;
|
background: var(--background);
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -96,30 +96,36 @@ hr {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.links {
|
ul.platform-links {
|
||||||
|
padding-left: 1em;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1em .5em;
|
gap: .5em;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.links li {
|
ul.platform-links li {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.links li a {
|
ul.platform-links li a {
|
||||||
padding: .4em .5em;
|
padding: .4em .5em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: .5em;
|
||||||
border: 1px solid var(--links);
|
border: 1px solid var(--links);
|
||||||
color: var(--links);
|
color: var(--links);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
transition-property: color, border-color, background-color;
|
transition-property: color, border-color, background-color, box-shadow;
|
||||||
transition-duration: .2s;
|
transition-duration: .2s;
|
||||||
animation-delay: 0s;
|
animation-delay: 0s;
|
||||||
animation: list-item-fadein .2s forwards;
|
animation: list-item-fadein .2s forwards;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.links li a:hover {
|
ul.platform-links li a:hover {
|
||||||
color: #eee;
|
color: #eee;
|
||||||
border-color: #eee;
|
border-color: #eee;
|
||||||
background-color: var(--links) !important;
|
background-color: var(--links) !important;
|
||||||
|
@ -127,6 +133,75 @@ ul.links li a:hover {
|
||||||
box-shadow: 0 0 1em var(--links);
|
box-shadow: 0 0 1em var(--links);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.platform-links li a img {
|
||||||
|
height: 1em;
|
||||||
|
width: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul#projects {
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.project-item {
|
||||||
|
padding: .5em;
|
||||||
|
border: 1px solid var(--links);
|
||||||
|
margin: 1em 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: .5em;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition-property: color, border-color, background-color, box-shadow;
|
||||||
|
transition-duration: .2s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
li.project-item a {
|
||||||
|
transition: color .2s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.project-item:hover {
|
||||||
|
color: #eee;
|
||||||
|
border-color: #eee;
|
||||||
|
background-color: var(--links) !important;
|
||||||
|
text-decoration: none;
|
||||||
|
box-shadow: 0 0 1em var(--links);
|
||||||
|
}
|
||||||
|
li.project-item:hover a {
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.project-item .project-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.project-item img.project-icon {
|
||||||
|
width: 2.5em;
|
||||||
|
height: 2.5em;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.project-item span.project-icon {
|
||||||
|
font-size: 2em;
|
||||||
|
display: block;
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
text-align: center;
|
||||||
|
/* background: #0004; */
|
||||||
|
/* border: 1px solid var(--on-background); */
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.project-item a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.project-item p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
div#web-buttons {
|
div#web-buttons {
|
||||||
margin: 2rem 0;
|
margin: 2rem 0;
|
||||||
}
|
}
|
||||||
|
@ -148,3 +223,112 @@ div#web-buttons {
|
||||||
box-shadow: 1px 1px 0 #eee, 2px 2px 0 #eee;
|
box-shadow: 1px 1px 0 #eee, 2px 2px 0 #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#live-banner {
|
||||||
|
margin: 1em 0 2em 0;
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--primary);
|
||||||
|
box-shadow: 0 0 8px var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#live-banner p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-highlight {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-preview div:first-of-type {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: .3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-thumbnail {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-button {
|
||||||
|
margin: .2em;
|
||||||
|
padding: .4em .5em;
|
||||||
|
display: inline-block;
|
||||||
|
color: var(--primary);
|
||||||
|
border: 1px solid var(--primary);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: color .1s linear, background-color .1s linear, box-shadow .1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-button:hover {
|
||||||
|
color: var(--background);
|
||||||
|
background-color: var(--primary);
|
||||||
|
box-shadow: 0 0 8px var(--primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: .3em;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#live-banner h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--on-background);
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-weight: 800;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-pinger {
|
||||||
|
width: .5em;
|
||||||
|
height: .5em;
|
||||||
|
margin: .1em .2em;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 100%;
|
||||||
|
background-color: var(--primary);
|
||||||
|
box-shadow: 0 0 4px var(--primary);
|
||||||
|
animation: live-pinger-pulse 1s infinite alternate ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes live-pinger-pulse {
|
||||||
|
from {
|
||||||
|
opacity: .8;
|
||||||
|
transform: scale(1.0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-game {
|
||||||
|
overflow: hidden;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-game .live-game-prefix {
|
||||||
|
opacity: .8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-title {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-viewers {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
@import url("/style/footer.css");
|
@import url("/style/footer.css");
|
||||||
@import url("/style/prideflag.css");
|
@import url("/style/prideflag.css");
|
||||||
@import url("/style/cursor.css");
|
@import url("/style/cursor.css");
|
||||||
|
@import url("/font/inter/inter.css");
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Monaspace Argon";
|
font-family: "Monaspace Argon";
|
||||||
|
|
|
@ -2,7 +2,12 @@
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<div id="footer">
|
<div id="footer">
|
||||||
<small><em>*made with <span aria-label="love">♥</span> by ari, 2025*</em></small>
|
<small>
|
||||||
|
<em>
|
||||||
|
*made with <span aria-label="love">♥</span> by ari, 2025*
|
||||||
|
<a href="https://git.arimelody.me/ari/arimelody.me" target="_blank">source</a>
|
||||||
|
</em>
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|
|
@ -23,9 +23,6 @@
|
||||||
<li>
|
<li>
|
||||||
<a href="/music" preload="mouseover">music</a>
|
<a href="/music" preload="mouseover">music</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<a href="https://git.arimelody.me/ari/arimelody.me" target="_blank">source</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<!-- coming later! -->
|
<!-- coming later! -->
|
||||||
<span title="coming later!">blog</span>
|
<span title="coming later!">blog</span>
|
||||||
|
|
45
view/index.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package view
|
||||||
|
|
||||||
|
import (
|
||||||
|
"arimelody-web/controller"
|
||||||
|
"arimelody-web/model"
|
||||||
|
"arimelody-web/templates"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IndexHandler(app *model.AppState) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodHead {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type IndexData struct {
|
||||||
|
TwitchStatus *model.TwitchStreamInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var twitchStatus *model.TwitchStreamInfo = nil
|
||||||
|
if app.Twitch != nil && len(app.Config.Twitch.Broadcaster) > 0 {
|
||||||
|
twitchStatus, err = controller.GetTwitchStatus(app, app.Config.Twitch.Broadcaster)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "WARN: Failed to get Twitch status for %s: %v\n", app.Config.Twitch.Broadcaster, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
|
||||||
|
err := templates.IndexTemplate.Execute(w, IndexData{
|
||||||
|
TwitchStatus: twitchStatus,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "WARN: Failed to render index page: %v\n", err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
StaticHandler("public").ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
163
view/index.html
|
@ -6,8 +6,8 @@
|
||||||
|
|
||||||
<meta property="og:title" content="ari melody">
|
<meta property="og:title" content="ari melody">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:url" content="www.arimelody.me">
|
<meta property="og:url" content="www.arimelody.space">
|
||||||
<meta property="og:image" content="https://www.arimelody.me/img/favicon.png">
|
<meta property="og:image" content="https://www.arimelody.space/img/favicon.png">
|
||||||
<meta property="og:site_name" content="ari melody">
|
<meta property="og:site_name" content="ari melody">
|
||||||
<meta property="og:description" content="home to your local SPACEGIRL 💫">
|
<meta property="og:description" content="home to your local SPACEGIRL 💫">
|
||||||
|
|
||||||
|
@ -17,11 +17,28 @@
|
||||||
<link rel="me" href="https://ice.arimelody.me/@ari">
|
<link rel="me" href="https://ice.arimelody.me/@ari">
|
||||||
<link rel="me" href="https://wetdry.world/@ari">
|
<link rel="me" href="https://wetdry.world/@ari">
|
||||||
|
|
||||||
<script type="module" src="/script/index.js" defer> </script>
|
<script type="module" src="/script/index.js" defer></script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<main>
|
<main>
|
||||||
|
{{if .TwitchStatus}}
|
||||||
|
<div id="live-banner">
|
||||||
|
<div class="live-preview">
|
||||||
|
<div>
|
||||||
|
<img src="{{.TwitchStatus.Thumbnail 144 81}}" alt="livestream thumbnail" class="live-thumbnail">
|
||||||
|
<a href="https://twitch.tv/{{.TwitchStatus.UserName}}" class="live-button">join in!</a>
|
||||||
|
</div>
|
||||||
|
<div class="live-info">
|
||||||
|
<h2>ari melody <span class="live-highlight">LIVE</span> <i class="live-pinger"></i></h2>
|
||||||
|
<p class="live-game"><span class="live-game-prefix">streaming:</span> {{.TwitchStatus.GameName}}</p>
|
||||||
|
<p class="live-title">{{.TwitchStatus.Title}}</p>
|
||||||
|
<p class="live-viewers">{{.TwitchStatus.ViewerCount}} viewers</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<h1 class="typeout">
|
<h1 class="typeout">
|
||||||
# hello, world!
|
# hello, world!
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -33,7 +50,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
i'm a <a href="/music">musician</a>, <a href="https://github.com/arimelody?tab=repositories">developer</a>,
|
i'm a <a href="/music">musician</a>, <a href="https://codeberg.org/arimelody?tab=repositories">developer</a>,
|
||||||
<a href="https://twitch.tv/arispacegirl">streamer</a>, <a href="https://youtube.com/@arispacegirl">youtuber</a>,
|
<a href="https://twitch.tv/arispacegirl">streamer</a>, <a href="https://youtube.com/@arispacegirl">youtuber</a>,
|
||||||
and probably a bunch of other things i forgot to mention!
|
and probably a bunch of other things i forgot to mention!
|
||||||
</p>
|
</p>
|
||||||
|
@ -44,7 +61,7 @@
|
||||||
<p>
|
<p>
|
||||||
if you're looking to support me financially, that's so cool of you!!
|
if you're looking to support me financially, that's so cool of you!!
|
||||||
if you like, you can buy some of my music over on
|
if you like, you can buy some of my music over on
|
||||||
<a href="https://arimelody.bandcamp.com" target="_blank">bandcamp</a>
|
<a href="https://arimelody.bandcamp.com">bandcamp</a>
|
||||||
so you can at least get something for your money.
|
so you can at least get something for your money.
|
||||||
thank you very much either way!! 💕
|
thank you very much either way!! 💕
|
||||||
</p>
|
</p>
|
||||||
|
@ -84,57 +101,97 @@
|
||||||
<p>
|
<p>
|
||||||
<strong>where to find me 🛰️</strong>
|
<strong>where to find me 🛰️</strong>
|
||||||
</p>
|
</p>
|
||||||
<ul class="links">
|
<ul class="platform-links">
|
||||||
<li>
|
<li>
|
||||||
<a href="https://youtube.com/@arispacegirl" target="_blank">youtube</a>
|
<a href="https://youtube.com/@arispacegirl" title="youtube">
|
||||||
|
<img src="/img/brand/youtube.svg" alt="youtube" width="32" height="32"/>
|
||||||
|
youtube
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://twitch.tv/arispacegirl" target="_blank">twitch</a>
|
<a href="https://twitch.tv/arispacegirl" title="twitch">
|
||||||
|
<img src="/img/brand/twitch.svg" alt="twitch" width="32" height="32"/>
|
||||||
|
twitch
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://sptfy.com/mellodoot" target="_blank">spotify</a>
|
<a href="https://arimelody.bandcamp.com" title="bandcamp">
|
||||||
|
<img src="/img/brand/bandcamp.svg" alt="bandcamp" width="32" height="32"/>
|
||||||
|
bandcamp
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://arimelody.bandcamp.com" target="_blank">bandcamp</a>
|
<a href="https://codeberg.org/arimelody" title="codeberg">
|
||||||
|
<img src="/img/brand/codeberg.svg" alt="codeberg" width="32" height="32"/>
|
||||||
|
codeberg
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://github.com/arimelody" target="_blank">github</a>
|
<a href="https://bsky.app/profile/arimelody.space" title="bluesky">
|
||||||
|
<img src="/img/brand/bluesky.svg" alt="bluesky" width="32" height="32"/>
|
||||||
|
bluesky
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://arimelody.space/discord" title="discord">
|
||||||
|
<img src="/img/brand/discord.svg" alt="discord" width="32" height="32"/>
|
||||||
|
discord
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<strong>projects i've worked on 🛠️</strong>
|
<strong>projects i've worked on 🛠️</strong>
|
||||||
</p>
|
</p>
|
||||||
<ul class="links">
|
<ul id="projects">
|
||||||
<li>
|
<li class="project-item">
|
||||||
<a href="https://catdance.arimelody.me" target="_blank">
|
<span aria-hidden=true class="project-icon">⛏️</span>
|
||||||
catdance
|
<div class="project-info">
|
||||||
</a>
|
<a href="https://mcq.bliss.town">McStatusFace</a>
|
||||||
|
<p>minecraft server query utility</p>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li class="project-item">
|
||||||
<a href="https://git.arimelody.me/ari/prideflag" target="_blank">
|
<img src="https://catdance.arimelody.me/img/favicon.png" alt="catdance icon" aria-hidden=true class="project-icon" width="64" height="64">
|
||||||
pride flag
|
<div class="project-info">
|
||||||
</a>
|
<a href="https://catdance.arimelody.me">catdance</a>
|
||||||
|
<p>watch the cat dance 🐱</p>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li class="project-item">
|
||||||
<a href="https://github.com/arimelody/ipaddrgen" target="_blank">
|
<img src="https://git.arimelody.me/repo-avatars/6b0a1ffb78cbc6f906f83152ea42a710220174e8f48a3e44f159ae58dacd7a2f" alt="pride flag icon" aria-hidden=true class="project-icon" width="64" height="64">
|
||||||
ipaddrgen
|
<div class="project-info">
|
||||||
</a>
|
<a href="https://git.arimelody.me/ari/prideflag">pride flag</a>
|
||||||
|
<p>progressive pride flag widget for websites</p>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li class="project-item">
|
||||||
<a href="https://impact.arimelody.me/" target="_blank">
|
<span aria-hidden=true class="project-icon">👩💻</span>
|
||||||
impact meme
|
<div class="project-info">
|
||||||
</a>
|
<a href="https://github.com/arimelody/ipaddrgen">ipaddrgen</a>
|
||||||
|
<p>silly hackerman IP address generator</p>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li class="project-item">
|
||||||
<a href="https://term.arimelody.me/" target="_blank">
|
<img src="https://impact.arimelody.me/favicon.png" alt="impact meme icon" aria-hidden=true class="project-icon" width="64" height="64">
|
||||||
OpenTerminal
|
<div class="project-info">
|
||||||
</a>
|
<a href="https://impact.arimelody.me/">impact meme</a>
|
||||||
|
<p>impact meme generator</p>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li class="project-item">
|
||||||
<a href="https://silver.bliss.town/" target="_blank">
|
<img src="https://codeberg.org/repo-avatars/e67303eeda4fa6d268948e71b7b0837357d8c519772701ffc36b84ae7975319f" alt="OpenTerminal icon" aria-hidden=true class="project-icon" width="64" height="64">
|
||||||
Silver.js
|
<div class="project-info">
|
||||||
</a>
|
<a href="https://term.arimelody.me/">OpenTerminal</a>
|
||||||
|
<p>communal online text buffer</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="project-item">
|
||||||
|
<span aria-hidden=true class="project-icon">📜</span>
|
||||||
|
<div class="project-info">
|
||||||
|
<a href="https://silver.bliss.town/">Silver.js</a>
|
||||||
|
<p>lightweight reactive state library</p>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
@ -145,43 +202,43 @@
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div id="web-buttons">
|
<div id="web-buttons">
|
||||||
<a href="https://arimelody.me">
|
<a href="https://arimelody.space">
|
||||||
<img src="/img/buttons/ari melody.gif" alt="ari melody web button" width="88" height="31">
|
<img src="/img/buttons/ari melody.gif" alt="ari melody web button" width="88" height="31">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://supitszaire.com" target="_blank">
|
<a href="https://supitszaire.com">
|
||||||
<img src="/img/buttons/zaire.gif" alt="zaire web button" width="88" height="31">
|
<img src="/img/buttons/zaire.gif" alt="zaire web button" width="88" height="31">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://mae.wtf" target="_blank">
|
<a href="https://mae.wtf">
|
||||||
<img src="/img/buttons/mae.png" alt="vimae web button" width="88" height="31">
|
<img src="/img/buttons/mae.png" alt="vimae web button" width="88" height="31">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://zvava.org" target="_blank">
|
<a href="https://girlthi.ng/~thermia/">
|
||||||
<img src="/img/buttons/zvava.png" alt="zvava web button" width="88" height="31">
|
<img src="/img/buttons/thermia.gif" alt="thermia web button" width="88" height="31">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://elke.cafe" target="_blank">
|
<a href="https://elke.cafe">
|
||||||
<img src="/img/buttons/elke.gif" alt="elke web button" width="88" height="31">
|
<img src="/img/buttons/elke.gif" alt="elke web button" width="88" height="31">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://invoxiplaygames.uk/" target="_blank">
|
<a href="https://invoxiplaygames.uk/">
|
||||||
<img src="/img/buttons/ipg.png" alt="InvoxiPlayGames web button" width="88" height="31">
|
<img src="/img/buttons/ipg.png" alt="InvoxiPlayGames web button" width="88" height="31">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://ioletsgo.gay" target="_blank">
|
<a href="https://ioletsgo.gay">
|
||||||
<img src="/img/buttons/ioletsgo.gif" alt="ioletsgo web button" width="88" height="31">
|
<img src="/img/buttons/ioletsgo.gif" alt="ioletsgo web button" width="88" height="31">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://notnite.com/" target="_blank">
|
<a href="https://notnite.com/">
|
||||||
<img src="/img/buttons/notnite.png" alt="notnite web button" width="88" height="31">
|
<img src="/img/buttons/notnite.png" alt="notnite web button" width="88" height="31">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.da.vidbuchanan.co.uk/" target="_blank">
|
<a href="https://www.da.vidbuchanan.co.uk/">
|
||||||
<img src="/img/buttons/retr0id_now.gif" alt="retr0id web button" width="88" height="31">
|
<img src="/img/buttons/retr0id_now.gif" alt="retr0id web button" width="88" height="31">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://aikoyori.xyz" target="_blank">
|
<a href="https://aikoyori.xyz">
|
||||||
<img src="/img/buttons/aikoyori.gif" alt="aikoyori web button" width="88" height="31">
|
<img src="/img/buttons/aikoyori.gif" alt="aikoyori web button" width="88" height="31">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://xenia.blahaj.land/" target="_blank">
|
<a href="https://xenia.blahaj.land/">
|
||||||
<img src="/img/buttons/xenia.png" alt="xenia web button" width="88" height="31">
|
<img src="/img/buttons/xenia.png" alt="xenia web button" width="88" height="31">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://stardust.elysium.gay/" target="_blank">
|
<a href="https://stardust.elysium.gay/">
|
||||||
<img src="/img/buttons/stardust.png" alt="stardust web button" width="88" height="31">
|
<img src="/img/buttons/stardust.png" alt="stardust web button" width="88" height="31">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://isabelroses.com/" target="_blank">
|
<a href="https://isabelroses.com/">
|
||||||
<img src="/img/buttons/isabelroses.gif" alt="isabel roses web button" width="88" height="31">
|
<img src="/img/buttons/isabelroses.gif" alt="isabel roses web button" width="88" height="31">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
@ -197,16 +254,16 @@
|
||||||
<img src="/img/buttons/misc/sprunk.gif" alt="sprunk" width="88" height="31">
|
<img src="/img/buttons/misc/sprunk.gif" alt="sprunk" width="88" height="31">
|
||||||
<img src="/img/buttons/misc/tohell.gif" alt="go straight to hell" width="88" height="31">
|
<img src="/img/buttons/misc/tohell.gif" alt="go straight to hell" width="88" height="31">
|
||||||
<img src="/img/buttons/misc/virusalert.gif" alt="virus alert! click here" onclick="alert('meow :3')" width="88" height="31">
|
<img src="/img/buttons/misc/virusalert.gif" alt="virus alert! click here" onclick="alert('meow :3')" width="88" height="31">
|
||||||
<a href="http://wiishopchannel.net/" target="_blank">
|
<a href="http://wiishopchannel.net/">
|
||||||
<img src="/img/buttons/misc/wii.gif" alt="wii" width="88" height="31">
|
<img src="/img/buttons/misc/wii.gif" alt="wii" width="88" height="31">
|
||||||
</a>
|
</a>
|
||||||
<img src="/img/buttons/misc/www2.gif" alt="www" width="88" height="31">
|
<img src="/img/buttons/misc/www2.gif" alt="www" width="88" height="31">
|
||||||
<img src="/img/buttons/misc/iemandatory.gif" alt="get mandatory internet explorer" width="88" height="31">
|
<img src="/img/buttons/misc/iemandatory.gif" alt="get mandatory internet explorer" width="88" height="31">
|
||||||
<img src="/img/buttons/misc/learn_html.gif" alt="HTML - learn it today!" width="88" height="31">
|
<img src="/img/buttons/misc/learn_html.gif" alt="HTML - learn it today!" width="88" height="31">
|
||||||
<a href="https://smokepowered.com" target="_blank">
|
<a href="https://smokepowered.com">
|
||||||
<img src="/img/buttons/misc/smokepowered.gif" alt="high on SMOKE" width="88" height="31">
|
<img src="/img/buttons/misc/smokepowered.gif" alt="high on SMOKE" width="88" height="31">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://epicblazed.com" target="_blank">
|
<a href="https://epicblazed.com">
|
||||||
<img src="/img/buttons/misc/epicblazed.png" alt="epic blazed" width="88" height="31">
|
<img src="/img/buttons/misc/epicblazed.png" alt="epic blazed" width="88" height="31">
|
||||||
</a>
|
</a>
|
||||||
<img src="/img/buttons/misc/blink.gif" alt="closeup anime blink" width="88" height="31">
|
<img src="/img/buttons/misc/blink.gif" alt="closeup anime blink" width="88" height="31">
|
||||||
|
|
31
view/static.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package view
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StaticHandler(directory string) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
info, err := os.Stat(filepath.Join(directory, filepath.Clean(r.URL.Path)))
|
||||||
|
|
||||||
|
// does the file exist?
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// is thjs a directory? (forbidden)
|
||||||
|
if info.IsDir() {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.FileServer(http.Dir(directory)).ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|