Compare commits
No commits in common. "b3aa1d26469cba40d9b47fb8235b4b3c36f4cc1d" and "05a17238051162636fa7f074590e38f2d4551243" have entirely different histories.
b3aa1d2646
...
05a1723805
12 changed files with 31 additions and 116 deletions
|
|
@ -35,15 +35,12 @@ RUN echo "Building Linux binary..." && \
|
|||
|
||||
# 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
|
||||
upx --best --ultra-brute bin/gopherbook-windows.exe
|
||||
|
||||
# Verify binaries were created
|
||||
RUN ls -lh bin/ && \
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -2,4 +2,4 @@ build:
|
|||
go build -o bin/main ./app/gopherbook
|
||||
|
||||
clean:
|
||||
rm -rf watch etc library cache binaries releases
|
||||
rm -rf watch etc library cache
|
||||
|
|
|
|||
18
README.md
18
README.md
|
|
@ -1,21 +1,11 @@
|
|||
<div align="center">
|
||||
|
||||
<img src="docs/images/gopherbook-title.png" alt="Description" width="50%">
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
## Self-Hosted Comic Library & CBZ/CBT Reader
|
||||
# Gopherbook – Self-Hosted Comic Library & CBZ/CBT Reader
|
||||
|
||||
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.
|
||||
|
||||
</div>
|
||||
|
||||
## License
|
||||
|
||||
[](LICENSE)
|
||||
[](LICENSE)
|
||||
|
||||
## Features
|
||||
|
||||
|
|
@ -196,10 +186,6 @@ Pull requests are welcome! Especially:
|
|||
|
||||
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
|
||||
|
||||
- yeka/zip – password-protected ZIP support in pure Go
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import (
|
|||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"io/fs"
|
||||
"sync"
|
||||
"time"
|
||||
"runtime"
|
||||
|
|
@ -40,9 +39,6 @@ import (
|
|||
//go:embed templates/index.html
|
||||
var templateFS embed.FS
|
||||
|
||||
//go:embed all:static
|
||||
var staticFS embed.FS
|
||||
|
||||
// ComicInfo represents the standard ComicInfo.xml metadata
|
||||
type ComicInfo struct {
|
||||
XMLName xml.Name `xml:"ComicInfo"`
|
||||
|
|
@ -139,14 +135,6 @@ func main() {
|
|||
|
||||
loadUsers()
|
||||
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/login", handleLogin)
|
||||
|
|
@ -166,7 +154,6 @@ func main() {
|
|||
http.HandleFunc("/api/admin/delete-comic/", authMiddleware(handleDeleteComic))
|
||||
http.HandleFunc("/api/watch-folder", authMiddleware(handleWatchFolder))
|
||||
http.HandleFunc("/", serveUI)
|
||||
http.Handle("/static/", http.StripPrefix("/static/", staticHandler))
|
||||
|
||||
go func() {
|
||||
for {
|
||||
|
|
@ -1612,7 +1599,7 @@ func saveJPEG(img image.Image, path string) error {
|
|||
defer out.Close()
|
||||
|
||||
// Lower quality = smaller memory footprint during encoding
|
||||
err = jpeg.Encode(out, img, &jpeg.Options{Quality: 85})
|
||||
err = jpeg.Encode(out, img, &jpeg.Options{Quality: 70})
|
||||
img = nil
|
||||
|
||||
runtime.GC()
|
||||
|
|
@ -1656,7 +1643,7 @@ func handleTags(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
if req.Color == "" {
|
||||
req.Color = "#446B6E"
|
||||
req.Color = "#1f6feb"
|
||||
}
|
||||
|
||||
tagsMutex.Lock()
|
||||
|
|
@ -1845,7 +1832,7 @@ func handleTryKnownPasswords(w http.ResponseWriter, r *http.Request) {
|
|||
tagData.Count++
|
||||
tags[tag] = tagData
|
||||
} else {
|
||||
tags[tag] = Tag{Name: tag, Color: "#446B6E", Count: 1}
|
||||
tags[tag] = Tag{Name: tag, Color: "#1f6feb", Count: 1}
|
||||
}
|
||||
}
|
||||
tagsMutex.Unlock()
|
||||
|
|
@ -1960,7 +1947,7 @@ func handleSetPassword(w http.ResponseWriter, r *http.Request) {
|
|||
tagData.Count++
|
||||
tags[tag] = tagData
|
||||
} else {
|
||||
tags[tag] = Tag{Name: tag, Color: "#446B6E", Count: 1}
|
||||
tags[tag] = Tag{Name: tag, Color: "#1f6feb", Count: 1}
|
||||
}
|
||||
}
|
||||
tagsMutex.Unlock()
|
||||
|
|
@ -2432,7 +2419,7 @@ func processComic(filePath, filename string, modTime time.Time) Comic {
|
|||
tagData.Count++
|
||||
tags[tag] = tagData
|
||||
} else {
|
||||
tags[tag] = Tag{Name: tag, Color: "#446B6E", Count: 1}
|
||||
tags[tag] = Tag{Name: tag, Color: "#1f6feb", Count: 1}
|
||||
}
|
||||
}
|
||||
tagsMutex.Unlock()
|
||||
|
|
@ -2494,7 +2481,7 @@ func loadComicMetadataLazy(comicID string) error {
|
|||
tagData.Count++
|
||||
tags[tag] = tagData
|
||||
} else {
|
||||
tags[tag] = Tag{Name: tag, Color: "#446B6E", Count: 1}
|
||||
tags[tag] = Tag{Name: tag, Color: "#1f6feb", Count: 1}
|
||||
}
|
||||
}
|
||||
tagsMutex.Unlock()
|
||||
|
|
|
|||
BIN
app/gopherbook/static/images/favicon/favicon.ico
(Stored with Git LFS)
BIN
app/gopherbook/static/images/favicon/favicon.ico
(Stored with Git LFS)
Binary file not shown.
BIN
app/gopherbook/static/images/pngs/CutePose2.png
(Stored with Git LFS)
BIN
app/gopherbook/static/images/pngs/CutePose2.png
(Stored with Git LFS)
Binary file not shown.
BIN
app/gopherbook/static/images/pngs/LogoPose2.png
(Stored with Git LFS)
BIN
app/gopherbook/static/images/pngs/LogoPose2.png
(Stored with Git LFS)
Binary file not shown.
|
|
@ -3,7 +3,6 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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>
|
||||
<style>
|
||||
* {
|
||||
|
|
@ -28,7 +27,7 @@
|
|||
header {
|
||||
background: #395E62;
|
||||
border-bottom: 1px solid #314C52;
|
||||
padding: 10px 0;
|
||||
padding: 20px 0;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
|
|
@ -44,8 +43,6 @@
|
|||
h1 {
|
||||
color: #1b1e2c;
|
||||
font-size: 24px;
|
||||
padding: 20px 10px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.auth-section {
|
||||
|
|
@ -246,7 +243,7 @@
|
|||
|
||||
.comic-artist {
|
||||
display: inline-block;
|
||||
background: #446B6E;
|
||||
background: #1f6feb;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
|
|
@ -278,18 +275,10 @@
|
|||
width: auto;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: #1b1e2c;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: #395E62;
|
||||
border-bottom-color: #395E62;
|
||||
}
|
||||
.tab.active:hover {
|
||||
color: #1b1e2c;
|
||||
border-bottom-color: #395E62;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 12px;
|
||||
|
|
@ -345,7 +334,7 @@
|
|||
}
|
||||
|
||||
.bookmark-indicator {
|
||||
color: #395E62;
|
||||
color: #f0883e;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -356,7 +345,7 @@
|
|||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: #395E62;
|
||||
background: #f0883e;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
|
|
@ -381,7 +370,7 @@
|
|||
}
|
||||
|
||||
.bookmark-list-title {
|
||||
color: #395E62;
|
||||
color: #f0883e;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
|
|
@ -405,7 +394,8 @@
|
|||
}
|
||||
|
||||
.bookmark-list-item.current {
|
||||
border-color: #395E62;
|
||||
border-color: #f0883e;
|
||||
background: rgba(240, 136, 62, 0.1);
|
||||
}
|
||||
|
||||
.bookmark-delete {
|
||||
|
|
@ -549,7 +539,7 @@
|
|||
}
|
||||
|
||||
.modal-title {
|
||||
color: #446B6E;
|
||||
color: #58a6ff;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
|
@ -677,7 +667,7 @@
|
|||
}
|
||||
|
||||
.password-modal-title {
|
||||
color: #446B6E;
|
||||
color: #58a6ff;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
|
|
@ -711,7 +701,7 @@
|
|||
|
||||
.password-input-group input:focus {
|
||||
outline: none;
|
||||
border-color: #446B6E;
|
||||
border-color: #58a6ff;
|
||||
}
|
||||
|
||||
.password-modal-buttons {
|
||||
|
|
@ -736,22 +726,16 @@
|
|||
<body>
|
||||
<header>
|
||||
<div class="header-content">
|
||||
<div style="display: flex;">
|
||||
<img src="/static/images/pngs/CutePose2.png" alt="on book" width="67px" height="75px">
|
||||
<h1>Gopherbook</h1>
|
||||
</div>
|
||||
<div id="userInfo" class="hidden">
|
||||
<button onclick="logout()" class="secondary-btn" style="width: auto; padding: 8px 16px;">Logout</button>
|
||||
</div>
|
||||
<h1>Gopherbook</h1>
|
||||
<div id="userInfo" class="hidden">
|
||||
<button onclick="logout()" class="secondary-btn" style="width: auto; padding: 8px 16px;">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div id="authSection" class="auth-section">
|
||||
<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">
|
||||
<button class="tab active" onclick="showTab('login')">Login</button>
|
||||
<button class="tab" onclick="showTab('register')">Register</button>
|
||||
|
|
@ -875,7 +859,7 @@
|
|||
<h4 style="font-size: 14px; color: #8b949e; margin-bottom: 12px;">Create New Tag</h4>
|
||||
<div class="add-tag-form">
|
||||
<input type="text" id="newTagName" class="add-tag-input" placeholder="Tag name">
|
||||
<input type="color" id="newTagColor" class="color-picker" value="#446B6E">
|
||||
<input type="color" id="newTagColor" class="color-picker" value="#1f6feb">
|
||||
<button onclick="createTag()" style="width: auto; padding: 8px 16px;">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1068,7 +1052,7 @@
|
|||
|
||||
selectedTags.forEach(function(tagName) {
|
||||
var tag = allTags.find(function(t) { return t.name === tagName; });
|
||||
var color = tag ? tag.color : '#446B6E';
|
||||
var color = tag ? tag.color : '#1f6feb';
|
||||
var filter = document.createElement('div');
|
||||
filter.className = 'tag-filter';
|
||||
filter.style.background = color;
|
||||
|
|
@ -1216,7 +1200,7 @@
|
|||
tagsHTML = '<div class="comic-tags">';
|
||||
comic.tags.forEach(tagName => {
|
||||
const tag = allTags.find(t => t.name === tagName);
|
||||
const color = tag ? tag.color : '#446B6E';
|
||||
const color = tag ? tag.color : '#1f6feb';
|
||||
tagsHTML += '<span class="comic-tag" style="background: ' + color + '">' + tagName + '</span>';
|
||||
});
|
||||
tagsHTML += '</div>';
|
||||
|
|
@ -1295,7 +1279,7 @@
|
|||
|
||||
managingComic.tags.forEach(tagName => {
|
||||
const tag = allTags.find(t => t.name === tagName);
|
||||
const color = tag ? tag.color : '#446B6E';
|
||||
const color = tag ? tag.color : '#1f6feb';
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'tag-item';
|
||||
|
|
@ -1383,7 +1367,7 @@
|
|||
|
||||
if (res.ok) {
|
||||
document.getElementById('newTagName').value = '';
|
||||
document.getElementById('newTagColor').value = '#446B6E';
|
||||
document.getElementById('newTagColor').value = '#1f6feb';
|
||||
loadTags();
|
||||
renderAvailableTags();
|
||||
showMessage('Tag created!', 'success');
|
||||
|
|
@ -2002,30 +1986,6 @@
|
|||
.catch(function(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>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
BIN
docs/images/gopherbook-title.png
(Stored with Git LFS)
BIN
docs/images/gopherbook-title.png
(Stored with Git LFS)
Binary file not shown.
BIN
docs/images/svgs/PIL.svg
(Stored with Git LFS)
BIN
docs/images/svgs/PIL.svg
(Stored with Git LFS)
Binary file not shown.
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
set -e
|
||||
|
||||
VERSION="${1:-v1.3.000}"
|
||||
VERSION="${1:-v1.1.000}"
|
||||
RELEASE_DIR="./releases"
|
||||
BINARIES_DIR="./binaries"
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,6 @@ echo "Cleaning up old images..."
|
|||
podman image prune --force
|
||||
|
||||
echo "Update and cleanup complete!"
|
||||
echo "Container is running with memory limit: 512MB"
|
||||
echo "Container is running with memory limit: 512MB, swap: 512MB"
|
||||
echo "Go memory limit (GOMEMLIMIT): 512MiB"
|
||||
echo "Aggressive GC enabled (GOGC=50)"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue