init
This commit is contained in:
commit
38a5c47dab
11 changed files with 550 additions and 0 deletions
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Ignore everything
|
||||||
|
*
|
||||||
|
|
||||||
|
# Whitelist gitignore itself
|
||||||
|
!.gitignore
|
||||||
|
|
||||||
|
# Whitelist Go files
|
||||||
|
!*.go
|
||||||
|
!go.mod
|
||||||
|
!go.sum
|
||||||
|
|
||||||
|
# Whitelist templates
|
||||||
|
!templates/
|
||||||
|
!templates/*.html
|
||||||
|
|
||||||
|
# Whitelist assets
|
||||||
|
!assets/
|
||||||
|
!assets/*.js
|
||||||
|
!assets/*.css
|
||||||
|
|
||||||
|
# Whitelist nix
|
||||||
|
!flake.nix
|
||||||
|
!flake.lock
|
||||||
|
|
||||||
|
# Whitelist database
|
||||||
|
!*.sqlite
|
||||||
5
assets/alpine.min.js
vendored
Normal file
5
assets/alpine.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/htmx.min.js
vendored
Normal file
1
assets/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
db.sqlite
Executable file
BIN
db.sqlite
Executable file
Binary file not shown.
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1766651565,
|
||||||
|
"narHash": "sha256-QEhk0eXgyIqTpJ/ehZKg9IKS7EtlWxF3N7DXy42zPfU=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "3e2499d5539c16d0d173ba53552a4ff8547f4539",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
34
flake.nix
Normal file
34
flake.nix
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
description = "Henna - Go web server with SQLite";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages.default = pkgs.buildGoModule {
|
||||||
|
pname = "henna";
|
||||||
|
version = "0.1.0";
|
||||||
|
src = ./.;
|
||||||
|
vendorHash = null; # Update after running go mod tidy
|
||||||
|
|
||||||
|
buildInputs = [ pkgs.sqlite ];
|
||||||
|
|
||||||
|
CGO_ENABLED = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
go
|
||||||
|
sqlite
|
||||||
|
gcc
|
||||||
|
];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
5
go.mod
Normal file
5
go.mod
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
module henna
|
||||||
|
|
||||||
|
go 1.23
|
||||||
|
|
||||||
|
require github.com/mattn/go-sqlite3 v1.14.24
|
||||||
2
go.sum
Normal file
2
go.sum
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
296
main.go
Normal file
296
main.go
Normal file
|
|
@ -0,0 +1,296 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"embed"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed templates/*
|
||||||
|
var templatesFS embed.FS
|
||||||
|
|
||||||
|
//go:embed assets/*
|
||||||
|
var assetsFS embed.FS
|
||||||
|
|
||||||
|
type CoverData struct {
|
||||||
|
Comic int
|
||||||
|
Path string
|
||||||
|
Prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
db *sql.DB
|
||||||
|
tmpl *template.Template
|
||||||
|
imaginaryURL string
|
||||||
|
galleryPath string
|
||||||
|
databasePath string
|
||||||
|
randomCovers []CoverData
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.StringVar(&imaginaryURL, "imaginary", "http://192.168.88.54:10001", "imaginary root URL")
|
||||||
|
flag.StringVar(&galleryPath, "gallery", "/home/user/mnt/panda/galleries/", "gallery path")
|
||||||
|
flag.StringVar(&databasePath, "database", "./db.sqlite", "database path")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
db, err = sql.Open("sqlite3", "./db.sqlite")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
if err = db.Ping(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Println("SQLite connected")
|
||||||
|
|
||||||
|
tmpl, err = template.ParseFS(templatesFS, "templates/*.html")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
http.HandleFunc("/gallery/", handleGallery)
|
||||||
|
http.HandleFunc("/random/", handleRandom)
|
||||||
|
http.HandleFunc("/random", handleRandom)
|
||||||
|
http.HandleFunc("/", handleIndex)
|
||||||
|
http.Handle("/assets/", http.FileServer(http.FS(assetsFS)))
|
||||||
|
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(galleryPath))))
|
||||||
|
|
||||||
|
log.Println("Server listening on :8080")
|
||||||
|
log.Fatal(http.ListenAndServe(":10000", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRandom(w http.ResponseWriter, r *http.Request) {
|
||||||
|
path := strings.TrimPrefix(r.URL.Path, "/random")
|
||||||
|
path = strings.TrimPrefix(path, "/")
|
||||||
|
|
||||||
|
// If accessing /random without chapter, shuffle and redirect to /random/0
|
||||||
|
if path == "" {
|
||||||
|
query := `
|
||||||
|
select page.id_comic, comic.directory || '/' || page.filename path, page.prefix
|
||||||
|
from page
|
||||||
|
join comic on comic.id_comic = page.id_comic
|
||||||
|
where page.number = 0
|
||||||
|
`
|
||||||
|
rows, err := db.Query(query)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
randomCovers = nil
|
||||||
|
for rows.Next() {
|
||||||
|
var c CoverData
|
||||||
|
if err := rows.Scan(&c.Comic, &c.Path, &c.Prefix); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
randomCovers = append(randomCovers, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
rand.Shuffle(len(randomCovers), func(i, j int) {
|
||||||
|
randomCovers[i], randomCovers[j] = randomCovers[j], randomCovers[i]
|
||||||
|
})
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/random/0", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chapter, err := strconv.Atoi(path)
|
||||||
|
if err != nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const limit = 18
|
||||||
|
offset := chapter * limit
|
||||||
|
|
||||||
|
type Cover struct {
|
||||||
|
Comic int
|
||||||
|
Url string
|
||||||
|
}
|
||||||
|
|
||||||
|
end := offset + limit*2
|
||||||
|
if end > len(randomCovers) {
|
||||||
|
end = len(randomCovers)
|
||||||
|
}
|
||||||
|
start := offset
|
||||||
|
if start > len(randomCovers) {
|
||||||
|
start = len(randomCovers)
|
||||||
|
}
|
||||||
|
|
||||||
|
all := make([]Cover, 0, limit*2)
|
||||||
|
for _, c := range randomCovers[start:end] {
|
||||||
|
var coverUrl string
|
||||||
|
if c.Prefix == "imaginary" {
|
||||||
|
coverUrl = fmt.Sprintf("%s/smartcrop?width=300&height=370&file=%s", imaginaryURL, url.QueryEscape(c.Path))
|
||||||
|
} else {
|
||||||
|
coverUrl = "/static/" + c.Path
|
||||||
|
}
|
||||||
|
all = append(all, Cover{Comic: c.Comic, Url: coverUrl})
|
||||||
|
}
|
||||||
|
|
||||||
|
covers := all
|
||||||
|
var preload []Cover
|
||||||
|
if len(all) > limit {
|
||||||
|
covers = all[:limit]
|
||||||
|
preload = all[limit:]
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextChapter int
|
||||||
|
if len(preload) > 0 {
|
||||||
|
nextChapter = chapter + 1
|
||||||
|
} else {
|
||||||
|
nextChapter = chapter
|
||||||
|
}
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
Covers []Cover
|
||||||
|
Preload []Cover
|
||||||
|
Chapter int
|
||||||
|
Prefix string
|
||||||
|
}{covers, preload, nextChapter, "/random"}
|
||||||
|
|
||||||
|
tmpl.ExecuteTemplate(w, "index.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleGallery(w http.ResponseWriter, r *http.Request) {
|
||||||
|
comic := strings.TrimPrefix(r.URL.Path, "/gallery/")
|
||||||
|
if comic == "" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `
|
||||||
|
select comic.title, comic.directory || '/' || page.filename, page.prefix
|
||||||
|
from page
|
||||||
|
join comic on page.id_comic = comic.id_comic
|
||||||
|
where comic.id_comic = ?
|
||||||
|
order by page.number
|
||||||
|
`
|
||||||
|
rows, err := db.Query(query, comic)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
type Page struct {
|
||||||
|
Path string
|
||||||
|
Prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
var title string
|
||||||
|
var pages []Page
|
||||||
|
for rows.Next() {
|
||||||
|
var path, prefix string
|
||||||
|
if err := rows.Scan(&title, &path, &prefix); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pages = append(pages, Page{Path: path, Prefix: prefix})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pages) == 0 {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
Title string
|
||||||
|
Pages []Page
|
||||||
|
ImaginaryURL string
|
||||||
|
}{title, pages, imaginaryURL}
|
||||||
|
|
||||||
|
tmpl.ExecuteTemplate(w, "gallery.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
|
chapter := 0
|
||||||
|
path := strings.TrimPrefix(r.URL.Path, "/")
|
||||||
|
if path != "" {
|
||||||
|
var err error
|
||||||
|
chapter, err = strconv.Atoi(path)
|
||||||
|
if err != nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const limit = 18
|
||||||
|
offset := chapter * limit
|
||||||
|
|
||||||
|
query := `
|
||||||
|
select page.id_comic, comic.directory || '/' || page.filename path, page.prefix
|
||||||
|
from page
|
||||||
|
join comic on comic.id_comic = page.id_comic
|
||||||
|
where page.number = 0
|
||||||
|
order by comic.id_comic desc
|
||||||
|
limit ? offset ?
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := db.Query(query, limit*2, offset)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
type Cover struct {
|
||||||
|
Comic int
|
||||||
|
Url string
|
||||||
|
}
|
||||||
|
|
||||||
|
all := make([]Cover, 0, limit*2)
|
||||||
|
for rows.Next() {
|
||||||
|
var comic int
|
||||||
|
var path, prefix string
|
||||||
|
if err := rows.Scan(&comic, &path, &prefix); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var coverUrl string
|
||||||
|
if prefix == "imaginary" {
|
||||||
|
coverUrl = fmt.Sprintf("%s/smartcrop?width=300&height=370&file=%s", imaginaryURL, url.QueryEscape(path))
|
||||||
|
} else {
|
||||||
|
coverUrl = "/static/" + path
|
||||||
|
}
|
||||||
|
all = append(all, Cover{Comic: comic, Url: coverUrl})
|
||||||
|
}
|
||||||
|
|
||||||
|
covers := all
|
||||||
|
var preload []Cover
|
||||||
|
if len(all) > limit {
|
||||||
|
covers = all[:limit]
|
||||||
|
preload = all[limit:]
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextChapter int
|
||||||
|
if len(preload) > 0 {
|
||||||
|
nextChapter = chapter + 1
|
||||||
|
} else {
|
||||||
|
nextChapter = chapter
|
||||||
|
}
|
||||||
|
data := struct {
|
||||||
|
Covers []Cover
|
||||||
|
Preload []Cover
|
||||||
|
Chapter int
|
||||||
|
Prefix string
|
||||||
|
}{covers, preload, nextChapter, ""}
|
||||||
|
|
||||||
|
tmpl.ExecuteTemplate(w, "index.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
79
templates/gallery.html
Normal file
79
templates/gallery.html
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<script src="/assets/alpine.min.js" defer></script>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { background: black; height: 100vh; overflow: hidden; }
|
||||||
|
.page {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
.page.landscape {
|
||||||
|
width: 100vh;
|
||||||
|
height: 100vw;
|
||||||
|
transform: rotate(270deg) translateY(-50%);
|
||||||
|
transform-origin: top right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div
|
||||||
|
x-data="{
|
||||||
|
current: 0,
|
||||||
|
landscape: false,
|
||||||
|
width: 1080,
|
||||||
|
height: 1920,
|
||||||
|
imaginary: '{{.ImaginaryURL}}',
|
||||||
|
pages: [{{range $i, $p := .Pages}}{{if $i}},{{end}}{path: '{{$p.Path}}', prefix: '{{$p.Prefix}}'}{{end}}],
|
||||||
|
urls: [],
|
||||||
|
prefetched: new Set(),
|
||||||
|
buildUrls() {
|
||||||
|
this.urls = this.pages.map(p => {
|
||||||
|
if (p.prefix === 'imaginary') {
|
||||||
|
return this.imaginary + '/fit?width=' + this.width + '&height=' + this.height + '&file=' + encodeURIComponent(p.path);
|
||||||
|
}
|
||||||
|
return '/static/' + p.path;
|
||||||
|
});
|
||||||
|
this.prefetched.clear();
|
||||||
|
},
|
||||||
|
prefetch() {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
for (let i = 1; i <= 10 && this.current + i < this.urls.length; i++) {
|
||||||
|
const url = this.urls[this.current + i];
|
||||||
|
if (!this.prefetched.has(url)) {
|
||||||
|
this.prefetched.add(url);
|
||||||
|
new Image().src = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
img.src = this.urls[this.current];
|
||||||
|
},
|
||||||
|
swap() {
|
||||||
|
this.landscape = !this.landscape;
|
||||||
|
[this.width, this.height] = [this.height, this.width];
|
||||||
|
this.buildUrls();
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.buildUrls();
|
||||||
|
this.prefetch();
|
||||||
|
this.$watch('current', () => this.prefetch());
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
@keydown.window.left.prevent="current = Math.max(0, current - 1)"
|
||||||
|
@keydown.window.right.prevent="current = Math.min(urls.length - 1, current + 1)"
|
||||||
|
@keydown.window.k.prevent="swap()"
|
||||||
|
@keydown.window.backspace.prevent="history.back()"
|
||||||
|
autofocus
|
||||||
|
>
|
||||||
|
<div class="page" :class="landscape ? 'landscape' : ''" :style="'background-image: url(\'' + urls[current] + '\');'"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
41
templates/index.html
Normal file
41
templates/index.html
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Henna</title>
|
||||||
|
<script src="/assets/htmx.min.js" defer></script>
|
||||||
|
<script src="/assets/alpine.min.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; background-color: black; overflow: hidden;">
|
||||||
|
<div id="app"
|
||||||
|
x-data="{active: 0, total: {{len .Covers}}, row: 6, chapter: {{.Chapter}}, prefix: '{{.Prefix}}', comics: [{{range $i, $c := .Covers}}{{if $i}},{{end}}{{$c.Comic}}{{end}}]}"
|
||||||
|
@keydown.window.left.prevent="active = Math.max(0, active - 1)"
|
||||||
|
@keydown.window.right.prevent="active = Math.min(total - 1, active + 1)"
|
||||||
|
@keydown.window.up.prevent="active = Math.max(0, active - row)"
|
||||||
|
@keydown.window.down.prevent="active = Math.min(total - 1, active + row)"
|
||||||
|
@keydown.window.i.prevent="htmx.ajax('GET', prefix + '/' + chapter, {target: '#app', swap: 'outerHTML'})"
|
||||||
|
@keydown.window.n.prevent="htmx.ajax('GET', prefix + '/' + Math.max(0, chapter - 2), {target: '#app', swap: 'outerHTML'})"
|
||||||
|
@keydown.window.enter.prevent="window.location.href = '/gallery/' + comics[active]"
|
||||||
|
@wheel.window.prevent="$event.deltaY > 0 ? htmx.ajax('GET', prefix + '/' + chapter, {target: '#app', swap: 'outerHTML'}) : htmx.ajax('GET', prefix + '/' + Math.max(0, chapter - 2), {target: '#app', swap: 'outerHTML'})"
|
||||||
|
autofocus
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{{range $i, $c := .Covers}}
|
||||||
|
<div class="cover" :class="{'active': active === {{$i}}}">
|
||||||
|
<img src="{{$c.Url}}" width="300" height="370">
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
.cover { display: inline-block; margin: 2px; outline: 2px solid transparent; }
|
||||||
|
.cover.active { outline: 2px solid #0af; }
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
setTimeout(() => {
|
||||||
|
[{{range $i, $c := .Preload}}{{if $i}},{{end}}'{{$c.Url}}'{{end}}].forEach(url => new Image().src = url);
|
||||||
|
}, 200);
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue