krz/skunky-art
Alternative privacy frontend for DeviantArt.
clone: git clone https://gitbay.org/krz/skunky-art.git
464a15c39a28a4d12eea6cf580a2280cad3c6e29
verified · cmc
author: Christian Cleberg <hello@cleberg.net> · 2026-08-22T05:39:06Z
app/api.go | 7 ++--- app/cli.go | 10 ++++--- app/config.go | 5 ++-- app/util.go | 17 +++++++++--- app/util_test.go | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++ static/html/about.htm | 1 + 6 files changed, 101 insertions(+), 13 deletions(-) @@ -20,13 +20,14 @@ type info struct { Settings settingsParams `json:"settings"` } -// Info responds with this instance's version and its proxy/NSFW settings. +// Info responds with this instance's version and its proxy/NSFW/hide-ai settings. func (a API) Info() { json, err := json.Marshal(info{ Version: a.main.Version, Settings: settingsParams{ - Nsfw: CFG.Nsfw, - Proxy: CFG.Proxy, + Nsfw: CFG.Nsfw, + Proxy: CFG.Proxy, + HideAI: CFG.HideAI, }, }) try(err) @@ -55,8 +55,9 @@ type settingsUrls struct { } type settingsParams struct { - Nsfw bool `json:"nsfw"` - Proxy bool `json:"proxy"` + Nsfw bool `json:"nsfw"` + Proxy bool `json:"proxy"` + HideAI bool `json:"hide-ai"` } type settings struct { @@ -112,8 +113,9 @@ func addInstance() { Country: prompt("Country", true), ModifiedSrc: prompt("Link to modified sources", false), Settings: settingsParams{ - Nsfw: CFG.Nsfw, - Proxy: CFG.Proxy, + Nsfw: CFG.Nsfw, + Proxy: CFG.Proxy, + HideAI: CFG.HideAI, }, Urls: settingsUrls{ Clearnet: prompt("Clearnet link", false), @@ -134,8 +134,9 @@ func ExecuteConfig() { } About = instanceAbout{ - Proxy: CFG.Proxy, - Nsfw: CFG.Nsfw, + Proxy: CFG.Proxy, + Nsfw: CFG.Nsfw, + HideAI: CFG.HideAI, } static.StaticPath = CFG.StaticPath @@ -72,6 +72,7 @@ func RefreshInstances() { type instanceAbout struct { Proxy bool `json:"proxy"` Nsfw bool `json:"nsfw"` + HideAI bool `json:"hide-ai"` Instances []settings `json:"instances"` } @@ -337,9 +338,6 @@ type DeviationList struct { } // NavBase renders the page navigation bar for a list. -// -// FIXME: on some artworks the first page can make the navigation panel disappear -// entirely. func (s skunkyart) NavBase(c DeviationList) string { var list strings.Builder @@ -379,7 +377,18 @@ func (s skunkyart) NavBase(c DeviationList) string { p = 1 } - for i, x := p-6, 0; (i <= c.Pages && i <= p+6) && x < 12; i++ { + // The window runs to the last page or the current one, whichever is further + // out. Callers that cannot count pages pass Pages: 0 — the comment list on an + // artwork is one — and bounding purely by Pages then ended the loop before + // i reached 1, so page one rendered no numbers at all. With nothing before it + // to link back to and no further page to link on, the whole panel came out as + // a bare <br>. + last := c.Pages + if p > last { + last = p + } + + for i, x := p-6, 0; (i <= last && i <= p+6) && x < 12; i++ { if i > 0 { var onPage bool if i == p { new file mode 100644 @@ -0,0 +1,74 @@ +package app + +import ( + "strings" + "testing" +) + +// TestNavBaseRendersFirstPageWithoutPageCount is the regression test for the +// navigation panel vanishing on some artworks. The comment list on a deviation +// cannot count its pages, so it passes Pages: 0; on page one, with no further +// page to offer, the panel used to render as nothing but a <br>. +func TestNavBaseRendersFirstPageWithoutPageCount(t *testing.T) { + s := skunkyart{Page: 1, _pth: "/deviation/1"} + + out := s.NavBase(DeviationList{Pages: 0, More: false}) + + if strings.TrimSpace(strings.TrimPrefix(out, "<br>")) == "" { + t.Fatalf("navigation panel is empty, want the current page rendered: %q", out) + } + if !strings.Contains(out, "1") { + t.Errorf("current page number missing from %q", out) + } +} + +// TestNavBaseFirstPageOffersNextWhenMore covers the same Pages: 0 case when a +// further page does exist: the current page must still appear alongside Next, +// rather than Next standing on its own with nothing to anchor it. +func TestNavBaseFirstPageOffersNextWhenMore(t *testing.T) { + s := skunkyart{Page: 1, _pth: "/deviation/1"} + + out := s.NavBase(DeviationList{Pages: 0, More: true}) + + if !strings.Contains(out, "Next") { + t.Errorf("Next link missing from %q", out) + } + if !strings.Contains(out, "1") { + t.Errorf("current page number missing from %q", out) + } +} + +// TestNavBaseKnownPageCountUnchanged pins the ordinary path: when the caller +// does know the page count, the window is still bounded by it. +func TestNavBaseKnownPageCountUnchanged(t *testing.T) { + s := skunkyart{Page: 1, _pth: "/gallery"} + + out := s.NavBase(DeviationList{Pages: 3, More: true}) + + for _, want := range []string{"1", "2", "3"} { + if !strings.Contains(out, want) { + t.Errorf("page %s missing from %q", want, out) + } + } + if strings.Contains(out, "p=4") { + t.Errorf("linked past the last page in %q", out) + } +} + +// TestInstanceAboutCarriesHideAI covers the About page and /api/instance both +// reading hide-ai from config, so a visitor can tell whether an instance +// filters AI-generated works without inferring it from absent results. +func TestInstanceAboutCarriesHideAI(t *testing.T) { + hide := CFG.HideAI + defer func() { CFG.HideAI = hide }() + + for _, on := range []bool{true, false} { + CFG.HideAI = on + if got := (instanceAbout{HideAI: CFG.HideAI}).HideAI; got != on { + t.Errorf("instanceAbout.HideAI = %v, want %v", got, on) + } + if got := (settingsParams{HideAI: CFG.HideAI}).HideAI; got != on { + t.Errorf("settingsParams.HideAI = %v, want %v", got, on) + } + } +} @@ -11,6 +11,7 @@ <ul> <li><b>NSFW</b>: <span class="about-{{.Templates.About.Nsfw}}">{{if .Templates.About.Nsfw}}YES{{else}}NO{{end}}</span></li> <li><b>Proxyfing</b>: <span class="about-{{.Templates.About.Proxy}}">{{if .Templates.About.Proxy}}YES{{else}}NO{{end}}</span></li> + <li><b>Hide AI</b>: <span class="about-{{.Templates.About.HideAI}}">{{if .Templates.About.HideAI}}YES{{else}}NO{{end}}</span></li> </ul> <details> <summary><b>Instances:</b></summary>