Compare commits

...

1 commit

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
13 changed files with 111 additions and 38 deletions

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

View file

@ -35,12 +35,15 @@ RUN echo "Building Linux binary..." && \
# Build Windows binary # Build Windows binary
RUN echo "Building 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 \ CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CC=x86_64-w64-mingw32-gcc go build \
-a \ -a \
-ldflags="-s -w" \ -ldflags="-s -w" \
-trimpath \ -trimpath \
-o bin/gopherbook-windows.exe ./app/gopherbook && \ -o bin/gopherbook-windows.exe ./app/gopherbook && \
upx --best --ultra-brute bin/gopherbook-windows.exe upx --best --ultra-brute bin/gopherbook-windows.exe && \
rm ./app/gopherbook/gopherbook.syso gopherbook.rc
# Verify binaries were created # Verify binaries were created
RUN ls -lh bin/ && \ RUN ls -lh bin/ && \

View file

@ -2,4 +2,4 @@ build:
go build -o bin/main ./app/gopherbook 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
@ -186,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

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 {
@ -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()

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

View file

@ -2,7 +2,7 @@
set -e set -e
VERSION="${1:-v1.1.000}" VERSION="${1:-v1.3.000}"
RELEASE_DIR="./releases" RELEASE_DIR="./releases"
BINARIES_DIR="./binaries" BINARIES_DIR="./binaries"

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)"