@@ -50,7 +52,7 @@ const isNSFW = comic.md_comics.content_rating === 'erotica';
-
+
{comic.md_comics.title}
diff --git a/src/config/backgrounds.ts b/src/config/backgrounds.ts
index 329fce9..4179bc8 100644
--- a/src/config/backgrounds.ts
+++ b/src/config/backgrounds.ts
@@ -75,3 +75,10 @@ export function getRandomBackground(): BackgroundImage {
}
return backgrounds[Math.floor(Math.random() * backgrounds.length)];
}
+
+export function preloadBackgrounds() {
+ backgrounds.forEach(bg => {
+ const img = new Image();
+ img.src = bg.url;
+ });
+}
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro
index 7c27522..7ce0919 100644
--- a/src/layouts/Layout.astro
+++ b/src/layouts/Layout.astro
@@ -147,11 +147,12 @@ const isCreditsPage = Astro.url.pathname === '/credits';
}
};
- // Initialize theme
- const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null;
- const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
- const initialTheme = savedTheme || (prefersDark ? 'dark' : 'light');
- updateThemeToggle(initialTheme);
+ // Add theme initialization to prevent flash of wrong theme
+ const themeScript = `
+ const theme = localStorage.getItem('theme') ??
+ (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
+ document.documentElement.setAttribute('data-theme', theme);
+ `;
// Event listeners
settingsButton?.addEventListener('click', () => {
diff --git a/src/lib/api.ts b/src/lib/api.ts
index d566cbe..3d482f1 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -69,30 +69,43 @@ export function getGenreNames(genreIds: number[], genres: Genre[]): string[] {
.filter(name => name !== '');
}
+export class APIError extends Error {
+ constructor(
+ message: string,
+ public statusCode?: number,
+ public endpoint?: string
+ ) {
+ super(message);
+ this.name = 'APIError';
+ }
+}
+
export async function fetchComics(
language: string = 'en',
contentType: string = 'sfw',
comicType: ComicType = 'all',
page: number = 1
): Promise
{
+ const apiUrl = `${urlBase}?lang=${language}&page=${page}&order=new&accept_erotic_content=${contentType === 'nsfw'}${comicType === 'all' ? '' : `&type=${comicType}`}`;
+
try {
- const typeParam = comicType === 'all' ? '' : `&type=${comicType}`;
- const apiUrl = `${urlBase}?lang=${language}&page=${page}&order=new&accept_erotic_content=${contentType === 'nsfw'}${typeParam}`;
-
console.log('Fetching comics from:', apiUrl);
const response = await fetchWithRetry(apiUrl);
const data = await response.json();
if (!Array.isArray(data)) {
- console.error('Unexpected API response format:', data);
- return [];
+ throw new APIError('Invalid API response format', undefined, apiUrl);
}
return data;
} catch (error) {
- console.error('Error fetching comics:', error);
- return [];
+ if (error instanceof APIError) throw error;
+ throw new APIError(
+ 'Failed to fetch comics',
+ error instanceof Response ? error.status : undefined,
+ apiUrl
+ );
}
}
diff --git a/src/lib/db.ts b/src/lib/db.ts
index 84a8deb..ffcc509 100644
--- a/src/lib/db.ts
+++ b/src/lib/db.ts
@@ -3,6 +3,8 @@ import type { FeedMapping } from './types';
const REDIS_URL = import.meta.env.REDIS_URL || process.env.REDIS_URL;
let redis: Redis | null = null;
+let redisRetryCount = 0;
+const MAX_RETRIES = 3;
try {
console.log('Environment check:', {
@@ -23,11 +25,14 @@ try {
console.log(`Retry attempt ${times} with delay ${delay}ms`);
return delay;
},
- maxRetriesPerRequest: 3,
+ maxRetriesPerRequest: 2,
enableReadyCheck: true,
- reconnectOnError(err) {
- console.log('Reconnect on error:', err.message);
- return true;
+ reconnectOnError: (err) => {
+ const targetError = 'READONLY';
+ if (err.message.includes(targetError)) {
+ return true;
+ }
+ return false;
}
});
@@ -55,6 +60,38 @@ try {
}
}
+async function getRedisClient(): Promise {
+ if (redis?.status === 'ready') return redis;
+
+ if (redisRetryCount >= MAX_RETRIES) {
+ throw new Error('Failed to connect to Redis after multiple attempts');
+ }
+
+ try {
+ redis = new Redis(REDIS_URL, {
+ retryStrategy(times) {
+ const delay = Math.min(times * 50, 2000);
+ console.log(`Retry attempt ${times} with delay ${delay}ms`);
+ return delay;
+ },
+ maxRetriesPerRequest: 2,
+ enableReadyCheck: true,
+ reconnectOnError: (err) => {
+ const targetError = 'READONLY';
+ if (err.message.includes(targetError)) {
+ return true;
+ }
+ return false;
+ }
+ });
+ redisRetryCount = 0;
+ return redis;
+ } catch (error) {
+ redisRetryCount++;
+ throw error;
+ }
+}
+
export async function storeFeedMapping(feedId: string, slugs: string[], lang: string): Promise {
if (!redis) {
console.error('Redis client not initialized');
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 69b3f03..2970743 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -6,11 +6,9 @@ export interface Comic {
title: string;
slug: string;
genres: number[];
- md_covers: {
- b2key: string;
- }[];
- content_rating: string;
- comic_type: string;
+ md_covers: ComicCover[];
+ content_rating: 'safe' | 'suggestive' | 'erotica';
+ comic_type: ComicType;
};
}
@@ -24,4 +22,10 @@ export interface FeedMapping {
lang: string;
}
-export type ComicType = 'manga' | 'manhwa' | 'manhua' | 'all';
\ No newline at end of file
+export type ComicType = 'manga' | 'manhwa' | 'manhua' | 'all';
+
+export interface ComicCover {
+ b2key: string;
+ width?: number;
+ height?: number;
+}
\ No newline at end of file