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
@@ -12,6 +12,13 @@
<a class="ghost" href="/install/manga-bookmark.user.js">Install Manga script</a>
<a class="ghost" href="/install/novel-bookmark.user.js">Install Novels script</a>
</p>
<p class="setup-copy">On mobile, Violentmonkey does not pick up the install
links — the script opens as text. Download the file instead, then add it
from Violentmonkey's own menu.</p>
<p class="setup-links">
<a class="ghost" href="/install/manga-bookmark.user.js?download=1">Download Manga script</a>
<a class="ghost" href="/install/novel-bookmark.user.js?download=1">Download Novels script</a>
</p>
{{if .Rotated}}
<p class="setup-warn" role="status">Credential rotated — the old one no
longer works. Reinstall both scripts on every device now, or they will
+7
View File
@@ -547,6 +547,10 @@ func (h *Handler) uiDelete(w http.ResponseWriter, r *http.Request) {
// derived credential substituted in. The credential is derived, not stored,
// so installs work after any restart; the Reader never types or copies it —
// clicking Install is the whole setup.
//
// ?download=1 forces a save instead. Mobile Violentmonkey (Chromium) does not
// intercept navigation to a .user.js URL, so the Install link only renders the
// source as text there; the Reader needs the file on disk to add it by hand.
func (h *Handler) installUserscript(name string) http.HandlerFunc {
path := h.mangaUserscriptPath
if name == "novel-bookmark.user.js" {
@@ -559,6 +563,9 @@ func (h *Handler) installUserscript(name string) http.HandlerFunc {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
if r.URL.Query().Has("download") {
w.Header().Set("Content-Disposition", `attachment; filename="`+name+`"`)
}
userscript.Render(w, r, path, token.Token(h.tokenKey, discordID, epoch))
}
}