diff --git a/controller/config.go b/controller/config.go index fdfa756..1d3cbbb 100644 --- a/controller/config.go +++ b/controller/config.go @@ -77,9 +77,5 @@ 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_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 } diff --git a/controller/qr.go b/controller/qr.go index dd08637..6b04e69 100644 --- a/controller/qr.go +++ b/controller/qr.go @@ -1,9 +1,13 @@ package controller import ( + "bytes" "encoding/base64" + "errors" + "fmt" "image" "image/color" + "image/png" "github.com/skip2/go-qrcode" ) @@ -29,6 +33,69 @@ const ( 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) { for yi := range 7 { for xi := range 7 { diff --git a/controller/twitch.go b/controller/twitch.go deleted file mode 100644 index 98d3b9a..0000000 --- a/controller/twitch.go +++ /dev/null @@ -1,94 +0,0 @@ -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 -} diff --git a/main.go b/main.go index e7a0291..1e37bb1 100644 --- a/main.go +++ b/main.go @@ -22,6 +22,7 @@ import ( "arimelody-web/cursor" "arimelody-web/log" "arimelody-web/model" + "arimelody-web/templates" "arimelody-web/view" "github.com/jmoiron/sqlx" @@ -39,7 +40,6 @@ func main() { app := model.AppState{ Config: controller.GetConfig(), - Twitch: nil, } // initialise database connection @@ -460,13 +460,6 @@ func main() { // handle DB migrations 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 accountsCount := 0 err = app.DB.Get(&accountsCount, "SELECT count(*) FROM account") @@ -519,13 +512,49 @@ func createServeMux(app *model.AppState) *http.ServeMux { mux.Handle("/api/", http.StripPrefix("/api", api.Handler(app))) mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler(app))) mux.Handle("/blog/", http.StripPrefix("/blog", view.BlogHandler(app))) - mux.Handle("/uploads/", http.StripPrefix("/uploads", view.StaticHandler(filepath.Join(app.Config.DataDirectory, "uploads")))) + mux.Handle("/uploads/", http.StripPrefix("/uploads", staticHandler(filepath.Join(app.Config.DataDirectory, "uploads")))) mux.Handle("/cursor-ws", cursor.Handler(app)) - mux.Handle("/", view.IndexHandler(app)) + mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + 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 } +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{ "nerd rage", "estrogen", diff --git a/model/appstate.go b/model/appstate.go index 3a1c230..a910a29 100644 --- a/model/appstate.go +++ b/model/appstate.go @@ -21,12 +21,6 @@ type ( Secret string `toml:"secret"` } - TwitchConfig struct { - Broadcaster string `toml:"broadcaster"` - ClientID string `toml:"client_id"` - Secret string `toml:"secret"` - } - Config struct { BaseUrl string `toml:"base_url" comment:"Used for OAuth redirects."` Host string `toml:"host"` @@ -34,14 +28,12 @@ type ( DataDirectory string `toml:"data_dir"` TrustedProxies []string `toml:"trusted_proxies"` DB DBConfig `toml:"db"` - Discord *DiscordConfig `toml:"discord"` - Twitch *TwitchConfig `toml:"twitch"` + Discord DiscordConfig `toml:"discord"` } AppState struct { DB *sqlx.DB Config Config Log log.Logger - Twitch *TwitchState } ) diff --git a/model/twitch.go b/model/twitch.go deleted file mode 100644 index 6bca17d..0000000 --- a/model/twitch.go +++ /dev/null @@ -1,43 +0,0 @@ -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 -} diff --git a/public/img/brand/bandcamp.svg b/public/img/brand/bandcamp.svg deleted file mode 100644 index 9623ec2..0000000 --- a/public/img/brand/bandcamp.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/public/img/brand/bluesky.svg b/public/img/brand/bluesky.svg deleted file mode 100644 index d77fafe..0000000 --- a/public/img/brand/bluesky.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/public/img/brand/codeberg.svg b/public/img/brand/codeberg.svg deleted file mode 100644 index 028b729..0000000 --- a/public/img/brand/codeberg.svg +++ /dev/null @@ -1,164 +0,0 @@ - - - Codeberg logo - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - Codeberg logo - - - - Robert Martinez - - - - - Codeberg and the Codeberg Logo are trademarks of Codeberg e.V. - - - 2020-04-09 - - - Codeberg e.V. - - - codeberg.org - - - - - - - - - - - - - diff --git a/public/img/brand/discord.svg b/public/img/brand/discord.svg deleted file mode 100644 index b636d15..0000000 --- a/public/img/brand/discord.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/img/brand/twitch.svg b/public/img/brand/twitch.svg deleted file mode 100644 index 3120fea..0000000 --- a/public/img/brand/twitch.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - -Asset 2 - - - - - - - - - - - diff --git a/public/img/brand/youtube.svg b/public/img/brand/youtube.svg deleted file mode 100644 index 3286071..0000000 --- a/public/img/brand/youtube.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/public/img/buttons/thermia.gif b/public/img/buttons/thermia.gif deleted file mode 100644 index a7ee70a..0000000 Binary files a/public/img/buttons/thermia.gif and /dev/null differ diff --git a/public/script/index.js b/public/script/index.js index 2197bd4..512ed8f 100644 --- a/public/script/index.js +++ b/public/script/index.js @@ -1,5 +1,3 @@ -import { hijackClickEvent } from "./main.js"; - const hexPrimary = document.getElementById("hex-primary"); const hexSecondary = document.getElementById("hex-secondary"); const hexTertiary = document.getElementById("hex-tertiary"); @@ -16,8 +14,3 @@ updateHexColours(); window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { updateHexColours(); }); - -document.querySelectorAll("ul#projects li.project-item").forEach(projectItem => { - const link = projectItem.querySelector('a'); - hijackClickEvent(projectItem, link); -}); diff --git a/public/script/main.js b/public/script/main.js index 19eddc2..c1d5101 100644 --- a/public/script/main.js +++ b/public/script/main.js @@ -45,23 +45,6 @@ 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.querySelectorAll(".typeout")] .filter((e) => e.innerText != "") diff --git a/public/script/music.js b/public/script/music.js index 91f34ab..273ce2b 100644 --- a/public/script/music.js +++ b/public/script/music.js @@ -1,6 +1,12 @@ -import { hijackClickEvent } from "./main.js"; +import "./main.js"; document.querySelectorAll("div.music").forEach(container => { - const link = container.querySelector(".music-title a") - hijackClickEvent(container, link); + const link = container.querySelector(".music-title a").href + + container.addEventListener("click", event => { + if (event.target.href) return; + + event.preventDefault(); + location = link; + }); }); diff --git a/public/style/colours.css b/public/style/colours.css index 898e5b9..1407aa8 100644 --- a/public/style/colours.css +++ b/public/style/colours.css @@ -7,7 +7,6 @@ --secondary: #f8e05b; --tertiary: #f788fe; --links: #5eb2ff; - --live: #fd3737; } @media (prefers-color-scheme: light) { diff --git a/public/style/header.css b/public/style/header.css index f399d4d..48971b5 100644 --- a/public/style/header.css +++ b/public/style/header.css @@ -25,6 +25,7 @@ nav { flex-grow: 1; display: flex; gap: .5em; + cursor: pointer; } img#header-icon { @@ -35,19 +36,19 @@ img#header-icon { } #header-text { + width: 11em; display: flex; flex-direction: column; justify-content: center; + flex-grow: 1; } #header-text h1 { - width: fit-content; margin: 0; font-size: 1em; } #header-text h2 { - width: fit-content; height: 1.2em; line-height: 1.2em; margin: 0; @@ -153,7 +154,7 @@ header ul li a:hover { flex-direction: column; gap: 1rem; border-bottom: 1px solid #888; - background: var(--background); + background: #080808; display: none; } diff --git a/public/style/index.css b/public/style/index.css index 6efe241..f3cb761 100644 --- a/public/style/index.css +++ b/public/style/index.css @@ -96,36 +96,30 @@ hr { overflow: visible; } -ul.platform-links { - padding-left: 1em; +ul.links { display: flex; - gap: .5em; + gap: 1em .5em; flex-wrap: wrap; } -ul.platform-links li { +ul.links li { list-style: none; } -ul.platform-links li a { +ul.links li a { padding: .4em .5em; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - gap: .5em; border: 1px solid var(--links); color: var(--links); border-radius: 2px; background-color: transparent; - transition-property: color, border-color, background-color, box-shadow; + transition-property: color, border-color, background-color; transition-duration: .2s; animation-delay: 0s; animation: list-item-fadein .2s forwards; opacity: 0; } -ul.platform-links li a:hover { +ul.links li a:hover { color: #eee; border-color: #eee; background-color: var(--links) !important; @@ -133,75 +127,6 @@ ul.platform-links li a:hover { 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 { margin: 2rem 0; } @@ -223,112 +148,3 @@ div#web-buttons { 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; -} diff --git a/public/style/main.css b/public/style/main.css index 680ee2b..4e9e113 100644 --- a/public/style/main.css +++ b/public/style/main.css @@ -3,7 +3,6 @@ @import url("/style/footer.css"); @import url("/style/prideflag.css"); @import url("/style/cursor.css"); -@import url("/font/inter/inter.css"); @font-face { font-family: "Monaspace Argon"; diff --git a/view/footer.html b/view/footer.html index eccf125..217c4b5 100644 --- a/view/footer.html +++ b/view/footer.html @@ -2,12 +2,7 @@ diff --git a/view/header.html b/view/header.html index 03a8384..291b8ac 100644 --- a/view/header.html +++ b/view/header.html @@ -23,6 +23,9 @@
  • music
  • +
  • + source +
  • blog diff --git a/view/index.go b/view/index.go deleted file mode 100644 index b6e3891..0000000 --- a/view/index.go +++ /dev/null @@ -1,45 +0,0 @@ -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) - }) -} diff --git a/view/index.html b/view/index.html index 0e09475..ba0ca15 100644 --- a/view/index.html +++ b/view/index.html @@ -6,8 +6,8 @@ - - + + @@ -17,28 +17,11 @@ - + {{end}} {{define "content"}}
    - {{if .TwitchStatus}} -
    -
    -
    - livestream thumbnail - join in! -
    -
    -

    ari melody LIVE

    -

    streaming: {{.TwitchStatus.GameName}}

    -

    {{.TwitchStatus.Title}}

    -

    {{.TwitchStatus.ViewerCount}} viewers

    -
    -
    -
    - {{end}} -

    # hello, world!

    @@ -50,7 +33,7 @@

    - i'm a musician, developer, + i'm a musician, developer, streamer, youtuber, and probably a bunch of other things i forgot to mention!

    @@ -61,7 +44,7 @@

    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 - bandcamp + bandcamp so you can at least get something for your money. thank you very much either way!! 💕

    @@ -101,97 +84,57 @@

    where to find me 🛰️

    -