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
+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))
}
}