Compare commits

...

3 commits

Author SHA1 Message Date
f0efc83cf6
declaring 1.3.0
Package Release Number

better licence image

Shrunk image

Fixed size

MASCOT UPDATE

fixed CSS and Added Mascot to main site

allows Enter to Register/Login

changed jpeg cover quality from 70 to 85

Wink over form

more CSS fixes

linked wiki

corrected Readme

Reviewed-on: #8
Co-authored-by: riomoo <alister@kamikishi.net>
Co-committed-by: riomoo <alister@kamikishi.net>

removed ico from LFS

fixing ico

Reviewed-on: #9
Co-authored-by: riomoo <alister@kamikishi.net>
Co-committed-by: riomoo <alister@kamikishi.net>

fixing lfs

Reviewed-on: #10
Co-authored-by: riomoo <alister@kamikishi.net>
Co-committed-by: riomoo <alister@kamikishi.net>
2026-02-02 06:23:55 -05:00
05a1723805
feat: Environment Variables added and more
Reviewed-on: #6
2026-01-19 02:41:55 -05:00
bd8437bd1f
Environment Variables added
Include Windows Build option

containerignore updated

readme Environment Variable Additions

Added Building for windows AND linux

- using podman or docker
2026-01-18 23:41:38 -05:00
18 changed files with 456 additions and 47 deletions

View file

@ -2,11 +2,15 @@
.git .git
.gitignore .gitignore
.gitattributes .gitattributes
bash-scripts scripts-bash
scripts-go
Containerfile Containerfile
Containerfile.build
binaries
README.md README.md
xml-template xml-template
cache cache
etc etc
library library
watch watch
releases

7
.gitattributes vendored
View file

@ -1,8 +1,5 @@
*.jpg filter=lfs diff=lfs merge=lfs -text
*.mp3 filter=lfs diff=lfs merge=lfs -text *.mp3 filter=lfs diff=lfs merge=lfs -text
*.lua filter=lfs diff=lfs merge=lfs -text *.lua filter=lfs diff=lfs merge=lfs -text
*.svg filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.webp filter=lfs diff=lfs merge=lfs -text *.webp filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text *.gif filter=lfs diff=lfs merge=lfs -text
*.webm filter=lfs diff=lfs merge=lfs -text *.webm filter=lfs diff=lfs merge=lfs -text
@ -31,7 +28,3 @@
*.odt filter=lfs diff=lfs merge=lfs -text *.odt filter=lfs diff=lfs merge=lfs -text
*.docx filter=lfs diff=lfs merge=lfs -text *.docx filter=lfs diff=lfs merge=lfs -text
*.apk filter=lfs diff=lfs merge=lfs -text *.apk filter=lfs diff=lfs merge=lfs -text
*.ico filter=lfs diff=lfs merge=lfs -text
*.JXL filter=lfs diff=lfs merge=lfs -text
*.AVIF filter=lfs diff=lfs merge=lfs -text
*.PNG filter=lfs diff=lfs merge=lfs -text

2
.gitignore vendored
View file

@ -7,3 +7,5 @@ cache
library library
etc etc
watch watch
binaries
releases

View file

@ -25,7 +25,7 @@ RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build \
-a \ -a \
-ldflags="-s -w -linkmode external -extldflags '-static' -X main.GOMEMLIMIT=512MiB -X runtime.defaultGOGC=50" \ -ldflags="-s -w -linkmode external -extldflags '-static' -X main.GOMEMLIMIT=512MiB -X runtime.defaultGOGC=50" \
-trimpath \ -trimpath \
-o bin/main app/gopherbook/main.go -o bin/main ./app/gopherbook
RUN upx --best --ultra-brute bin/main RUN upx --best --ultra-brute bin/main
RUN chmod +x bin/main RUN chmod +x bin/main

55
Containerfile.build Normal file
View file

@ -0,0 +1,55 @@
# Multi-platform build container for Gopherbook
FROM golang:alpine AS builder
RUN apk add --no-cache \
musl-dev \
gcc \
g++ \
mingw-w64-gcc \
wget \
xz \
git
# Install UPX for binary compression
RUN wget https://github.com/upx/upx/releases/download/v5.0.2/upx-5.0.2-amd64_linux.tar.xz && \
tar -xf upx-5.0.2-amd64_linux.tar.xz && \
mv upx-5.0.2-amd64_linux/upx /usr/local/bin/upx && \
rm -r upx-5.0.2-amd64_linux upx-5.0.2-amd64_linux.tar.xz
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Build Linux binary
RUN echo "Building Linux binary..." && \
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build \
-a \
-ldflags="-s -w -linkmode external -extldflags '-static'" \
-trimpath \
-o bin/gopherbook-linux ./app/gopherbook && \
upx --best --ultra-brute bin/gopherbook-linux && \
chmod +x bin/gopherbook-linux
# Build Windows binary
RUN echo "Building Windows binary..." && \
echo 'IDI_ICON1 ICON "./app/gopherbook/static/images/favicon/favicon.ico"' > gopherbook.rc && \
x86_64-w64-mingw32-windres gopherbook.rc -o ./app/gopherbook/gopherbook.syso && \
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CC=x86_64-w64-mingw32-gcc go build \
-a \
-ldflags="-s -w" \
-trimpath \
-o bin/gopherbook-windows.exe ./app/gopherbook && \
upx --best --ultra-brute bin/gopherbook-windows.exe && \
rm ./app/gopherbook/gopherbook.syso gopherbook.rc
# Verify binaries were created
RUN ls -lh bin/ && \
echo "Build complete!" && \
echo "Linux binary size: $(du -h bin/gopherbook-linux | cut -f1)" && \
echo "Windows binary size: $(du -h bin/gopherbook-windows.exe | cut -f1)"
# Keep the builder stage as the final stage so we can copy files out
FROM builder

View file

@ -1,5 +1,5 @@
build: build:
go build -o bin/main app/gopherbook/main.go go build -o bin/main ./app/gopherbook
clean: clean:
rm -rf watch etc library cache rm -rf watch etc library cache binaries releases

View file

@ -1,11 +1,21 @@
# Gopherbook Self-Hosted Comic Library & CBZ/CBT Reader <div align="center">
<img src="https://git.jester-designs.com/riomoo/gopherbook/media/branch/main/docs/images/gopherbook-title.png" alt="Description" width="50%">
</div>
<div align="center">
## Self-Hosted Comic Library & CBZ/CBT Reader
Gopherbook is a lightweight, single-binary, self-hosted web comic reader and library manager written in Go. Gopherbook is a lightweight, single-binary, self-hosted web comic reader and library manager written in Go.
It is designed for people who want full control over their digital comic collection (CBZ/CBT files), including support for password-protected/encrypted archives, per-user libraries, tagging, automatic organization, and a clean modern reader. It is designed for people who want full control over their digital comic collection (CBZ/CBT files), including support for password-protected/encrypted archives, per-user libraries, tagging, automatic organization, and a clean modern reader.
</div>
## License ## License
[![Custom badge](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fshare.jester-designs.com%2Fview%2Fpil.json)](LICENSE) [![Custom badge](https://git.jester-designs.com/riomoo/gopherbook/media/branch/main/docs/images/svgs/PIL.svg)](LICENSE)
## Features ## Features
@ -78,6 +88,15 @@ Then open http://localhost:12010 in your browser.
./etc/admin.json ← admin settings (registration toggle) ./etc/admin.json ← admin settings (registration toggle)
``` ```
## Environment Variables
Can be used to set where everything is stored with:
```
GOPHERBOOK_LIBRARY=$HOME/.config/gopherbook/library
GOPHERBOOK_CACHE=$HOME/.config/gopherbook/covers
GOPHERBOOK_ETC=$HOME/.config/gopherbook/etc
GOPHERBOOK_WATCH=$HOME/.config/gopherbook/watch
```
## Watch folder for bulk imports ## Watch folder for bulk imports
Gopherbook includes an automatic watch folder system that makes bulk importing comics effortless: Gopherbook includes an automatic watch folder system that makes bulk importing comics effortless:
@ -177,6 +196,10 @@ Pull requests are welcome! Especially:
Please open an issue first for bigger changes. Please open an issue first for bigger changes.
## If you'd like to know about the new mascot Vinny
- Check the [Wiki](/riomoo/gopherbook/wiki/Meet-Vinny.md)!
## Thanks / Credits ## Thanks / Credits
- yeka/zip password-protected ZIP support in pure Go - yeka/zip password-protected ZIP support in pure Go

39
app/gopherbook/config.go Normal file
View file

@ -0,0 +1,39 @@
package main
import (
"os"
"path/filepath"
)
var (
baseLibraryPath string
baseCachePath string
baseEtcPath string
baseWatchPath string
)
func init() {
// Store the base paths (before user-specific paths are added)
baseLibraryPath = libraryPath
baseCachePath = cachePath
baseEtcPath = etcPath
baseWatchPath = watchPath
// Override from environment variables if set
if env := os.Getenv("GOPHERBOOK_LIBRARY"); env != "" {
baseLibraryPath = filepath.Clean(env)
libraryPath = baseLibraryPath
}
if env := os.Getenv("GOPHERBOOK_CACHE"); env != "" {
baseCachePath = filepath.Clean(env)
cachePath = baseCachePath
}
if env := os.Getenv("GOPHERBOOK_ETC"); env != "" {
baseEtcPath = filepath.Clean(env)
etcPath = baseEtcPath
}
if env := os.Getenv("GOPHERBOOK_WATCH"); env != "" {
baseWatchPath = filepath.Clean(env)
watchPath = baseWatchPath
}
}

View file

@ -21,6 +21,7 @@ import (
"regexp" "regexp"
"sort" "sort"
"strings" "strings"
"io/fs"
"sync" "sync"
"time" "time"
"runtime" "runtime"
@ -39,6 +40,9 @@ import (
//go:embed templates/index.html //go:embed templates/index.html
var templateFS embed.FS var templateFS embed.FS
//go:embed all:static
var staticFS embed.FS
// ComicInfo represents the standard ComicInfo.xml metadata // ComicInfo represents the standard ComicInfo.xml metadata
type ComicInfo struct { type ComicInfo struct {
XMLName xml.Name `xml:"ComicInfo"` XMLName xml.Name `xml:"ComicInfo"`
@ -135,6 +139,14 @@ func main() {
loadUsers() loadUsers()
initWatchFolders() initWatchFolders()
// Create static sub-filesystem once
staticSubFS, err := fs.Sub(staticFS, "static")
if err != nil {
log.Fatal(fmt.Errorf("failed to create static sub-filesystem: %w", err))
}
// Create handlers once and reuse
staticHandler := http.FileServer(http.FS(staticSubFS))
http.HandleFunc("/api/register", handleRegister) http.HandleFunc("/api/register", handleRegister)
http.HandleFunc("/api/login", handleLogin) http.HandleFunc("/api/login", handleLogin)
@ -154,6 +166,7 @@ func main() {
http.HandleFunc("/api/admin/delete-comic/", authMiddleware(handleDeleteComic)) http.HandleFunc("/api/admin/delete-comic/", authMiddleware(handleDeleteComic))
http.HandleFunc("/api/watch-folder", authMiddleware(handleWatchFolder)) http.HandleFunc("/api/watch-folder", authMiddleware(handleWatchFolder))
http.HandleFunc("/", serveUI) http.HandleFunc("/", serveUI)
http.Handle("/static/", http.StripPrefix("/static/", staticHandler))
go func() { go func() {
for { for {
@ -544,8 +557,8 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
currentUser = req.Username currentUser = req.Username
key := deriveKey(req.Password) key := deriveKey(req.Password)
libraryPath = filepath.Join("./library", currentUser) libraryPath = filepath.Join(baseLibraryPath, currentUser)
cachePath = filepath.Join("./cache/covers", currentUser) cachePath = filepath.Join(baseCachePath, currentUser)
os.MkdirAll(filepath.Join(libraryPath, "Unorganized"), 0755) os.MkdirAll(filepath.Join(libraryPath, "Unorganized"), 0755)
os.MkdirAll(cachePath, 0755) os.MkdirAll(cachePath, 0755)
@ -599,8 +612,8 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
passwordsMutex.Unlock() passwordsMutex.Unlock()
currentEncryptionKey = nil currentEncryptionKey = nil
currentUser = "" currentUser = ""
libraryPath = "./library" libraryPath = baseLibraryPath
cachePath = "./cache/covers" cachePath = baseCachePath
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: "session", Name: "session",
@ -1599,7 +1612,7 @@ func saveJPEG(img image.Image, path string) error {
defer out.Close() defer out.Close()
// Lower quality = smaller memory footprint during encoding // Lower quality = smaller memory footprint during encoding
err = jpeg.Encode(out, img, &jpeg.Options{Quality: 70}) err = jpeg.Encode(out, img, &jpeg.Options{Quality: 85})
img = nil img = nil
runtime.GC() runtime.GC()
@ -1643,7 +1656,7 @@ func handleTags(w http.ResponseWriter, r *http.Request) {
} }
if req.Color == "" { if req.Color == "" {
req.Color = "#1f6feb" req.Color = "#446B6E"
} }
tagsMutex.Lock() tagsMutex.Lock()
@ -1832,7 +1845,7 @@ func handleTryKnownPasswords(w http.ResponseWriter, r *http.Request) {
tagData.Count++ tagData.Count++
tags[tag] = tagData tags[tag] = tagData
} else { } else {
tags[tag] = Tag{Name: tag, Color: "#1f6feb", Count: 1} tags[tag] = Tag{Name: tag, Color: "#446B6E", Count: 1}
} }
} }
tagsMutex.Unlock() tagsMutex.Unlock()
@ -1947,7 +1960,7 @@ func handleSetPassword(w http.ResponseWriter, r *http.Request) {
tagData.Count++ tagData.Count++
tags[tag] = tagData tags[tag] = tagData
} else { } else {
tags[tag] = Tag{Name: tag, Color: "#1f6feb", Count: 1} tags[tag] = Tag{Name: tag, Color: "#446B6E", Count: 1}
} }
} }
tagsMutex.Unlock() tagsMutex.Unlock()
@ -2419,7 +2432,7 @@ func processComic(filePath, filename string, modTime time.Time) Comic {
tagData.Count++ tagData.Count++
tags[tag] = tagData tags[tag] = tagData
} else { } else {
tags[tag] = Tag{Name: tag, Color: "#1f6feb", Count: 1} tags[tag] = Tag{Name: tag, Color: "#446B6E", Count: 1}
} }
} }
tagsMutex.Unlock() tagsMutex.Unlock()
@ -2481,7 +2494,7 @@ func loadComicMetadataLazy(comicID string) error {
tagData.Count++ tagData.Count++
tags[tag] = tagData tags[tag] = tagData
} else { } else {
tags[tag] = Tag{Name: tag, Color: "#1f6feb", Count: 1} tags[tag] = Tag{Name: tag, Color: "#446B6E", Count: 1}
} }
} }
tagsMutex.Unlock() tagsMutex.Unlock()
@ -2697,9 +2710,16 @@ func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
next(w, r) next(w, r)
} }
} }
func getUsersPath() string {
return filepath.Join(etcPath, "users.json")
}
func getAdminPath() string {
return filepath.Join(etcPath, "admin.json")
}
func loadUsers() { func loadUsers() {
data, err := os.ReadFile("etc/users.json") data, err := os.ReadFile(getUsersPath())
if err != nil { if err != nil {
return return
} }
@ -2707,7 +2727,7 @@ func loadUsers() {
log.Printf("Error unmarshaling users: %v", err) log.Printf("Error unmarshaling users: %v", err)
} }
adminData, err := os.ReadFile("etc/admin.json") adminData, err := os.ReadFile(getAdminPath())
if err == nil && len(adminData) > 0 { if err == nil && len(adminData) > 0 {
var adminConfig struct{ RegistrationEnabled bool } var adminConfig struct{ RegistrationEnabled bool }
if err := json.Unmarshal(adminData, &adminConfig); err == nil { if err := json.Unmarshal(adminData, &adminConfig); err == nil {
@ -2718,13 +2738,13 @@ func loadUsers() {
func saveUsers() { func saveUsers() {
data, _ := json.MarshalIndent(users, "", " ") data, _ := json.MarshalIndent(users, "", " ")
os.WriteFile("etc/users.json", data, 0644) os.WriteFile(getUsersPath(), data, 0644)
} }
func saveAdminConfig() { func saveAdminConfig() {
config := struct{ RegistrationEnabled bool }{RegistrationEnabled: registrationEnabled} config := struct{ RegistrationEnabled bool }{RegistrationEnabled: registrationEnabled}
data, _ := json.MarshalIndent(config, "", " ") data, _ := json.MarshalIndent(config, "", " ")
os.WriteFile("etc/admin.json", data, 0644) os.WriteFile(getAdminPath(), data, 0644)
} }
func loadTags() { func loadTags() {

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

View file

@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="/static/images/favicon/favicon.ico" rel="shortcut icon" type="image/x-icon">
<title>Gopherbook</title> <title>Gopherbook</title>
<style> <style>
* { * {
@ -27,7 +28,7 @@
header { header {
background: #395E62; background: #395E62;
border-bottom: 1px solid #314C52; border-bottom: 1px solid #314C52;
padding: 20px 0; padding: 10px 0;
margin-bottom: 30px; margin-bottom: 30px;
} }
@ -43,6 +44,8 @@
h1 { h1 {
color: #1b1e2c; color: #1b1e2c;
font-size: 24px; font-size: 24px;
padding: 20px 10px;
margin: auto;
} }
.auth-section { .auth-section {
@ -243,7 +246,7 @@
.comic-artist { .comic-artist {
display: inline-block; display: inline-block;
background: #1f6feb; background: #446B6E;
color: white; color: white;
padding: 2px 8px; padding: 2px 8px;
border-radius: 12px; border-radius: 12px;
@ -275,10 +278,18 @@
width: auto; width: auto;
} }
.tab:hover {
color: #1b1e2c;
}
.tab.active { .tab.active {
color: #395E62; color: #395E62;
border-bottom-color: #395E62; border-bottom-color: #395E62;
} }
.tab.active:hover {
color: #1b1e2c;
border-bottom-color: #395E62;
}
.message { .message {
padding: 12px; padding: 12px;
@ -334,7 +345,7 @@
} }
.bookmark-indicator { .bookmark-indicator {
color: #f0883e; color: #395E62;
font-size: 14px; font-size: 14px;
display: flex; display: flex;
align-items: center; align-items: center;
@ -345,7 +356,7 @@
position: absolute; position: absolute;
top: 8px; top: 8px;
right: 8px; right: 8px;
background: #f0883e; background: #395E62;
color: white; color: white;
padding: 4px 8px; padding: 4px 8px;
border-radius: 4px; border-radius: 4px;
@ -370,7 +381,7 @@
} }
.bookmark-list-title { .bookmark-list-title {
color: #f0883e; color: #395E62;
font-weight: 600; font-weight: 600;
margin-bottom: 8px; margin-bottom: 8px;
font-size: 14px; font-size: 14px;
@ -394,8 +405,7 @@
} }
.bookmark-list-item.current { .bookmark-list-item.current {
border-color: #f0883e; border-color: #395E62;
background: rgba(240, 136, 62, 0.1);
} }
.bookmark-delete { .bookmark-delete {
@ -539,7 +549,7 @@
} }
.modal-title { .modal-title {
color: #58a6ff; color: #446B6E;
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
} }
@ -667,7 +677,7 @@
} }
.password-modal-title { .password-modal-title {
color: #58a6ff; color: #446B6E;
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
margin-bottom: 8px; margin-bottom: 8px;
@ -701,7 +711,7 @@
.password-input-group input:focus { .password-input-group input:focus {
outline: none; outline: none;
border-color: #58a6ff; border-color: #446B6E;
} }
.password-modal-buttons { .password-modal-buttons {
@ -726,16 +736,22 @@
<body> <body>
<header> <header>
<div class="header-content"> <div class="header-content">
<h1>Gopherbook</h1> <div style="display: flex;">
<div id="userInfo" class="hidden"> <img src="/static/images/pngs/CutePose2.png" alt="on book" width="67px" height="75px">
<button onclick="logout()" class="secondary-btn" style="width: auto; padding: 8px 16px;">Logout</button> <h1>Gopherbook</h1>
</div> </div>
<div id="userInfo" class="hidden">
<button onclick="logout()" class="secondary-btn" style="width: auto; padding: 8px 16px;">Logout</button>
</div>
</div> </div>
</header> </header>
<div class="container"> <div class="container">
<div id="authSection" class="auth-section"> <div id="authSection" class="auth-section">
<div id="authMessage" class="message hidden"></div> <div id="authMessage" class="message hidden"></div>
<div style="display: flex; justify-content: center; margin: 0px 10px 20px 10px;">
<img src="/static/images/pngs/LogoPose2.png" alt="wink" style="width: 100px; height auto;">
</div>
<div class="tabs"> <div class="tabs">
<button class="tab active" onclick="showTab('login')">Login</button> <button class="tab active" onclick="showTab('login')">Login</button>
<button class="tab" onclick="showTab('register')">Register</button> <button class="tab" onclick="showTab('register')">Register</button>
@ -859,7 +875,7 @@
<h4 style="font-size: 14px; color: #8b949e; margin-bottom: 12px;">Create New Tag</h4> <h4 style="font-size: 14px; color: #8b949e; margin-bottom: 12px;">Create New Tag</h4>
<div class="add-tag-form"> <div class="add-tag-form">
<input type="text" id="newTagName" class="add-tag-input" placeholder="Tag name"> <input type="text" id="newTagName" class="add-tag-input" placeholder="Tag name">
<input type="color" id="newTagColor" class="color-picker" value="#1f6feb"> <input type="color" id="newTagColor" class="color-picker" value="#446B6E">
<button onclick="createTag()" style="width: auto; padding: 8px 16px;">Add</button> <button onclick="createTag()" style="width: auto; padding: 8px 16px;">Add</button>
</div> </div>
</div> </div>
@ -1052,7 +1068,7 @@
selectedTags.forEach(function(tagName) { selectedTags.forEach(function(tagName) {
var tag = allTags.find(function(t) { return t.name === tagName; }); var tag = allTags.find(function(t) { return t.name === tagName; });
var color = tag ? tag.color : '#1f6feb'; var color = tag ? tag.color : '#446B6E';
var filter = document.createElement('div'); var filter = document.createElement('div');
filter.className = 'tag-filter'; filter.className = 'tag-filter';
filter.style.background = color; filter.style.background = color;
@ -1200,7 +1216,7 @@
tagsHTML = '<div class="comic-tags">'; tagsHTML = '<div class="comic-tags">';
comic.tags.forEach(tagName => { comic.tags.forEach(tagName => {
const tag = allTags.find(t => t.name === tagName); const tag = allTags.find(t => t.name === tagName);
const color = tag ? tag.color : '#1f6feb'; const color = tag ? tag.color : '#446B6E';
tagsHTML += '<span class="comic-tag" style="background: ' + color + '">' + tagName + '</span>'; tagsHTML += '<span class="comic-tag" style="background: ' + color + '">' + tagName + '</span>';
}); });
tagsHTML += '</div>'; tagsHTML += '</div>';
@ -1279,7 +1295,7 @@
managingComic.tags.forEach(tagName => { managingComic.tags.forEach(tagName => {
const tag = allTags.find(t => t.name === tagName); const tag = allTags.find(t => t.name === tagName);
const color = tag ? tag.color : '#1f6feb'; const color = tag ? tag.color : '#446B6E';
const item = document.createElement('div'); const item = document.createElement('div');
item.className = 'tag-item'; item.className = 'tag-item';
@ -1367,7 +1383,7 @@
if (res.ok) { if (res.ok) {
document.getElementById('newTagName').value = ''; document.getElementById('newTagName').value = '';
document.getElementById('newTagColor').value = '#1f6feb'; document.getElementById('newTagColor').value = '#446B6E';
loadTags(); loadTags();
renderAvailableTags(); renderAvailableTags();
showMessage('Tag created!', 'success'); showMessage('Tag created!', 'success');
@ -1986,6 +2002,30 @@
.catch(function(err) { .catch(function(err) {
console.error('Initial comics fetch failed:', err); console.error('Initial comics fetch failed:', err);
}); });
// Add Enter key support for login form
document.getElementById('loginUsername').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
login();
}
});
document.getElementById('loginPassword').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
login();
}
});
// Add Enter key support for register form
document.getElementById('regUsername').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
register();
}
});
document.getElementById('regPassword').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
register();
}
});
</script> </script>
</body> </body>
</html> </html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

10
docs/images/svgs/PIL.svg Normal file
View file

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="114" height="28">
<g shape-rendering="crispEdges">
<rect width="75" height="28" fill="#555"/>
<rect x="75" width="39" height="28" fill="#07124A"/>
</g>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="10" font-weight="bold">
<text x="37" y="18">LICENSE</text>
<text x="94" y="18">PIL</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 447 B

62
scripts-bash/build-release.sh Executable file
View file

@ -0,0 +1,62 @@
#!/bin/bash
set -e
IMAGE_NAME="localhost/gopherbook-builder:latest"
CONTAINER_NAME="gopherbook-builder-tmp"
OUTPUT_DIR="./binaries"
echo "=== Building cross-compilation container ==="
podman build --force-rm -t "$IMAGE_NAME" -f Containerfile.build .
if [ $? -ne 0 ]; then
echo "Image build failed. Exiting."
exit 1
fi
echo ""
echo "=== Creating temporary container ==="
podman create --name "$CONTAINER_NAME" "$IMAGE_NAME"
echo ""
echo "=== Creating output directory ==="
mkdir -p "$OUTPUT_DIR"
rm -f "$OUTPUT_DIR"/*
echo ""
echo "=== Extracting Linux binary ==="
podman cp "$CONTAINER_NAME:/app/bin/gopherbook-linux" "$OUTPUT_DIR/gopherbook-linux"
chmod +x "$OUTPUT_DIR/gopherbook-linux"
echo ""
echo "=== Extracting Windows binary ==="
podman cp "$CONTAINER_NAME:/app/bin/gopherbook-windows.exe" "$OUTPUT_DIR/gopherbook-windows.exe"
chmod +x "$OUTPUT_DIR/gopherbook-windows.exe"
echo ""
echo "=== Cleaning up temporary container ==="
podman rm "$CONTAINER_NAME"
echo ""
echo "=== Build complete! ==="
echo "Binaries are in: $OUTPUT_DIR/"
ls -lh "$OUTPUT_DIR/"
echo ""
echo "=== Binary sizes ==="
du -h "$OUTPUT_DIR"/*
echo ""
echo "=== Cleaning up builder image ==="
podman rmi "$IMAGE_NAME"
echo ""
echo "✓ Done! Your binaries are ready:"
echo " • Linux: $OUTPUT_DIR/gopherbook-linux"
echo " • Windows: $OUTPUT_DIR/gopherbook-windows.exe"
echo ""
echo "To run the Linux binary:"
echo " $OUTPUT_DIR/gopherbook-linux"
echo ""
echo "To test the Windows binary (requires wine):"
echo " wine $OUTPUT_DIR/gopherbook-windows.exe"

161
scripts-bash/package-release.sh Executable file
View file

@ -0,0 +1,161 @@
#!/bin/bash
set -e
VERSION="${1:-v1.3.000}"
RELEASE_DIR="./releases"
BINARIES_DIR="./binaries"
echo "=== Packaging Gopherbook $VERSION ==="
echo ""
# Create release directory
mkdir -p "$RELEASE_DIR"
# Clean old releases for this version
rm -f "$RELEASE_DIR"/gopherbook-$VERSION-*
# Check if binaries exist
if [ ! -f "$BINARIES_DIR/gopherbook-linux" ] || [ ! -f "$BINARIES_DIR/gopherbook-windows.exe" ]; then
echo "Error: Binaries not found. Run ./build-and-extract.sh first."
exit 1
fi
echo "=== Creating README.txt ==="
cat > /tmp/README.txt << 'EOF'
Gopherbook - Comic Book Reader (CBZ/CBT)
=========================================
Quick Start:
------------
1. Run the gopherbook executable
2. Open your browser to http://localhost:8080
3. Register a new user account
4. Upload your CBZ/CBT comic files
Features:
---------
• Supports CBZ (ZIP) and CBT (TAR) formats
• Password-protected archives
• Tag management and filtering
• Bookmark pages
• Auto-organize by artist and story arc
• Watch folder for automatic imports
• Multi-user support with admin controls
Watch Folder:
-------------
Place CBZ/CBT files in the ./watch/<username>/ directory
and they will be automatically imported to your library.
Directory Structure:
--------------------
./library/ - Your comic library (per user)
./cache/ - Cover image cache
./etc/ - User data and settings
./watch/ - Watch folders for auto-import
Default Port: 8080
For more information, visit:
https://github.com/riomoo/gopherbook
https://codeberg.org/riomoo/gofudge
https://gitgud.io/riomoo/gopherbook
EOF
echo "=== Creating Linux package ==="
LINUX_DIR="/tmp/gopherbook-linux"
rm -rf "$LINUX_DIR"
mkdir -p "$LINUX_DIR"
# Copy Linux binary
cp "$BINARIES_DIR/gopherbook-linux" "$LINUX_DIR/gopherbook"
chmod +x "$LINUX_DIR/gopherbook"
# Copy documentation
cp /tmp/README.txt "$LINUX_DIR/"
# Create run script
cat > "$LINUX_DIR/run.sh" << 'EOF'
#!/bin/bash
echo "Starting Gopherbook..."
echo "Open your browser to: http://localhost:8080"
echo "Press Ctrl+C to stop"
echo ""
./gopherbook
EOF
chmod +x "$LINUX_DIR/run.sh"
# Package Linux
cd /tmp
tar -czf "$LINUX_DIR.tar.gz" gopherbook-linux/
cd - > /dev/null
mv "/tmp/gopherbook-linux.tar.gz" "$RELEASE_DIR/gopherbook-$VERSION-linux-amd64.tar.gz"
rm -rf "$LINUX_DIR"
echo "✓ Linux package created"
echo ""
echo "=== Creating Windows package ==="
WINDOWS_DIR="/tmp/gopherbook-windows"
rm -rf "$WINDOWS_DIR"
mkdir -p "$WINDOWS_DIR"
# Copy Windows binary
cp "$BINARIES_DIR/gopherbook-windows.exe" "$WINDOWS_DIR/gopherbook.exe"
# Copy documentation (Windows line endings)
unix2dos < /tmp/README.txt > "$WINDOWS_DIR/README.txt" 2>/dev/null || cp /tmp/README.txt "$WINDOWS_DIR/README.txt"
# Create batch file
cat > "$WINDOWS_DIR/run.bat" << 'EOF'
@echo off
echo Starting Gopherbook...
echo Open your browser to: http://localhost:8080
echo Press Ctrl+C to stop
echo.
gopherbook.exe
pause
EOF
# Create PowerShell script
cat > "$WINDOWS_DIR/run.ps1" << 'EOF'
Write-Host "Starting Gopherbook..." -ForegroundColor Green
Write-Host "Open your browser to: http://localhost:8080" -ForegroundColor Cyan
Write-Host "Press Ctrl+C to stop" -ForegroundColor Yellow
Write-Host ""
.\gopherbook.exe
EOF
# Package Windows
cd /tmp
zip -q -r "$WINDOWS_DIR.zip" gopherbook-windows/
cd - > /dev/null
mv "/tmp/gopherbook-windows.zip" "$RELEASE_DIR/gopherbook-$VERSION-windows-amd64.zip"
rm -rf "$WINDOWS_DIR"
echo "✓ Windows package created"
echo ""
echo "=== Creating checksums ==="
cd "$RELEASE_DIR"
sha256sum gopherbook-$VERSION-*.tar.gz gopherbook-$VERSION-*.zip > gopherbook-$VERSION-checksums.txt
cd - > /dev/null
echo "✓ Checksums created"
echo ""
echo "=== Release packages ready! ==="
echo ""
ls -lh "$RELEASE_DIR"/gopherbook-$VERSION-*
echo ""
echo "Release files:"
echo "$RELEASE_DIR/gopherbook-$VERSION-linux-amd64.tar.gz"
echo "$RELEASE_DIR/gopherbook-$VERSION-windows-amd64.zip"
echo "$RELEASE_DIR/gopherbook-$VERSION-checksums.txt"
echo ""
echo "Upload these files to GitHub Releases!"
# Cleanup
rm -f /tmp/README.txt

View file

@ -41,6 +41,6 @@ echo "Cleaning up old images..."
podman image prune --force podman image prune --force
echo "Update and cleanup complete!" echo "Update and cleanup complete!"
echo "Container is running with memory limit: 512MB, swap: 512MB" echo "Container is running with memory limit: 512MB"
echo "Go memory limit (GOMEMLIMIT): 512MiB" echo "Go memory limit (GOMEMLIMIT): 512MiB"
echo "Aggressive GC enabled (GOGC=50)" echo "Aggressive GC enabled (GOGC=50)"