Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e65883cc9e | ||
|
|
b19af1e8f3 | ||
|
|
2864c4a6c0 | ||
|
|
6d0dac256d | ||
|
|
8922111471 | ||
|
|
74e7c8e8d1 | ||
|
|
2f74b2b229 | ||
|
|
cb9598a786 |
@@ -55,102 +55,7 @@ jobs:
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ui-build
|
||||
path: ui/build
|
||||
retention-days: 1
|
||||
|
||||
# ── ui: source map upload ─────────────────────────────────────────────────────
|
||||
# Commented out — re-enable when GlitchTip source map uploads are needed again.
|
||||
#
|
||||
# upload-sourcemaps:
|
||||
# name: Upload source maps
|
||||
# runs-on: ubuntu-latest
|
||||
# needs: [check-ui]
|
||||
# steps:
|
||||
# - name: Compute release version (strip leading v)
|
||||
# id: ver
|
||||
# run: |
|
||||
# V="${{ gitea.ref_name }}"
|
||||
# echo "version=${V#v}" >> "$GITHUB_OUTPUT"
|
||||
#
|
||||
# - name: Download build artifacts
|
||||
# uses: actions/download-artifact@v3
|
||||
# with:
|
||||
# name: ui-build
|
||||
# path: build
|
||||
#
|
||||
# - name: Install sentry-cli
|
||||
# run: npm install -g @sentry/cli
|
||||
#
|
||||
# - name: Inject debug IDs into build artifacts
|
||||
# run: sentry-cli sourcemaps inject ./build
|
||||
# env:
|
||||
# SENTRY_URL: https://errors.libnovel.cc/
|
||||
# SENTRY_AUTH_TOKEN: ${{ secrets.GLITCHTIP_AUTH_TOKEN }}
|
||||
# SENTRY_ORG: libnovel
|
||||
# SENTRY_PROJECT: ui
|
||||
#
|
||||
# - name: Upload injected build (for docker-ui)
|
||||
# uses: actions/upload-artifact@v3
|
||||
# with:
|
||||
# name: ui-build-injected
|
||||
# path: build
|
||||
# retention-days: 1
|
||||
#
|
||||
# - name: Create GlitchTip release
|
||||
# run: sentry-cli releases new ${{ steps.ver.outputs.version }}
|
||||
# env:
|
||||
# SENTRY_URL: https://errors.libnovel.cc/
|
||||
# SENTRY_AUTH_TOKEN: ${{ secrets.GLITCHTIP_AUTH_TOKEN }}
|
||||
# SENTRY_ORG: libnovel
|
||||
# SENTRY_PROJECT: ui
|
||||
#
|
||||
# - name: Upload source maps to GlitchTip
|
||||
# run: sentry-cli sourcemaps upload ./build --release ${{ steps.ver.outputs.version }}
|
||||
# env:
|
||||
# SENTRY_URL: https://errors.libnovel.cc/
|
||||
# SENTRY_AUTH_TOKEN: ${{ secrets.GLITCHTIP_AUTH_TOKEN }}
|
||||
# SENTRY_ORG: libnovel
|
||||
# SENTRY_PROJECT: ui
|
||||
#
|
||||
# - name: Finalize GlitchTip release
|
||||
# run: sentry-cli releases finalize ${{ steps.ver.outputs.version }}
|
||||
# env:
|
||||
# SENTRY_URL: https://errors.libnovel.cc/
|
||||
# SENTRY_AUTH_TOKEN: ${{ secrets.GLITCHTIP_AUTH_TOKEN }}
|
||||
# SENTRY_ORG: libnovel
|
||||
# SENTRY_PROJECT: ui
|
||||
#
|
||||
# - name: Prune old GlitchTip releases (keep latest 10)
|
||||
# run: |
|
||||
# set -euo pipefail
|
||||
# KEEP=10
|
||||
# OLD=$(curl -sf \
|
||||
# -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \
|
||||
# "$SENTRY_URL/api/0/organizations/$SENTRY_ORG/releases/?project=$SENTRY_PROJECT&per_page=100" \
|
||||
# | python3 -c "
|
||||
# import sys, json
|
||||
# releases = json.load(sys.stdin)
|
||||
# for r in releases[$KEEP:]:
|
||||
# print(r['version'])
|
||||
# " KEEP=$KEEP)
|
||||
# for ver in $OLD; do
|
||||
# echo "Deleting old release: $ver"
|
||||
# sentry-cli releases delete "$ver" || true
|
||||
# done
|
||||
# env:
|
||||
# SENTRY_URL: https://errors.libnovel.cc
|
||||
# SENTRY_AUTH_TOKEN: ${{ secrets.GLITCHTIP_AUTH_TOKEN }}
|
||||
# SENTRY_ORG: libnovel
|
||||
# SENTRY_PROJECT: ui
|
||||
|
||||
# ── docker: all images in one job (single login) ──────────────────────────────
|
||||
# backend, runner, ui, and caddy are built sequentially in one job so Docker
|
||||
# Hub only needs to be authenticated once. This also eliminates 3 redundant
|
||||
# checkout + setup-buildx + scheduler round-trips compared to separate jobs.
|
||||
# ── docker: build + push all images via docker bake ──────────────────────────
|
||||
docker:
|
||||
name: Docker
|
||||
runs-on: ubuntu-latest
|
||||
@@ -160,143 +65,31 @@ jobs:
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Single login — credential is written to ~/.docker/config.json and
|
||||
# reused by all subsequent build-push-action steps in this job.
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
# ── backend ──────────────────────────────────────────────────────────────
|
||||
- name: Docker meta / backend
|
||||
id: meta-backend
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ secrets.DOCKER_USER }}/libnovel-backend
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=latest
|
||||
|
||||
- name: Build and push / backend
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: backend
|
||||
target: backend
|
||||
push: true
|
||||
tags: ${{ steps.meta-backend.outputs.tags }}
|
||||
labels: ${{ steps.meta-backend.outputs.labels }}
|
||||
build-args: |
|
||||
VERSION=${{ steps.meta-backend.outputs.version }}
|
||||
COMMIT=${{ gitea.sha }}
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_USER }}/libnovel-backend:latest
|
||||
cache-to: type=inline
|
||||
|
||||
# ── runner ───────────────────────────────────────────────────────────────
|
||||
- name: Docker meta / runner
|
||||
id: meta-runner
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ secrets.DOCKER_USER }}/libnovel-runner
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=latest
|
||||
|
||||
- name: Build and push / runner
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: backend
|
||||
target: runner
|
||||
push: true
|
||||
tags: ${{ steps.meta-runner.outputs.tags }}
|
||||
labels: ${{ steps.meta-runner.outputs.labels }}
|
||||
build-args: |
|
||||
VERSION=${{ steps.meta-runner.outputs.version }}
|
||||
COMMIT=${{ gitea.sha }}
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_USER }}/libnovel-runner:latest
|
||||
cache-to: type=inline
|
||||
|
||||
# ── ui ───────────────────────────────────────────────────────────────────
|
||||
- name: Download ui build artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ui-build
|
||||
path: ui/build
|
||||
|
||||
- name: Allow build/ into Docker context (override .dockerignore)
|
||||
- name: Compute version tags
|
||||
id: ver
|
||||
run: |
|
||||
grep -v '^build$' ui/.dockerignore > ui/.dockerignore.tmp
|
||||
mv ui/.dockerignore.tmp ui/.dockerignore
|
||||
V="${{ gitea.ref_name }}"
|
||||
VER="${V#v}"
|
||||
echo "version=$VER" >> "$GITHUB_OUTPUT"
|
||||
echo "major_minor=$(echo "$VER" | cut -d. -f1-2)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Docker meta / ui
|
||||
id: meta-ui
|
||||
uses: docker/metadata-action@v5
|
||||
- name: Build and push all images
|
||||
uses: docker/bake-action@v6
|
||||
with:
|
||||
images: ${{ secrets.DOCKER_USER }}/libnovel-ui
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=latest
|
||||
|
||||
- name: Build and push / ui
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ui
|
||||
push: true
|
||||
tags: ${{ steps.meta-ui.outputs.tags }}
|
||||
labels: ${{ steps.meta-ui.outputs.labels }}
|
||||
build-args: |
|
||||
BUILD_VERSION=${{ steps.meta-ui.outputs.version }}
|
||||
BUILD_COMMIT=${{ gitea.sha }}
|
||||
BUILD_TIME=${{ gitea.event.head_commit.timestamp }}
|
||||
PREBUILT=1
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_USER }}/libnovel-ui:latest
|
||||
cache-to: type=inline
|
||||
|
||||
# ── caddy ────────────────────────────────────────────────────────────────
|
||||
- name: Docker meta / caddy
|
||||
id: meta-caddy
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ secrets.DOCKER_USER }}/libnovel-caddy
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=latest
|
||||
|
||||
- name: Build and push / caddy
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: caddy
|
||||
push: true
|
||||
tags: ${{ steps.meta-caddy.outputs.tags }}
|
||||
labels: ${{ steps.meta-caddy.outputs.labels }}
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_USER }}/libnovel-caddy:latest
|
||||
cache-to: type=inline
|
||||
|
||||
# ── pocketbase ───────────────────────────────────────────────────────────
|
||||
- name: Docker meta / pocketbase
|
||||
id: meta-pocketbase
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ secrets.DOCKER_USER }}/libnovel-pocketbase
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=latest
|
||||
|
||||
- name: Build and push / pocketbase
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: backend
|
||||
target: pocketbase
|
||||
push: true
|
||||
tags: ${{ steps.meta-pocketbase.outputs.tags }}
|
||||
labels: ${{ steps.meta-pocketbase.outputs.labels }}
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_USER }}/libnovel-pocketbase:latest
|
||||
cache-to: type=inline
|
||||
files: docker-bake.hcl
|
||||
set: |
|
||||
*.output=type=image,push=true
|
||||
env:
|
||||
VERSION: ${{ steps.ver.outputs.version }}
|
||||
MAJOR_MINOR: ${{ steps.ver.outputs.major_minor }}
|
||||
COMMIT: ${{ gitea.sha }}
|
||||
BUILD_TIME: ${{ gitea.event.head_commit.timestamp }}
|
||||
|
||||
# ── deploy: sync docker-compose.yml + restart prod ───────────────────────────
|
||||
# Runs after all images are pushed to Docker Hub.
|
||||
@@ -336,7 +129,7 @@ jobs:
|
||||
"${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}" \
|
||||
'set -euo pipefail
|
||||
cd /opt/libnovel
|
||||
doppler run -- docker compose pull
|
||||
doppler run -- docker compose pull backend runner ui caddy pocketbase
|
||||
doppler run -- docker compose up -d --remove-orphans'
|
||||
|
||||
# ── Gitea release ─────────────────────────────────────────────────────────────
|
||||
|
||||
11
CLAUDE.md
11
CLAUDE.md
@@ -2,6 +2,17 @@
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Environment / Secrets
|
||||
|
||||
All project environment variables are stored in **Doppler**. When you need to access any secret or env var (e.g. API tokens, database URLs, credentials), fetch them via:
|
||||
|
||||
```bash
|
||||
doppler run -- <command> # inject all secrets into a command
|
||||
doppler secrets get SECRET_NAME # inspect a specific secret
|
||||
```
|
||||
|
||||
Never use `.env` files. Do not ask the user to provide secrets manually — they are available via Doppler.
|
||||
|
||||
## Commands
|
||||
|
||||
### Docker (via `just` — the primary way to run services)
|
||||
|
||||
106
docker-bake.hcl
Normal file
106
docker-bake.hcl
Normal file
@@ -0,0 +1,106 @@
|
||||
# docker-bake.hcl — defines all five production images.
|
||||
#
|
||||
# CI passes version info as environment variables; locally everything gets :dev tags.
|
||||
#
|
||||
# Local build (no push):
|
||||
# docker buildx bake
|
||||
#
|
||||
# CI environment variables: VERSION, MAJOR_MINOR, COMMIT, BUILD_TIME
|
||||
|
||||
variable "DOCKER_USER" { default = "kalekber" }
|
||||
variable "VERSION" { default = "dev" } # e.g. "4.1.6" (no leading v)
|
||||
variable "MAJOR_MINOR" { default = "dev" } # e.g. "4.1"
|
||||
variable "COMMIT" { default = "unknown" }
|
||||
variable "BUILD_TIME" { default = "" }
|
||||
|
||||
# ── Shared defaults ───────────────────────────────────────────────────────────
|
||||
|
||||
target "_defaults" {
|
||||
pull = true
|
||||
# CI overrides to push=true via --set *.output=type=image,push=true
|
||||
output = ["type=image,push=false"]
|
||||
cache-to = ["type=inline"]
|
||||
}
|
||||
|
||||
# ── Go targets (share the backend/ build context + builder stage) ─────────────
|
||||
|
||||
target "backend" {
|
||||
inherits = ["_defaults"]
|
||||
context = "backend"
|
||||
target = "backend"
|
||||
tags = [
|
||||
"${DOCKER_USER}/libnovel-backend:${VERSION}",
|
||||
"${DOCKER_USER}/libnovel-backend:${MAJOR_MINOR}",
|
||||
"${DOCKER_USER}/libnovel-backend:latest",
|
||||
]
|
||||
cache-from = ["type=registry,ref=${DOCKER_USER}/libnovel-backend:latest"]
|
||||
args = {
|
||||
VERSION = VERSION
|
||||
COMMIT = COMMIT
|
||||
}
|
||||
}
|
||||
|
||||
target "runner" {
|
||||
inherits = ["_defaults"]
|
||||
context = "backend"
|
||||
target = "runner"
|
||||
tags = [
|
||||
"${DOCKER_USER}/libnovel-runner:${VERSION}",
|
||||
"${DOCKER_USER}/libnovel-runner:${MAJOR_MINOR}",
|
||||
"${DOCKER_USER}/libnovel-runner:latest",
|
||||
]
|
||||
cache-from = ["type=registry,ref=${DOCKER_USER}/libnovel-runner:latest"]
|
||||
args = {
|
||||
VERSION = VERSION
|
||||
COMMIT = COMMIT
|
||||
}
|
||||
}
|
||||
|
||||
target "pocketbase" {
|
||||
inherits = ["_defaults"]
|
||||
context = "backend"
|
||||
target = "pocketbase"
|
||||
tags = [
|
||||
"${DOCKER_USER}/libnovel-pocketbase:${VERSION}",
|
||||
"${DOCKER_USER}/libnovel-pocketbase:${MAJOR_MINOR}",
|
||||
"${DOCKER_USER}/libnovel-pocketbase:latest",
|
||||
]
|
||||
cache-from = ["type=registry,ref=${DOCKER_USER}/libnovel-pocketbase:latest"]
|
||||
}
|
||||
|
||||
# ── UI (SvelteKit — separate context) ────────────────────────────────────────
|
||||
|
||||
target "ui" {
|
||||
inherits = ["_defaults"]
|
||||
context = "ui"
|
||||
tags = [
|
||||
"${DOCKER_USER}/libnovel-ui:${VERSION}",
|
||||
"${DOCKER_USER}/libnovel-ui:${MAJOR_MINOR}",
|
||||
"${DOCKER_USER}/libnovel-ui:latest",
|
||||
]
|
||||
cache-from = ["type=registry,ref=${DOCKER_USER}/libnovel-ui:latest"]
|
||||
args = {
|
||||
BUILD_VERSION = VERSION
|
||||
BUILD_COMMIT = COMMIT
|
||||
BUILD_TIME = BUILD_TIME
|
||||
}
|
||||
}
|
||||
|
||||
# ── Caddy (custom plugins — separate context) ─────────────────────────────────
|
||||
|
||||
target "caddy" {
|
||||
inherits = ["_defaults"]
|
||||
context = "caddy"
|
||||
tags = [
|
||||
"${DOCKER_USER}/libnovel-caddy:${VERSION}",
|
||||
"${DOCKER_USER}/libnovel-caddy:${MAJOR_MINOR}",
|
||||
"${DOCKER_USER}/libnovel-caddy:latest",
|
||||
]
|
||||
cache-from = ["type=registry,ref=${DOCKER_USER}/libnovel-caddy:latest"]
|
||||
}
|
||||
|
||||
# ── Default group: all five images ────────────────────────────────────────────
|
||||
|
||||
group "default" {
|
||||
targets = ["backend", "runner", "pocketbase", "ui", "caddy"]
|
||||
}
|
||||
@@ -96,7 +96,7 @@ services:
|
||||
|
||||
# ─── Meilisearch (full-text search) ──────────────────────────────────────────
|
||||
meilisearch:
|
||||
image: getmeili/meilisearch:latest
|
||||
image: getmeili/meilisearch:v1.40.0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MEILI_MASTER_KEY: "${MEILI_MASTER_KEY}"
|
||||
|
||||
2
justfile
2
justfile
@@ -63,7 +63,7 @@ build-push: build push
|
||||
|
||||
# Pull all images from Docker Hub (uses GIT_TAG from Doppler)
|
||||
pull-images:
|
||||
{{doppler}} docker compose pull backend runner ui caddy
|
||||
{{doppler}} docker compose pull backend runner ui caddy pocketbase
|
||||
|
||||
# Pull all third-party base images (minio, pocketbase, etc.)
|
||||
pull-infra:
|
||||
|
||||
@@ -21,11 +21,7 @@ ENV PUBLIC_BUILD_VERSION=$BUILD_VERSION
|
||||
ENV PUBLIC_BUILD_COMMIT=$BUILD_COMMIT
|
||||
ENV PUBLIC_BUILD_TIME=$BUILD_TIME
|
||||
|
||||
# PREBUILT=1 skips npm run build — used in CI when the build/ directory has
|
||||
# already been compiled (and debug IDs injected) by a prior job. The caller
|
||||
# must copy the pre-built build/ into the Docker context before building.
|
||||
ARG PREBUILT=0
|
||||
RUN [ "$PREBUILT" = "1" ] || npm run build
|
||||
RUN npm run build
|
||||
|
||||
# ── Runtime image ──────────────────────────────────────────────────────────────
|
||||
# adapter-node bundles most server-side code, but packages with dynamic
|
||||
|
||||
@@ -16,6 +16,7 @@ export const load: PageServerLoad = async ({ url, locals }) => {
|
||||
const sort = url.searchParams.get('sort') ?? 'popular';
|
||||
const status = url.searchParams.get('status') ?? 'all';
|
||||
const q = url.searchParams.get('q') ?? '';
|
||||
const audioOnly = url.searchParams.get('audio') === '1';
|
||||
|
||||
const params = new URLSearchParams({ page, genre, sort, status });
|
||||
if (q.trim().length >= 2) {
|
||||
@@ -64,7 +65,8 @@ export const load: PageServerLoad = async ({ url, locals }) => {
|
||||
isAdmin: locals.user?.role === 'admin',
|
||||
searchQuery: q.trim().length >= 2 ? q.trim() : '',
|
||||
searchLocalCount: 0,
|
||||
searchRemoteCount: 0
|
||||
searchRemoteCount: 0,
|
||||
audioOnly
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -21,11 +21,13 @@
|
||||
});
|
||||
|
||||
function applyFilters() {
|
||||
filtersOpen = false;
|
||||
const params = new URLSearchParams();
|
||||
params.set('sort', filterSort);
|
||||
params.set('genre', filterGenre);
|
||||
params.set('status', filterStatus);
|
||||
params.set('page', '1');
|
||||
if (filterAudioOnly) params.set('audio', '1');
|
||||
goto(`/catalogue?${params.toString()}`);
|
||||
}
|
||||
|
||||
@@ -215,7 +217,7 @@
|
||||
|
||||
// ── Audio-available set ───────────────────────────────────────────────────
|
||||
let audioSlugs = $state<Set<string>>(new Set());
|
||||
let filterAudioOnly = $state(false);
|
||||
let filterAudioOnly = $state(untrack(() => data.audioOnly));
|
||||
|
||||
$effect(() => {
|
||||
fetch('/api/audio/slugs')
|
||||
@@ -224,6 +226,17 @@
|
||||
.catch(() => { /* non-critical */ });
|
||||
});
|
||||
|
||||
function toggleAudio() {
|
||||
filterAudioOnly = !filterAudioOnly;
|
||||
const u = new URL(window.location.href);
|
||||
if (filterAudioOnly) {
|
||||
u.searchParams.set('audio', '1');
|
||||
} else {
|
||||
u.searchParams.delete('audio');
|
||||
}
|
||||
history.replaceState({}, '', u.toString());
|
||||
}
|
||||
|
||||
const displayedNovels = $derived(
|
||||
filterAudioOnly ? novels.filter((n) => audioSlugs.has(n.slug)) : novels
|
||||
);
|
||||
@@ -249,7 +262,7 @@
|
||||
{m.catalogue_rank_no_data_body()}
|
||||
{/if}
|
||||
{:else}
|
||||
{m.catalogue_browse_source()}
|
||||
{m.catalogue_browse_source()}{#if data.total > 0} <span class="text-(--color-muted) text-xs">{data.total.toLocaleString()} novels</span>{/if}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
@@ -349,7 +362,7 @@
|
||||
<!-- Audio-only filter toggle -->
|
||||
{#if audioSlugs.size > 0}
|
||||
<button
|
||||
onclick={() => (filterAudioOnly = !filterAudioOnly)}
|
||||
onclick={toggleAudio}
|
||||
title="Show only books with audio"
|
||||
class="flex items-center gap-1.5 px-2.5 py-2 rounded border text-sm transition-colors shrink-0
|
||||
{filterAudioOnly
|
||||
@@ -503,7 +516,7 @@
|
||||
{m.catalogue_rank_run_scrape_user()}
|
||||
{/if}
|
||||
{:else if filterAudioOnly}
|
||||
<button onclick={() => (filterAudioOnly = false)} class="text-(--color-brand) hover:underline">Clear audio filter</button>
|
||||
<button onclick={toggleAudio} class="text-(--color-brand) hover:underline">Clear audio filter</button>
|
||||
{:else}
|
||||
{m.catalogue_no_results_filters()}
|
||||
{/if}
|
||||
@@ -531,11 +544,8 @@
|
||||
loading="lazy"
|
||||
/>
|
||||
{:else}
|
||||
<div class="w-full h-full flex items-center justify-center text-(--color-muted)">
|
||||
<svg class="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
<div class="w-full h-full flex items-center justify-center bg-(--color-surface-3)">
|
||||
<span class="text-5xl font-bold text-(--color-muted) select-none opacity-50">{novel.title.charAt(0).toUpperCase()}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if novel.rank}
|
||||
@@ -622,11 +632,8 @@
|
||||
{#if novel.cover}
|
||||
<img src={novel.cover} alt={novel.title} class="w-full h-full object-cover" loading="lazy" />
|
||||
{:else}
|
||||
<div class="w-full h-full flex items-center justify-center text-(--color-muted)">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
<div class="w-full h-full flex items-center justify-center bg-(--color-surface-3)">
|
||||
<span class="text-xl font-bold text-(--color-muted) select-none opacity-50">{novel.title.charAt(0).toUpperCase()}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if isLoading}
|
||||
@@ -688,6 +695,8 @@
|
||||
<span class="text-xs text-emerald-400 font-medium">{m.catalogue_scrape_queued_badge()}</span>
|
||||
{:else if scrapeResult[novel.slug] === 'busy'}
|
||||
<span class="text-xs text-yellow-400 font-medium">{m.catalogue_scrape_busy_list()}</span>
|
||||
{:else if scrapeResult[novel.slug] === 'forbidden'}
|
||||
<span class="text-xs text-(--color-danger) font-medium">{m.catalogue_scrape_forbidden_badge()}</span>
|
||||
{:else if scrapeResult[novel.slug] === 'error'}
|
||||
<span class="text-xs text-(--color-danger) font-medium">{m.common_error()}</span>
|
||||
{:else}
|
||||
|
||||
Reference in New Issue
Block a user