diff --git a/docs/PREFERENCES.md b/docs/PREFERENCES.md index 5792c34..1164e40 100644 --- a/docs/PREFERENCES.md +++ b/docs/PREFERENCES.md @@ -1,22 +1,16 @@ # Preferences - | Name | Key | Default | Possible values | Description | | :--------------------------------- | --------------------- | ------------------------------------------------------------------------ | ------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Parse descriptions | ParseDescriptions | true | true, false | Turn @mentions, external links (https://example.org) and emails (hello@example.org) inside descriptions into clickable links | -| Show current audio | ShowAudio | false | true, false | Show what [audio preset](AUDIO_PRESETS.md) is being streamed below the audio player | -| Proxy images | ProxyImages | same as ProxyImages in backend config | true, false | Proxy images through the backend. ProxyImages must be enabled on the backend | | Download audio | DownloadAudio | "mpeg" | "mpeg", "opus", "aac", "best" | What [audio preset](AUDIO_PRESETS.md) should be loaded when downloading audio with metadata. Restream must be enabled on the backend | -| Autoplay next track in playlists | AutoplayNextTrack | false | true, false | Automatically start playlist playback when you open a track from the playlist. Requires JS | -| Default autoplay mode (in playlists) | DefaultAutoplayMode | "normal" | "normal", "random" | Default mode for playlist autoplay. Normal - play songs in order. Random - play random song next | -| Autoplay next related track | AutoplayNextRelatedTrack | false | true, false | Automatically play a related track next. Requires JS -| Fetch search suggestions | SearchSuggestions | false | true, false | Load search suggestions on main page when you type. Requires JS | -| Dynamically load comments | DynamicLoadComments | false | true, false | Dynamically load track comments, without leaving the page. Requires JS | + +# Player preferences + +| Name | Key | Default | Possible values | Description | +| :--------------------------------- | --------------------- | ------------------------------------------------------------------------ | ------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Player | Player | "restream" if Restream is enabled in backend config, otherwise - "hls" | "restream", "hls", "none" | Method used to play the track in the frontend. HLS - requires JavaScript, loads the track in pieces. Restream - works without JavaScript, loads entire track through the backend right away. None - don't play the track | -## Player-specific preferences - -### HLS Player +## HLS Player | Name | Key | Default | Possible values | Description | | :-------------------- | ------------------- | ---------------------------------------- | ----------------- | :------------------------------------------------------------------------------------ | @@ -24,9 +18,30 @@ | Fully preload track | FullyPreloadTrack | false | true, false | Fully load track when the page is loaded (track stream expires in ~5 minutes) | | Streaming audio | HLSAudio | "mpeg" | "mpeg", "aac" | What [audio preset](AUDIO_PRESETS.md) should be loaded when streaming audio | -### Restream Player +## Restream Player | Name | Key | Default | Possible values | Description | | :---------------- | --------------- | --------- | ------------------------------- | :--------------------------------------------------------------------------- | | Streaming audio | RestreamAudio | "mpeg" | "mpeg", "opus", "aac", "best" | What [audio preset](AUDIO_PRESETS.md) should be loaded when streaming audio | + + +# Frontend enhancements + +| Name | Key | Default | Possible values | Description | +| :--------------------------------- | --------------------- | ------------------------------------------------------------------------ | ------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Proxy images | ProxyImages | same as ProxyImages in backend config | true, false | Proxy images through the backend. ProxyImages must be enabled on the backend | +| Parse descriptions | ParseDescriptions | true | true, false | Turn @mentions, external links (https://example.org) and emails (hello@example.org) inside descriptions into clickable links | +| Show current audio | ShowAudio | false | true, false | Show what [audio preset](AUDIO_PRESETS.md) is being streamed below the audio player | +| Fetch search suggestions | SearchSuggestions | false | true, false | Load search suggestions on main page when you type. Requires JS | +| Dynamically load comments | DynamicLoadComments | false | true, false | Dynamically load track comments, without leaving the page. Requires JS +| Keep player focus | KeepPlayerFocus | false | true, false | Always keep track element in focus, so you can control it with keyboard. Requires JS | + +## Autoplay +*Requires JS. You also need to allow autoplay from this domain* + +| Name | Key | Default | Possible values | Description | +| :--------------------------------- | --------------------- | ------------------------------------------------------------------------ | ------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Autoplay next track in playlists | AutoplayNextTrack | false | true, false | Automatically start playlist playback when you open a track from the playlist. | +| Default autoplay mode (in playlists) | DefaultAutoplayMode | "normal" | "normal", "random" | Default mode for playlist autoplay. Normal - play songs in order. Random - play random song next | +| Autoplay next related track | AutoplayNextRelatedTrack | false | true, false | Automatically play a related track next. \ No newline at end of file diff --git a/lib/cfg/init.go b/lib/cfg/init.go index 6eb7301..913abfc 100644 --- a/lib/cfg/init.go +++ b/lib/cfg/init.go @@ -1,7 +1,6 @@ package cfg import ( - "fmt" "log" "os" "strconv" @@ -144,6 +143,7 @@ func defaultPreferences() { DefaultPreferences.SearchSuggestions = &False DefaultPreferences.DynamicLoadComments = &False + DefaultPreferences.KeepPlayerFocus = &False } func loadDefaultPreferences(loaded Preferences) { @@ -238,21 +238,18 @@ func loadDefaultPreferences(loaded Preferences) { } else { DefaultPreferences.DynamicLoadComments = &False } + + if loaded.KeepPlayerFocus != nil { + DefaultPreferences.KeepPlayerFocus = loaded.KeepPlayerFocus + } else { + DefaultPreferences.KeepPlayerFocus = &False + } } func boolean(in string) bool { return strings.Trim(strings.ToLower(in), " ") == "true" } -type wrappedError struct { - err error - fault string -} - -func (w wrappedError) Error() string { - return fmt.Sprintf("error loading %s: %s", w.fault, w.err) -} - func fromEnv() error { env := os.Getenv("GET_WEB_PROFILES") if env != "" { @@ -264,7 +261,7 @@ func fromEnv() error { var p Preferences err := json.Unmarshal(S2b(env), &p) if err != nil { - return wrappedError{err, "DEFAULT_PREFERENCES"} + return err } loadDefaultPreferences(p) @@ -306,7 +303,7 @@ func fromEnv() error { if env != "" { num, err := strconv.ParseInt(env, 10, 64) if err != nil { - return wrappedError{err, "CLIENT_ID_TTL"} + return err } ClientIDTTL = time.Duration(num) * time.Second @@ -316,7 +313,7 @@ func fromEnv() error { if env != "" { num, err := strconv.ParseInt(env, 10, 64) if err != nil { - return wrappedError{err, "USER_TTL"} + return err } UserTTL = time.Duration(num) * time.Second @@ -326,7 +323,7 @@ func fromEnv() error { if env != "" { num, err := strconv.ParseInt(env, 10, 64) if err != nil { - return wrappedError{err, "USER_CACHE_CLEAN_DELAY"} + return err } UserCacheCleanDelay = time.Duration(num) * time.Second @@ -336,7 +333,7 @@ func fromEnv() error { if env != "" { num, err := strconv.ParseInt(env, 10, 64) if err != nil { - return wrappedError{err, "TRACK_TTL"} + return err } TrackTTL = time.Duration(num) * time.Second @@ -346,7 +343,7 @@ func fromEnv() error { if env != "" { num, err := strconv.ParseInt(env, 10, 64) if err != nil { - return wrappedError{err, "TRACK_CACHE_CLEAN_DELAY"} + return err } TrackCacheCleanDelay = time.Duration(num) * time.Second @@ -356,7 +353,7 @@ func fromEnv() error { if env != "" { num, err := strconv.ParseInt(env, 10, 64) if err != nil { - return wrappedError{err, "PLAYLIST_TTL"} + return err } PlaylistTTL = time.Duration(num) * time.Second @@ -366,7 +363,7 @@ func fromEnv() error { if env != "" { num, err := strconv.ParseInt(env, 10, 64) if err != nil { - return wrappedError{err, "PLAYLIST_CACHE_CLEAN_DELAY"} + return err } PlaylistCacheCleanDelay = time.Duration(num) * time.Second @@ -381,7 +378,7 @@ func fromEnv() error { if env != "" { num, err := strconv.ParseInt(env, 10, 64) if err != nil { - return wrappedError{err, "DNS_CACHE_TTL"} + return err } DNSCacheTTL = time.Duration(num) * time.Second @@ -412,7 +409,7 @@ func fromEnv() error { var p []string err := json.Unmarshal(S2b(env), &p) if err != nil { - return wrappedError{err, "TRUSTED_PROXIES"} + return err } TrustedProxies = p @@ -436,12 +433,8 @@ func init() { if env := os.Getenv("SOUNDCLOAK_CONFIG"); env == "FROM_ENV" { err := fromEnv() if err != nil { - // So we only set default preferences if it fails to load that in - if err.(wrappedError).fault == "DEFAULT_PREFERENCES" { - defaultPreferences() - } - - log.Println("failed to load config from environment:", err) + log.Println("Warning: failed to load config from environment:", err) + defaultPreferences() } return @@ -451,7 +444,7 @@ func init() { data, err := os.ReadFile(filename) if err != nil { - log.Printf("failed to load config from %s: %s\n", filename, err) + log.Printf("Warning: failed to load config from %s: %s\n", filename, err) defaultPreferences() return } diff --git a/lib/cfg/misc.go b/lib/cfg/misc.go index e7e1590..21986ea 100644 --- a/lib/cfg/misc.go +++ b/lib/cfg/misc.go @@ -80,6 +80,8 @@ type Preferences struct { SearchSuggestions *bool // load search suggestions on main page DynamicLoadComments *bool // dynamic comments loader without leaving track page + + KeepPlayerFocus *bool // keep player element in focus } func B2s(b []byte) string { diff --git a/lib/preferences/init.go b/lib/preferences/init.go index cf9909e..1aa2cb4 100644 --- a/lib/preferences/init.go +++ b/lib/preferences/init.go @@ -11,6 +11,8 @@ import ( "github.com/gofiber/fiber/v3" ) +const on = "on" + func Defaults(dst *cfg.Preferences) { if dst.Player == nil { dst.Player = cfg.DefaultPreferences.Player @@ -67,6 +69,10 @@ func Defaults(dst *cfg.Preferences) { if dst.DynamicLoadComments == nil { dst.DynamicLoadComments = cfg.DefaultPreferences.DynamicLoadComments } + + if dst.KeepPlayerFocus == nil { + dst.KeepPlayerFocus = cfg.DefaultPreferences.KeepPlayerFocus + } } func Get(c fiber.Ctx) (cfg.Preferences, error) { @@ -97,6 +103,7 @@ type PrefsForm struct { ShowAudio string SearchSuggestions string DynamicLoadComments string + KeepPlayerFocus string } type Export struct { @@ -130,19 +137,19 @@ func Load(r *fiber.App) { old.DefaultAutoplayMode = &p.DefaultAutoplayMode } - if p.AutoplayNextTrack == "on" { + if p.AutoplayNextTrack == on { old.AutoplayNextTrack = &cfg.True } else { old.AutoplayNextTrack = &cfg.False } - if p.AutoplayNextRelatedTrack == "on" { + if p.AutoplayNextRelatedTrack == on { old.AutoplayNextRelatedTrack = &cfg.True } else { old.AutoplayNextRelatedTrack = &cfg.False } - if p.ShowAudio == "on" { + if p.ShowAudio == on { old.ShowAudio = &cfg.True } else { old.ShowAudio = &cfg.False @@ -150,14 +157,14 @@ func Load(r *fiber.App) { if *old.Player == cfg.HLSPlayer { if cfg.ProxyStreams { - if p.ProxyStreams == "on" { + if p.ProxyStreams == on { old.ProxyStreams = &cfg.True } else if p.ProxyStreams == "" { old.ProxyStreams = &cfg.False } } - if p.FullyPreloadTrack == "on" { + if p.FullyPreloadTrack == on { old.FullyPreloadTrack = &cfg.True } else if p.FullyPreloadTrack == "" { old.FullyPreloadTrack = &cfg.False @@ -175,31 +182,37 @@ func Load(r *fiber.App) { } if cfg.ProxyImages { - if p.ProxyImages == "on" { + if p.ProxyImages == on { old.ProxyImages = &cfg.True } else if p.ProxyImages == "" { old.ProxyImages = &cfg.False } } - if p.ParseDescriptions == "on" { + if p.ParseDescriptions == on { old.ParseDescriptions = &cfg.True } else { old.ParseDescriptions = &cfg.False } - if p.SearchSuggestions == "on" { + if p.SearchSuggestions == on { old.SearchSuggestions = &cfg.True } else { old.SearchSuggestions = &cfg.False } - if p.DynamicLoadComments == "on" { + if p.DynamicLoadComments == on { old.DynamicLoadComments = &cfg.True } else { old.DynamicLoadComments = &cfg.False } + if p.KeepPlayerFocus == on { + old.KeepPlayerFocus = &cfg.True + } else { + old.KeepPlayerFocus = &cfg.False + } + old.Player = &p.Player data, err := json.Marshal(old) diff --git a/static/assets/keepfocus.js b/static/assets/keepfocus.js new file mode 100644 index 0000000..42c572b --- /dev/null +++ b/static/assets/keepfocus.js @@ -0,0 +1,9 @@ +var audio = document.getElementById('track'); +audio.onblur = function (e) { + if (e.target != e.relatedTarget) { + setTimeout(function() { + e.target.focus({preventScroll: true, focusVisible: false}); + }) + } +} +audio.focus({focusVisible: false}); \ No newline at end of file diff --git a/templates/preferences.templ b/templates/preferences.templ index 75f323e..a14b02d 100644 --- a/templates/preferences.templ +++ b/templates/preferences.templ @@ -42,55 +42,13 @@ templ sel_audio(name string, selected string, noOpus bool) { templ Preferences(prefs cfg.Preferences) {

Preferences

- - - if cfg.ProxyImages { - - } if cfg.Restream { } - - if *prefs.AutoplayNextTrack { - - } - - - +

Player preferences

switch *prefs.Player { case cfg.HLSPlayer: -

Player-specific preferences

if cfg.ProxyStreams { case cfg.RestreamPlayer: -

Player-specific preferences

} +

Frontend enhancements

+ if cfg.ProxyImages { + + } + + + + + +

Autoplay

+ Requires JS. You also need to allow autoplay from this domain + + if *prefs.AutoplayNextTrack { + + } +

These preferences get saved in a cookie.

@@ -137,6 +142,5 @@ templ Preferences(prefs cfg.Preferences) { - } diff --git a/templates/track.templ b/templates/track.templ index fffe740..507ab8c 100644 --- a/templates/track.templ +++ b/templates/track.templ @@ -47,38 +47,36 @@ templ TrackHeader(prefs cfg.Preferences, t sc.Track, needPlayer bool) { - if needPlayer && *prefs.Player == cfg.HLSPlayer { - + if needPlayer { + if *prefs.Player == cfg.HLSPlayer { + + } } } -func next(c *sc.Track, t *sc.Track, p *sc.Playlist, autoplay bool, mode string, volume string) string { +func next(c *sc.Track, t *sc.Track, p *sc.Playlist, mode string, volume string) string { r := t.Href() - if p != nil { + if p != nil { r += "?playlist=" + p.Href()[1:] - } - - if autoplay { - if p == nil { - r += "?" - } else { - r += "&" + if mode != "" { + r += "&mode=" + mode + } + r += "&" + } else { + r += "?" + + if c != nil { + r += "prev=" + string(c.ID) + "&" } - r += "autoplay=true" } - if mode != "" { - r += "&mode=" + mode - } - + r += "autoplay=true" + if volume != "" { r += "&volume=" + volume } - if p == nil { - r += "&prev=" + string(c.ID) - } return r } @@ -87,19 +85,22 @@ templ TrackPlayer(prefs cfg.Preferences, track sc.Track, stream string, displayE {{ return }} } if displayErr == "" { - {{ var audioPref string }} + {{ var audioPref *string }} if cfg.Restream && *prefs.Player == cfg.RestreamPlayer { - {{ audioPref = *prefs.RestreamAudio }} + {{ audioPref = prefs.RestreamAudio }} if nextTrack != nil { - + } else { - + + } + if *prefs.KeepPlayerFocus { + } } else if stream != "" { - {{ audioPref = *prefs.HLSAudio }} + {{ audioPref = prefs.HLSAudio }} if nextTrack != nil { - + } else { } @@ -108,6 +109,9 @@ templ TrackPlayer(prefs cfg.Preferences, track sc.Track, stream string, displayE } else { } + if *prefs.KeepPlayerFocus { + + }