Compare commits

..

No commits in common. "74de3ddee2c13782f9932c61b988fd583dce564c" and "91f8380a1f2b23ba4eb0e5d65d284b63a7b4e974" have entirely different histories.

8 changed files with 25 additions and 279 deletions

5
.gitignore vendored
View File

@ -1,7 +1,6 @@
.files/
.last-branch
__pycache__/
*.pyc
.claude/
.codex

View File

@ -8,9 +8,10 @@ set -euo pipefail
# - Build & push Docker images for each service under ./containers/*
# - Two modes:
# t (test) = only push :dev
# r (release) = push :<version>, :dev, :latest, then commit + tag and
# push a release/<version> branch + tag to origin.
# Version is read from the top of changelog.md.
# r (release) = push :<version>, :dev, :latest
# version is read from the top of changelog.md
#
# No git operations: committing and tagging is done manually.
#
# Usage:
# ./build-and-push.sh [mode]
@ -24,67 +25,14 @@ set -euo pipefail
# ## vX.Y.Z — YYYY-MM-DD
# (the version is parsed from the first such line)
# - One Dockerfile per service under ./containers/<service>/Dockerfile
#
# ----------------------------------------------------------------------------
# Self-update notice (for humans AND for Claude):
#
# The canonical/master copy of this script lives at:
# /docker/develop/shared-integrations/tooling/docker-build-and-push/build-and-push.sh
#
# When this script is copied into a project repo, it may drift from master.
# On startup the script compares its own SCRIPT_VERSION to the master copy and
# warns if a newer version exists. It does NOT auto-overwrite (too risky during
# a release run).
#
# CLAUDE INSTRUCTION:
# If you (Claude) are about to invoke this script in a project repo, first
# check whether the master copy at the path above is newer (different
# SCRIPT_VERSION or differing file contents). If so, copy the master over
# the local copy BEFORE running, then proceed. Mention the update to the user.
# ============================================================================
SCRIPT_VERSION="1.2.0"
SCRIPT_MASTER_PATH="/docker/develop/shared-integrations/tooling/docker-build-and-push/build-and-push.sh"
DOCKER_REGISTRY="gitea.oskamp.info"
DOCKER_NAMESPACE="ivooskamp"
CHANGELOG_FILE="docs/changelog.md"
CONTAINERS_DIR="containers"
# --- Self-update check -------------------------------------------------------
# Compare this script to the canonical master copy and warn (don't overwrite).
self_update_check() {
local self_path="${BASH_SOURCE[0]}"
# Resolve to absolute path so a comparison against itself is detected.
local self_abs
self_abs="$(cd "$(dirname "$self_path")" 2>/dev/null && pwd)/$(basename "$self_path")" || self_abs="$self_path"
if [[ "$self_abs" == "$SCRIPT_MASTER_PATH" ]]; then
return 0 # We ARE the master copy.
fi
if [[ ! -f "$SCRIPT_MASTER_PATH" ]]; then
return 0 # Master not reachable from this host; silently skip.
fi
local master_version
master_version="$(grep -m1 -E '^SCRIPT_VERSION=' "$SCRIPT_MASTER_PATH" | sed -E 's/.*"([^"]+)".*/\1/')"
if [[ -n "$master_version" && "$master_version" != "$SCRIPT_VERSION" ]]; then
echo "[WARN] A newer build-and-push.sh is available."
echo " local : $SCRIPT_VERSION"
echo " master : $master_version ($SCRIPT_MASTER_PATH)"
echo " Update with: cp \"$SCRIPT_MASTER_PATH\" \"$self_abs\""
echo ""
elif ! cmp -s "$self_abs" "$SCRIPT_MASTER_PATH"; then
echo "[WARN] Local build-and-push.sh differs from master (same SCRIPT_VERSION=$SCRIPT_VERSION)."
echo " master : $SCRIPT_MASTER_PATH"
echo " Diff or update with: cp \"$SCRIPT_MASTER_PATH\" \"$self_abs\""
echo ""
fi
}
self_update_check
# --- Input: prompt if missing ------------------------------------------------
MODE="${1:-}"
if [[ -z "${MODE}" ]]; then
@ -279,33 +227,10 @@ for img in "${BUILT_IMAGES[@]}"; do
done
echo "============================================================"
echo ""
# --- Git: commit + release branch + tag (release mode only) -----------------
echo "[REMINDER] No git operations were performed. If this was a release,"
echo " commit and tag manually, e.g.:"
if [[ "$MODE" == "r" ]]; then
if [[ ! -d ".git" ]]; then
echo "[WARN] Not a git checkout — skipping git commit/tag/push."
exit 0
fi
RELEASE_BRANCH="release/${VERSION}"
echo "[INFO] Preparing git release: branch=${RELEASE_BRANCH}, tag=${VERSION}"
# Stage everything and commit only if there's something to commit.
git add -A
if git diff --cached --quiet; then
echo "[INFO] Working tree clean — no commit needed."
else
git commit -m "Release ${VERSION}"
fi
# Create or move the annotated tag to current HEAD.
if git rev-parse -q --verify "refs/tags/${VERSION}" >/dev/null; then
echo "[WARN] Tag ${VERSION} already exists locally — leaving it as-is."
else
git tag -a "${VERSION}" -m "Release ${VERSION}"
fi
git push origin "HEAD:refs/heads/${RELEASE_BRANCH}"
git push origin "refs/tags/${VERSION}"
echo "[INFO] Pushed ${RELEASE_BRANCH} and tag ${VERSION} to origin."
echo " git add -A && git commit -m \"Release ${VERSION}\""
echo " git tag -a ${VERSION} -m \"Release ${VERSION}\""
echo " git push && git push --tags"
fi

View File

@ -3,34 +3,6 @@ Changelog data for Novela
"""
CHANGELOG = [
{
"version": "v0.2.11",
"date": "2026-05-10",
"summary": "Subheading styling now also applies when the wrapper contains a block element (e.g. <div class=\"subheading\"><p>…</p></div>) — the inner <p>/<h*> color rule no longer wins.",
"sections": [
{
"title": "Bug fixes",
"type": "bugfix",
"changes": [
"Reader: subheading styling now applies to descendants too — previously the inner <p> (or <h*>) had the same specificity as .subheading but applied more directly, so the wrapper's color was overridden and the paragraph kept the default text color. CSS now reads #chapter-content .subheading, #chapter-content .subheading * { … }, and the same change was made to .chat for safety.",
],
},
],
},
{
"version": "v0.2.10",
"date": "2026-05-10",
"summary": "Subheading and chat styling now also renders when the wrapper is a <div> instead of a <span> (e.g. when applied around or inside a heading).",
"sections": [
{
"title": "Bug fixes",
"type": "bugfix",
"changes": [
"Editor / Reader: subheading (S) and chat (C) styling now also applies when the wrapper is a <div> instead of a <span>. wrapSpan() in editor.js falls back to <div class=\"\"> whenever the selection contains a block element (e.g. <h1..6>, <p>, <div>) to keep the HTML valid; the previous CSS in reader.html only matched the <span> form, so anything wrapped around or inside a heading silently lost its color/weight. CSS selectors are now class-only (#chapter-content .subheading / .chat), matching both span and div wrappers.",
],
},
],
},
{
"version": "v0.2.9",
"date": "2026-05-09",

View File

@ -13,26 +13,8 @@
--success: #6baa6b;
--warning: #c8a03a;
--error: #c85a3a;
--shadow-ring: rgba(255,255,255,0.1);
--radius: 6px;
--sidebar: 220px;
--mono: 'DM Mono', monospace;
--serif: 'Libre Baskerville', Georgia, serif;
}
/* ── Sepia reading theme (reader only) ──────────────────────────────────── */
:root[data-theme="sepia"] {
--bg: #f4ecd8;
--surface: #ece2cb;
--surface2: #e3d7bb;
--border: #d8cbac;
--accent: #c17d12;
--accent2: #a9690c;
--text: #5b4636;
--text-dim: #7c6a55;
--text-faint: #ab9a82;
--success: #4e8a4e;
--warning: #9a7714;
--error: #b04a2c;
--shadow-ring: rgba(0,0,0,0.12);
}

View File

@ -71,7 +71,7 @@
.btn-header-bm:hover { background: rgba(255,162,14,0.08); border-color: var(--accent); }
.btn-header-series {
display: none;
color: var(--text-faint); border-color: var(--border);
color: var(--text-faint); border-color: rgba(255,255,255,0.08);
padding: 0.3rem 0.5rem;
}
.btn-header-series.active {
@ -177,26 +177,11 @@
width: 24px; height: 24px; border-radius: 50%;
border: 2px solid transparent;
cursor: pointer; transition: border-color 0.12s, transform 0.1s;
box-shadow: 0 0 0 1px var(--shadow-ring);
box-shadow: 0 0 0 1px rgba(255,255,255,0.1);
padding: 0;
}
.colour-swatch:hover { transform: scale(1.15); }
.colour-swatch.active { border-color: var(--accent); }
.colour-swatches.hidden { display: none; }
/* ── Theme toggle ── */
.theme-options { display: flex; gap: 0.5rem; }
.theme-btn {
flex: 1;
padding: 0.4rem 0.6rem;
background: var(--surface2); border: 1px solid var(--border);
border-radius: var(--radius);
font-family: var(--mono); font-size: 0.7rem;
color: var(--text-dim); cursor: pointer;
transition: color 0.12s, border-color 0.12s;
}
.theme-btn:hover { color: var(--text); border-color: var(--text-faint); }
.theme-btn.active { color: var(--accent); border-color: var(--accent); }
/* ── Viewer ── */
#viewer {
@ -234,10 +219,8 @@
margin-bottom: 2.5rem; padding-bottom: 1rem;
border-bottom: 1px solid var(--border);
}
#chapter-content .subheading,
#chapter-content .subheading * { color: rgb(224, 62, 45); font-weight: bold; }
#chapter-content .chat,
#chapter-content .chat * { color: rgb(230, 126, 35); }
#chapter-content span.subheading { color: rgb(224, 62, 45); font-weight: bold; }
#chapter-content span.chat { color: rgb(230, 126, 35); }
#chapter-content p[style*="padding-left"] { padding-left: 40px; }
#chapter-content .novela-comment {
border-left: 3px solid #6b9dd6;
@ -315,16 +298,6 @@
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
<script>
/* Apply saved reading theme before paint to avoid a flash. */
(function () {
try {
if (localStorage.getItem('reader-theme') === 'sepia') {
document.documentElement.setAttribute('data-theme', 'sepia');
}
} catch (e) {}
})();
</script>
</head>
<body>
@ -352,13 +325,6 @@
<div class="settings-overlay" id="settings-overlay" onclick="closeSettings()"></div>
<div class="settings-drawer" id="settings-drawer">
<div class="settings-drawer-title">Reading settings</div>
<div class="settings-row">
<div class="settings-label">Theme</div>
<div class="theme-options">
<button class="theme-btn" data-theme="dark" onclick="applyTheme('dark')">Dark</button>
<button class="theme-btn" data-theme="sepia" onclick="applyTheme('sepia')">Sepia</button>
</div>
</div>
<div class="settings-row">
<div class="settings-label">
Content width
@ -377,20 +343,13 @@
</div>
<div class="settings-row">
<div class="settings-label">Text colour</div>
<div class="colour-swatches" data-theme="dark">
<div class="colour-swatches">
<button class="colour-swatch" data-colour="#e8e2d9" title="Bright" style="background:#e8e2d9" onclick="applyTextColour('#e8e2d9')"></button>
<button class="colour-swatch" data-colour="#d4cec5" title="Warm cream" style="background:#d4cec5" onclick="applyTextColour('#d4cec5')"></button>
<button class="colour-swatch" data-colour="#bfb8ae" title="Soft sand" style="background:#bfb8ae" onclick="applyTextColour('#bfb8ae')"></button>
<button class="colour-swatch" data-colour="#a9a29a" title="Muted" style="background:#a9a29a" onclick="applyTextColour('#a9a29a')"></button>
<button class="colour-swatch" data-colour="#938d86" title="Dim" style="background:#938d86" onclick="applyTextColour('#938d86')"></button>
</div>
<div class="colour-swatches hidden" data-theme="sepia">
<button class="colour-swatch" data-colour="#5b4636" title="Warm brown" style="background:#5b4636" onclick="applyTextColour('#5b4636')"></button>
<button class="colour-swatch" data-colour="#4a3a2c" title="Dark cocoa" style="background:#4a3a2c" onclick="applyTextColour('#4a3a2c')"></button>
<button class="colour-swatch" data-colour="#6b5746" title="Medium" style="background:#6b5746" onclick="applyTextColour('#6b5746')"></button>
<button class="colour-swatch" data-colour="#3a2e24" title="Near black" style="background:#3a2e24" onclick="applyTextColour('#3a2e24')"></button>
<button class="colour-swatch" data-colour="#7c6a55" title="Dim" style="background:#7c6a55" onclick="applyTextColour('#7c6a55')"></button>
</div>
</div>
</div>
@ -491,49 +450,18 @@
applyWidth(saved);
}
// ── Theme (Dark / Sepia) ───────────────────────────────────────
const THEME_TEXT_DEFAULT = { dark: '#e8e2d9', sepia: '#5b4636' };
function currentTheme() {
return document.documentElement.getAttribute('data-theme') === 'sepia' ? 'sepia' : 'dark';
}
function applyTheme(theme) {
if (theme !== 'sepia') theme = 'dark';
if (theme === 'dark') {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
localStorage.setItem('reader-theme', theme);
document.querySelectorAll('.theme-btn').forEach(el => {
el.classList.toggle('active', el.dataset.theme === theme);
});
document.querySelectorAll('.colour-swatches').forEach(el => {
el.classList.toggle('hidden', el.dataset.theme !== theme);
});
loadTextColour();
}
function loadTheme() {
applyTheme(localStorage.getItem('reader-theme') || 'dark');
}
// ── Text colour (per theme) ────────────────────────────────────
// ── Text colour ────────────────────────────────────────────────
function applyTextColour(hex) {
const theme = currentTheme();
document.documentElement.style.setProperty('--text', hex);
localStorage.setItem('reader-text-colour-' + theme, hex);
document.querySelectorAll('.colour-swatches[data-theme="' + theme + '"] .colour-swatch')
.forEach(el => el.classList.toggle('active', el.dataset.colour === hex));
localStorage.setItem('reader-text-colour', hex);
document.querySelectorAll('.colour-swatch').forEach(el => {
el.classList.toggle('active', el.dataset.colour === hex);
});
}
function loadTextColour() {
const theme = currentTheme();
let saved = localStorage.getItem('reader-text-colour-' + theme);
// Migrate the pre-theme single key into the dark slot.
if (!saved && theme === 'dark') saved = localStorage.getItem('reader-text-colour');
applyTextColour(saved || THEME_TEXT_DEFAULT[theme]);
const saved = localStorage.getItem('reader-text-colour') || '#e8e2d9';
applyTextColour(saved);
}
// ── Font size ──────────────────────────────────────────────────
@ -709,7 +637,7 @@
// ── Init ───────────────────────────────────────────────────────
async function init() {
loadWidth();
loadTheme();
loadTextColour();
loadFontSize();
loadSeriesNav();

View File

@ -1,49 +1,5 @@
# Develop Changelog
## 2026-06-01 — Reader: Sepia reading theme
### Added
- The reader now offers a **Sepia** theme alongside the existing Dark theme, for easier long-form reading (warm paper background with dark brown text instead of light-on-black). New **Theme** toggle (Dark / Sepia) at the top of the reading settings drawer.
- `static/theme.css`: added a `:root[data-theme="sepia"]` palette overriding `--bg`, `--surface`, `--surface2`, `--border`, `--accent`, `--accent2`, `--text`, `--text-dim`, `--text-faint`, `--success`, `--warning`, `--error`. Also added a `--shadow-ring` variable (light hairline in dark, dark hairline in sepia) so swatch rings read correctly in both themes.
- `templates/reader.html`: theme toggle buttons + `.theme-btn` styling; an inline head script applies the saved theme before paint to avoid a flash; the Text colour row now has two theme-specific swatch sets (light tints for dark, dark brown tints for sepia), toggled by visibility.
- JS: `applyTheme()`/`loadTheme()` set the `data-theme` attribute and persist to `localStorage` (`reader-theme`). Text colour is now stored per theme (`reader-text-colour-dark` / `reader-text-colour-sepia`); the old single `reader-text-colour` key is migrated into the dark slot. `init()` calls `loadTheme()` (which loads the active theme's text colour).
- Replaced two hard-coded `rgba(255,255,255,...)` values in the reader CSS (swatch ring, series-button border) with theme variables so they don't show as white halos in sepia.
## 2026-05-31 — Build version in the sidebar
### Added
- The sidebar now shows the running build version at the bottom (e.g. `v0.2.11` for releases, `v0.2.11.3` for dev builds), linking to the changelog page.
- New `containers/novela/version.py` exposes `display_version()`. The semantic release version stays the single source in `changelog.py` (`CHANGELOG[0]["version"]`); a `BUILD` segment is appended for dev builds.
- `shared_templates.py` registers `app_version` as a Jinja global; `_sidebar.html` renders it as a `.sidebar-version` link styled in `sidebar.css`.
- `main.py` adds a `/api/version` endpoint returning `{"version": display_version()}`.
## 2026-05-31 — Find & Replace: scope option
### Added
- Editor Find & Replace: new **Current chapter only** checkbox in the modal options. When checked, the search/replace runs against the currently open chapter instead of every chapter in the book.
- `templates/editor.html`: added the `rp-current` checkbox next to Regex/Case sensitive; modal title changed from "Find & Replace — all chapters" to "Find & Replace" since scope is now selectable.
- `static/editor.js` (`replaceInAllChapters()`): reads `rp-current`; builds a `targets` list of either just `currentCh()` or all `chapters`, and iterates that. Shows an error ("No chapter open.") if Current-chapter-only is selected with no open chapter. Result message reads "… in current chapter" for the single-chapter case.
- Default is unchecked, so the existing all-chapters behaviour is preserved.
## 2026-05-10
- Reader: subheading/chat styling now also wins when the wrapper contains block elements with their own color rule (e.g. `<div class="subheading"><p>…</p></div>`)
- Previously the `<p>`/`<h*>` rules in `reader.html` (`#chapter-content p { color: var(--text); }`, etc.) had the same specificity as `.subheading` and applied more directly to the inner element, so the parent's color was effectively overridden — the wrapped paragraph stayed in the default text color
- CSS now applies the rule to the wrapper *and* all descendants: `#chapter-content .subheading, #chapter-content .subheading * { … }` (and the same for `.chat`)
- Reason: bug reported after v0.2.10 — `.chat` worked because it wraps plain inline text inside a `<p>`, but `.subheading` wrapping a `<p>` lost its color
---
*Released as v0.2.11 on 2026-05-10*
---
*Released as v0.2.10 on 2026-05-10*
## 2026-05-09 (2)
- Editor / Reader: subheading (**S**) and chat (**C**) styling now also applies when the wrapper is a `<div>` instead of a `<span>`
- `wrapSpan()` in `editor.js` falls back to `<div class="…">` whenever the selection contains a block element (`<h1..6>`, `<p>`, `<div>`, etc.) so the resulting HTML stays valid; the previous CSS in `reader.html` (`#chapter-content span.subheading` / `span.chat`) only matched the `<span>` form, so anything wrapped around or inside a heading silently lost its color/weight
- CSS selectors are now class-only (`#chapter-content .subheading` / `.chat`), matching both span and div wrappers
---
## 2026-05-09
- Reader: progress is now monotonic across devices — saved position only advances, never rewinds
- `POST /library/progress/{filename}` in `routers/reader.py` parses the incoming `cfi` as `(chapterIndex, scrollFrac)` and the currently stored row the same way, then skips the write when `new_pos <= cur_pos`
@ -52,7 +8,6 @@
- Reason: when reading the same book on device A (chapter 12) and then continuing on device B (chapter 15), opening device A again previously sent its stale chapter-12 cfi back to the server and overwrote the further progress; now the older position is ignored
---
*Released as v0.2.9 on 2026-05-09*
## 2026-04-22 (5)
- Grabber: newly converted books now appear in the **New** view again

View File

@ -1,21 +1,5 @@
# Changelog
## v0.2.11 — 2026-05-10
### Bug fixes
- Reader: subheading styling now also applies when the wrapper contains a block element with its own color rule, e.g. `<div class="subheading"><p>…</p></div>`. Previously the `<p>` (and `<h*>`) rules in `reader.html` had the same CSS specificity as `.subheading` but applied more directly to the inner element, so the wrapper's color was effectively overridden and the paragraph kept the default text color. The rule now applies to the wrapper and all descendants (`#chapter-content .subheading, #chapter-content .subheading * { … }`); the same change was made to `.chat` for safety. Reported right after v0.2.10 — `.chat` was unaffected because it wraps plain inline text, but `.subheading` wrapping a paragraph lost its color.
---
## v0.2.10 — 2026-05-10
### Bug fixes
- Editor / Reader: subheading (**S**) and chat (**C**) styling now also renders correctly when the wrapper is a `<div>` instead of a `<span>`. When the selection contained a block element (e.g. an `<h1..6>`, `<p>` or `<div>`), `wrapSpan()` in `editor.js` falls back to `<div class="…">` to keep the HTML valid; the previous CSS in `reader.html` only targeted `span.subheading` / `span.chat`, so anything wrapped around or inside a heading silently lost its color/weight. CSS selectors are now class-only (`#chapter-content .subheading` / `.chat`), matching both span and div wrappers.
---
## v0.2.9 — 2026-05-09
### Bug fixes

1
version.txt Normal file
View File

@ -0,0 +1 @@
v0.2.8