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

Keep the nav panel on page one, and surface hide-ai on the About page

Closes #21. NavBase bounded its page window by DeviationList.Pages alone. The
comment list on a deviation cannot count its pages and passes Pages: 0, so on
page one the loop ended before i reached 1 and emitted no page numbers; with no
previous page to link back to and no further page to link on, the panel came
out as a bare <br>. The window now runs to the last page or the current one,
whichever is further out, so the current page always renders.

Closes #22. The About page listed NSFW and Proxyfing but not hide-ai, so a
visitor could only infer it from the absence of robot markers. instanceAbout
and settingsParams both carry it now, which puts it on the About page and in
/api/instance, and addInstance records it when submitting an instance.
 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(-)

diff --git a/app/api.go b/app/api.go
index c89f10d..2c04123 100755
--- a/app/api.go
+++ b/app/api.go
@@ -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)
diff --git a/app/cli.go b/app/cli.go
index f023036..ec2b614 100755
--- a/app/cli.go
+++ b/app/cli.go
@@ -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),
diff --git a/app/config.go b/app/config.go
index b790f71..7a513f6 100755
--- a/app/config.go
+++ b/app/config.go
@@ -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
diff --git a/app/util.go b/app/util.go
index 205ed7a..804d06e 100755
--- a/app/util.go
+++ b/app/util.go
@@ -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 {
diff --git a/app/util_test.go b/app/util_test.go
new file mode 100644
index 0000000..7a70b68
--- /dev/null
+++ b/app/util_test.go
@@ -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)
+		}
+	}
+}
diff --git a/static/html/about.htm b/static/html/about.htm
index 3e8643e..8773437 100755
--- a/static/html/about.htm
+++ b/static/html/about.htm
@@ -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>