feat(web): offer the userscripts as a download for mobile

Violentmonkey on mobile Chromium does not intercept navigation to a
.user.js URL, so the Install link renders the script as text and there is
no way to get it installed. Adding ?download=1 sets Content-Disposition:
attachment on the same session-gated endpoint, so the Reader saves the
file and adds it from Violentmonkey's own menu.

The plain link stays inline on purpose: the updater polls the /u/ path and
an attachment disposition there would break auto-update. The test asserts
both halves.

Refs #26
This commit is contained in:
2026-08-08 16:36:45 +07:00
parent 40378192b1
commit 0830016729
4 changed files with 40 additions and 1 deletions
+23
View File
@@ -201,6 +201,27 @@ func TestInstallServesScriptWithCredential(t *testing.T) {
if loc := rr.Header().Get("Location"); loc != "" {
t.Fatalf("%s answered with a redirect, credential in Location %q", script, loc)
}
// The plain link must stay inline: Violentmonkey's updater polls the
// /u/ path and an attachment disposition there would break updates.
if cd := rr.Header().Get("Content-Disposition"); cd != "" {
t.Fatalf("%s served as %q, want inline", script, cd)
}
// ?download=1 is the mobile path: Violentmonkey on Chromium ignores a
// .user.js navigation, so the Reader saves the file and adds it by hand.
req = httptest.NewRequest(http.MethodGet, "/install/"+script+"?download=1", nil)
req.AddCookie(sessionCookie(t, st))
rr = httptest.NewRecorder()
srv.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("%s?download=1: status = %d, want 200", script, rr.Code)
}
if got, want := rr.Header().Get("Content-Disposition"), `attachment; filename="`+script+`"`; got != want {
t.Fatalf("%s?download=1: Content-Disposition = %q, want %q", script, got, want)
}
if !strings.Contains(rr.Body.String(), `API_TOKEN = "`+ownerCredential()+`"`) {
t.Fatalf("%s?download=1 does not carry the owner's credential", script)
}
}
}
@@ -327,6 +348,8 @@ func TestIndexShowsSetupPanelWithoutCredential(t *testing.T) {
for _, want := range []string{
`href="/install/manga-bookmark.user.js"`,
`href="/install/novel-bookmark.user.js"`,
`href="/install/manga-bookmark.user.js?download=1"`,
`href="/install/novel-bookmark.user.js?download=1"`,
"Rotate credential",
} {
if !strings.Contains(body, want) {