From da76aaabcf3c9ab434930759fc53a103e4cc38e4 Mon Sep 17 00:00:00 2001 From: Eduard Smet Date: Sat, 28 Feb 2026 19:28:05 +0100 Subject: [PATCH 1/4] fix(extension-list): Fix cache implementation and clean up IETF language tag mappings --- .../theme/composables/useAvailableData.ts | 126 +--------- src/.vitepress/theme/lib/extensions.ts | 226 ++++-------------- src/.vitepress/theme/lib/languageData.ts | 111 +++++++++ 3 files changed, 171 insertions(+), 292 deletions(-) create mode 100644 src/.vitepress/theme/lib/languageData.ts diff --git a/src/.vitepress/theme/composables/useAvailableData.ts b/src/.vitepress/theme/composables/useAvailableData.ts index 9eb8bf8..9725740 100644 --- a/src/.vitepress/theme/composables/useAvailableData.ts +++ b/src/.vitepress/theme/composables/useAvailableData.ts @@ -3,6 +3,10 @@ import { computed, ref, type Ref } from "vue"; import { normalizeLanguageTag, type Extension } from "../lib/extensions"; +import { + LANGUAGE_EMOJI_MAP, + VALID_IETF_LANGUAGE_TAGS, +} from "../lib/languageData"; export const useAvailableData = (extensions: Ref) => { // Cache for computed values to avoid recalculation @@ -36,67 +40,12 @@ export const useAvailableData = (extensions: Ref) => { if (ext.metadata?.language) { const normalizedLanguage = normalizeLanguageTag(ext.metadata.language); - // Use the complete set of valid IETF language tags from the emoji map - const languageEmojiMap = { - en: "🇬🇧", - zh: "🇨🇳", - hi: "🇮🇳", - es: "🇪🇸", - fr: "🇫🇷", - ar: "🇸🇦", - bn: "🇧🇩", - ru: "🇷🇺", - pt: "🇵🇹", - ur: "🇵🇰", - id: "🇮🇩", - de: "🇩🇪", - ja: "🇯🇵", - sw: "🇰🇪", - mr: "🇮🇳", - te: "🇮🇳", - tr: "🇹🇷", - ta: "🇮🇳", - ko: "🇰🇷", - vi: "🇻🇳", - it: "🇮🇹", - th: "🇹🇭", - gu: "🇮🇳", - fa: "🇮🇷", - pl: "🇵🇱", - uk: "🇺🇦", - ml: "🇮🇳", - kn: "🇮🇳", - or: "🇮🇳", - my: "🇲🇲", - pa: "🇮🇳", - nl: "🇳🇱", - ro: "🇷🇴", - hu: "🇭🇺", - el: "🇬🇷", - cs: "🇨🇿", - sv: "🇸🇪", - fi: "🇫🇮", - da: "🇩🇰", - no: "🇳🇴", - he: "🇮🇱", - sk: "🇸🇰", - bg: "🇧🇬", - hr: "🇭🇷", - sr: "🇷🇸", - lt: "🇱🇹", - sl: "🇸🇮", - et: "🇪🇪", - lv: "🇱🇻", - }; - - const validTags = new Set(Object.keys(languageEmojiMap)); - validTags.add("multi"); // Add multi as a special case - - // Add valid IETF tags, but allow non-matches to show as-is + const validTags = new Set(Object.keys(LANGUAGE_EMOJI_MAP)); + validTags.add("multi"); + if (validTags.has(normalizedLanguage)) { languages.add(normalizedLanguage); } else { - // For non-matching tags, add the original language string as-is if (ext.metadata.language && ext.metadata.language.trim()) { languages.add(ext.metadata.language); } @@ -108,75 +57,18 @@ export const useAvailableData = (extensions: Ref) => { const aLower = a.toLowerCase(); const bLower = b.toLowerCase(); - // Multi always first if (aLower === "multi") return -1; if (bLower === "multi") return 1; - // Define valid IETF language tags - const validIETFTags = new Set([ - "en", - "es", - "fr", - "de", - "it", - "pt", - "ru", - "ja", - "zh", - "ko", - "ar", - "tr", - "pl", - "nl", - "id", - "th", - "vi", - "hi", - "bn", - "ur", - "sw", - "mr", - "te", - "ta", - "gu", - "fa", - "uk", - "ml", - "kn", - "or", - "my", - "pa", - "ro", - "hu", - "el", - "cs", - "sv", - "fi", - "da", - "no", - "he", - "sk", - "bg", - "hr", - "sr", - "lt", - "sl", - "et", - "lv", - ]); - - // Check if A is valid IETF and B is not - const aIsIETF = validIETFTags.has(aLower); - const bIsIETF = validIETFTags.has(bLower); + const aIsIETF = VALID_IETF_LANGUAGE_TAGS.has(aLower); + const bIsIETF = VALID_IETF_LANGUAGE_TAGS.has(bLower); if (aIsIETF && !bIsIETF) return -1; if (!aIsIETF && bIsIETF) return 1; - // If both are IETF or both are non-IETF, sort alphabetically return a.localeCompare(b); }); - // Cache the result cachedLanguages.value = sortedLanguages; lastLanguagesHash.value = currentHash; diff --git a/src/.vitepress/theme/lib/extensions.ts b/src/.vitepress/theme/lib/extensions.ts index 1a26e4d..872de81 100644 --- a/src/.vitepress/theme/lib/extensions.ts +++ b/src/.vitepress/theme/lib/extensions.ts @@ -2,14 +2,11 @@ /* Copyright © 2025 Inkdex */ import { ref } from "vue"; - -export interface GitHubFile { - name: string; - path: string; - type: "file" | "dir"; - html_url: string; - download_url?: string; -} +import { + LANGUAGE_EMOJI_MAP, + LANGUAGE_NAMES_MAP, + VALID_IETF_LANGUAGE_TAGS, +} from "./languageData"; export interface ExtensionMetadata { id: string; @@ -138,29 +135,8 @@ export const getLanguageName = (language: string | null): string => { return "Multi Language"; } - const languageNames: Record = { - en: "English", - es: "Spanish", - fr: "French", - de: "German", - it: "Italian", - pt: "Portuguese", - ru: "Russian", - ja: "Japanese", - zh: "Chinese", - ko: "Korean", - ar: "Arabic", - tr: "Turkish", - pl: "Polish", - nl: "Dutch", - id: "Indonesian", - th: "Thai", - vi: "Vietnamese", - hi: "Hindi", - }; - return ( - languageNames[normalized] || + LANGUAGE_NAMES_MAP[normalized] || normalized.charAt(0).toUpperCase() + normalized.slice(1) ); }; @@ -175,28 +151,7 @@ export const getLanguageEmoji = (language: string | null): string | null => { return "🌐"; } - const emojiMap: Record = { - en: "🇬🇧", - es: "🇪🇸", - fr: "🇫🇷", - de: "🇩🇪", - it: "🇮🇹", - pt: "🇵🇹", - ru: "🇷🇺", - ja: "🇯🇵", - zh: "🇨🇳", - ko: "🇰🇷", - ar: "🇸🇦", - tr: "🇹🇷", - pl: "🇵🇱", - nl: "🇳🇱", - id: "🇮🇩", - th: "🇹🇭", - vi: "🇻🇳", - hi: "🇮🇳", - }; - - return emojiMap[normalized] || null; + return LANGUAGE_EMOJI_MAP[normalized] || null; }; export const buildIconUrl = ( @@ -215,88 +170,65 @@ export const buildBaseUrl = (repo: { return `https://raw.githubusercontent.com/${repo.owner}/${repo.name}/${repo.branch}/0.9/stable`; }; -// Cache for API responses with repo count * minutes TTL -const MINUTE_MS = 60 * 1000; // 1 minute in milliseconds const CACHE_STORAGE_KEY = "inkdex_api_cache"; -interface CacheEntry { - data: any; - fetchTime: number; // When the data was fetched -} - -interface CacheStorage { - [key: string]: CacheEntry; -} - -const loadCache = (): CacheStorage => { - if (typeof localStorage === "undefined") return {}; +const loadFromStorage = (key: string): any | null => { + if (typeof localStorage === "undefined") return null; try { - const stored = localStorage.getItem(CACHE_STORAGE_KEY); - return stored ? JSON.parse(stored) : {}; + const stored = localStorage.getItem(`${CACHE_STORAGE_KEY}:${key}`); + return stored ? JSON.parse(stored) : null; } catch { - return {}; + return null; } }; -const saveCache = (cache: CacheStorage): void => { +const saveToStorage = (key: string, data: any): void => { if (typeof localStorage === "undefined") return; try { - localStorage.setItem(CACHE_STORAGE_KEY, JSON.stringify(cache)); + localStorage.setItem(`${CACHE_STORAGE_KEY}:${key}`, JSON.stringify(data)); } catch (e) { - console.warn("Failed to save cache to localStorage:", e); + console.warn("Failed to save to localStorage:", e); } }; -// Function to get cached data with information about expiration status -const getCachedDataWithStatus = ( - key: string, - allRepos: CustomRepository[] = [], -): { data: any | null; isExpired: boolean } => { - const cache = loadCache(); - const cached = cache[key]; - if (!cached) return { data: null, isExpired: false }; - - // Calculate TTL based on current repo count - const ttlMs = calculateRepoBasedTtl(allRepos); - const isExpired = Date.now() - cached.fetchTime > ttlMs; - - if (isExpired) { - return { data: cached.data, isExpired: true }; // Return expired data but indicate it's expired - } +// Enhanced error handling for API calls +export const fetchVersioningJson = async (repo: { + owner: string; + name: string; + branch: string; +}): Promise<{ sources: ExtensionMetadata[] } | null> => { + const cacheKey = `${repo.owner}/${repo.name}/${repo.branch}`; - return { data: cached.data, isExpired: false }; -}; + // Fetch fresh data from GitHub + const url = `https://raw.githubusercontent.com/${repo.owner}/${repo.name}/${repo.branch}/0.9/stable/versioning.json`; -// Function to get only non-expired cached data -const getCachedData = ( - key: string, - allRepos: CustomRepository[] = [], -): any | null => { - const result = getCachedDataWithStatus(key, allRepos); - if (result.isExpired) { - // Only remove expired cache when explicitly checked for non-expired data - const cache = loadCache(); - delete cache[key]; - saveCache(cache); - return null; - } - return result.data; -}; + try { + const response = await fetch(url); -const setCachedData = (key: string, data: any): void => { - const cache = loadCache(); - cache[key] = { data, fetchTime: Date.now() }; - saveCache(cache); -}; + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + saveToStorage(cacheKey, data); + return data; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + console.error( + `Failed to fetch metadata from ${repo.owner}/${repo.name}:`, + errorMessage, + ); + + // If fetch fails, try to use existing localStorage entry + const fallback = loadFromStorage(cacheKey); + if (fallback) { + console.warn(`Using stored data for ${repo.owner}/${repo.name}`); + return fallback; + } -// Calculate TTL based on repo count in minutes to stay within 60 req/hour limit -const calculateRepoBasedTtl = (allRepos: CustomRepository[]): number => { - // Use the number of repos as cache TTL in minutes to prevent over-fetching - // If there are n repos, cache for n minutes to avoid exceeding 60 req/hour limit - const repoCount = allRepos.length; - // At least 1 minute to prevent spamming GitHub - const minutes = Math.max(repoCount, 1); - return minutes * MINUTE_MS; + return null; + } }; // Capability flags (SourceIntents) @@ -304,7 +236,7 @@ const CAPABILITY_CHAPTER_PROVIDING = 1 << 0; // 1 const CAPABILITY_MANGA_PROGRESS_PROVIDING = 1 << 1; // 2 const CAPABILITY_CLOUDFLARE_BYPASS_PROVIDING = 1 << 4; // 16 -export const hasCapability = ( +const hasCapability = ( capabilities: number | number[], flag: number, ): boolean => { @@ -358,62 +290,6 @@ export const saveCustomRepos = (repos: CustomRepository[]): void => { localStorage.setItem(STORAGE_KEY, JSON.stringify(repos)); }; -// Enhanced error handling for API calls -export const fetchVersioningJson = async ( - repo: { - owner: string; - name: string; - branch: string; - }, - allRepos: CustomRepository[] = [], -): Promise<{ sources: ExtensionMetadata[] } | null> => { - const cacheKey = `versioning:${repo.owner}/${repo.name}/${repo.branch}`; - - // First, check if there's any cached data (expired or not) to use as backup - const expiredCacheResult = getCachedDataWithStatus(cacheKey, allRepos); - const hasExpiredCache = - expiredCacheResult.isExpired && expiredCacheResult.data !== null; - const expiredCacheData = hasExpiredCache ? expiredCacheResult.data : null; - - // Get non-expired cached data (this removes expired cache) - const cached = getCachedData(cacheKey, allRepos); - - // If cache exists (and is not expired), return it - if (cached) { - return cached; - } - - // Cache is expired or doesn't exist, fetch from GitHub - const url = `https://raw.githubusercontent.com/${repo.owner}/${repo.name}/${repo.branch}/0.9/stable/versioning.json`; - - try { - const response = await fetch(url); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - const data = await response.json(); - setCachedData(cacheKey, data); - return data; - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; - console.error( - `Failed to fetch metadata from ${repo.owner}/${repo.name}:`, - errorMessage, - ); - - // If fetch fails and we have expired cache, return it - if (expiredCacheData) { - console.warn(`Using expired cache for ${repo.owner}/${repo.name}`); - return expiredCacheData; - } - - return null; - } -}; - export const checkBranchExists = async ( owner: string, name: string, @@ -475,7 +351,7 @@ export const useExtensions = () => { const allRepos = getAllRepos(); const metadataPromises = allRepos.map(async (repo) => { - const data = await fetchVersioningJson(repo, allRepos); + const data = await fetchVersioningJson(repo); return data ? { repo, data } : null; }); diff --git a/src/.vitepress/theme/lib/languageData.ts b/src/.vitepress/theme/lib/languageData.ts new file mode 100644 index 0000000..7559830 --- /dev/null +++ b/src/.vitepress/theme/lib/languageData.ts @@ -0,0 +1,111 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* Copyright © 2025 Inkdex */ + +export const LANGUAGE_NAMES_MAP: Record = { + en: "English", + es: "Spanish", + fr: "French", + de: "German", + it: "Italian", + pt: "Portuguese", + ru: "Russian", + ja: "Japanese", + zh: "Chinese", + ko: "Korean", + ar: "Arabic", + tr: "Turkish", + pl: "Polish", + nl: "Dutch", + id: "Indonesian", + th: "Thai", + vi: "Vietnamese", + hi: "Hindi", + bn: "Bengali", + ur: "Urdu", + sw: "Swahili", + mr: "Marathi", + te: "Telugu", + ta: "Tamil", + gu: "Gujarati", + fa: "Persian", + uk: "Ukrainian", + ml: "Malayalam", + kn: "Kannada", + or: "Odia", + my: "Burmese", + pa: "Punjabi", + ro: "Romanian", + hu: "Hungarian", + el: "Greek", + cs: "Czech", + sv: "Swedish", + fi: "Finnish", + da: "Danish", + no: "Norwegian", + he: "Hebrew", + sk: "Slovak", + bg: "Bulgarian", + hr: "Croatian", + sr: "Serbian", + lt: "Lithuanian", + sl: "Slovenian", + et: "Estonian", + lv: "Latvian", +}; + +export const LANGUAGE_EMOJI_MAP: Record = { + en: "🇬🇧", + es: "🇪🇸", + fr: "🇫🇷", + de: "🇩🇪", + it: "🇮🇹", + pt: "🇵🇹", + ru: "🇷🇺", + ja: "🇯🇵", + zh: "🇨🇳", + ko: "🇰🇷", + ar: "🇸🇦", + tr: "🇹🇷", + pl: "🇵🇱", + nl: "🇳🇱", + id: "🇮🇩", + th: "🇹🇭", + vi: "🇻🇳", + hi: "🇮🇳", + bn: "🇧🇩", + ur: "🇵🇰", + sw: "🇰🇪", + mr: "🇮🇳", + te: "🇮🇳", + ta: "🇮🇳", + gu: "🇮🇳", + fa: "🇮🇷", + uk: "🇺🇦", + ml: "🇮🇳", + kn: "🇮🇳", + or: "🇮🇳", + my: "🇲🇲", + pa: "🇮🇳", + ro: "🇷🇴", + hu: "🇭🇺", + el: "🇬🇷", + cs: "🇨🇿", + sv: "🇸🇪", + fi: "🇫🇮", + da: "🇩🇰", + no: "🇳🇴", + he: "🇮🇱", + sk: "🇸🇰", + bg: "🇧🇬", + hr: "🇭🇷", + sr: "🇷🇸", + lt: "🇱🇹", + sl: "🇸🇮", + et: "🇪🇪", + lv: "🇱🇻", +}; + +export const VALID_IETF_LANGUAGE_TAGS = new Set([ + ...Object.keys(LANGUAGE_EMOJI_MAP), + "multi", +]); From c9860b62544c89148bb811562b16278efd0610fd Mon Sep 17 00:00:00 2001 From: Eduard Smet Date: Sat, 28 Feb 2026 20:26:34 +0100 Subject: [PATCH 2/4] feat(extension-list): Add a Source button in the extension detials view --- .../theme/components/ExtensionDetails.vue | 64 +++++++++++-- .../theme/components/ExtensionList.css | 33 ++++++- .../theme/components/ExtensionList.vue | 1 + src/.vitepress/theme/lib/extensions.ts | 92 ++++++++++++++++--- 4 files changed, 165 insertions(+), 25 deletions(-) diff --git a/src/.vitepress/theme/components/ExtensionDetails.vue b/src/.vitepress/theme/components/ExtensionDetails.vue index 3d51e94..66f42c4 100644 --- a/src/.vitepress/theme/components/ExtensionDetails.vue +++ b/src/.vitepress/theme/components/ExtensionDetails.vue @@ -2,7 +2,7 @@