diff --git a/server.js b/server.js index 3176e1e..51e821c 100644 --- a/server.js +++ b/server.js @@ -10,6 +10,7 @@ const viewsRoutes = require('./src/views/views.routes'); const animeRoutes = require('./src/anime/anime.routes'); const booksRoutes = require('./src/books/books.routes'); const proxyRoutes = require('./src/shared/proxy/proxy.routes'); +const extensionsRoutes = require('./src/extensions/extensions.routes'); fastify.register(require('@fastify/static'), { root: path.join(__dirname, 'public'), @@ -33,6 +34,7 @@ fastify.register(viewsRoutes); fastify.register(animeRoutes, { prefix: '/api' }); fastify.register(booksRoutes, { prefix: '/api' }); fastify.register(proxyRoutes, { prefix: '/api' }); +fastify.register(extensionsRoutes, { prefix: '/api' }); function startCppScraper() { const exePath = path.join(__dirname, 'src', 'metadata', 'anilist.exe'); diff --git a/src/anime/anime.controller.ts b/src/anime/anime.controller.ts index ee44027..eec2193 100644 --- a/src/anime/anime.controller.ts +++ b/src/anime/anime.controller.ts @@ -1,7 +1,7 @@ import {FastifyReply, FastifyRequest} from 'fastify'; import * as animeService from './anime.service'; -import { getExtension, getExtensionsList } from '../shared/extensions'; -import {AnimeRequest, SearchRequest, ExtensionNameRequest, WatchStreamRequest, Anime} from '../types'; +import {getExtension} from '../shared/extensions'; +import {Anime, AnimeRequest, SearchRequest, WatchStreamRequest} from '../types'; export async function getAnime(req: AnimeRequest, reply: FastifyReply) { try { @@ -18,7 +18,7 @@ export async function getAnime(req: AnimeRequest, reply: FastifyReply) { const results = await animeService.searchAnimeInExtension( ext, extensionName, - id.replaceAll("-", " ") + id.replace(/--/g, '\u0000').replace(/-/g, ' ').replace(new RegExp('\u0000', 'g'), '-').trim() ); anime = results[0] || null; } @@ -29,6 +29,22 @@ export async function getAnime(req: AnimeRequest, reply: FastifyReply) { } } +export async function getAnimeEpisodes(req: AnimeRequest, reply: FastifyReply) { + try { + const { id } = req.params; + const extensionName = req.query.ext || 'anilist'; + const ext = getExtension(extensionName); + + return await animeService.searchChaptersInExtension( + ext, + extensionName, + id.replace(/--/g, '\u0000').replace(/-/g, ' ').replace(new RegExp('\u0000', 'g'), '-').trim() + ); + } catch (err) { + return { error: "Database error" }; + } +} + export async function getTrending(req: FastifyRequest, reply: FastifyReply) { try { const results = await animeService.getTrendingAnime(); @@ -64,25 +80,6 @@ export async function search(req: SearchRequest, reply: FastifyReply) { } } -export async function getExtensions(req: FastifyRequest, reply: FastifyReply) { - return { extensions: getExtensionsList() }; -} - -export async function getExtensionSettings(req: ExtensionNameRequest, reply: FastifyReply) { - const { name } = req.params; - const ext = getExtension(name); - - if (!ext) { - return { error: "Extension not found" }; - } - - if (!ext.getSettings) { - return { episodeServers: ["default"], supportsDub: false }; - } - - return ext.getSettings(); -} - export async function getWatchStream(req: WatchStreamRequest, reply: FastifyReply) { try { const { animeId, episode, server, category, ext } = req.query; @@ -98,7 +95,7 @@ export async function getWatchStream(req: WatchStreamRequest, reply: FastifyRepl const results = await animeService.searchAnimeInExtension( extension, ext, - animeId.replaceAll("-", " ") + animeId.replace(/--/g, '\u0000').replace(/-/g, ' ').replace(new RegExp('\u0000', 'g'), '-').trim() ); anime = results[0]; if (!anime) return { error: "Anime not found in extension search" }; diff --git a/src/anime/anime.routes.ts b/src/anime/anime.routes.ts index 4d9c632..e9da5af 100644 --- a/src/anime/anime.routes.ts +++ b/src/anime/anime.routes.ts @@ -3,11 +3,10 @@ import * as controller from './anime.controller'; async function animeRoutes(fastify: FastifyInstance) { fastify.get('/anime/:id', controller.getAnime); + fastify.get('/anime/:id/:episodes', controller.getAnimeEpisodes); fastify.get('/trending', controller.getTrending); fastify.get('/top-airing', controller.getTopAiring); fastify.get('/search', controller.search); - fastify.get('/extensions', controller.getExtensions); - fastify.get('/extension/:name/settings', controller.getExtensionSettings); fastify.get('/watch/stream', controller.getWatchStream); } diff --git a/src/anime/anime.service.ts b/src/anime/anime.service.ts index 9c40de3..07c8906 100644 --- a/src/anime/anime.service.ts +++ b/src/anime/anime.service.ts @@ -1,6 +1,6 @@ import { queryOne, queryAll } from '../shared/database'; -import { getAllExtensions } from '../shared/extensions'; -import { Anime, Extension, StreamData } from '../types'; +import { getAnimeExtensionsMap } from '../shared/extensions'; +import {Anime, Episode, Extension, StreamData} from '../types'; export async function getAnimeById(id: string | number): Promise { const row = await queryOne("SELECT full_data FROM anime WHERE id = ?", [id]); @@ -47,16 +47,12 @@ export async function searchAnimeLocal(query: string): Promise { return cleanResults.slice(0, 10); } -export async function searchAnimeInExtension( - ext: Extension | null, - name: string, - query: string -): Promise { +export async function searchAnimeInExtension(ext: Extension | null, name: string, query: string): Promise { if (!ext) return []; - if ((ext.type === 'anime-board') && ext.search) { + if (ext.type === 'anime-board' && ext.search) { try { - console.log(`[${name}] Searching for book: ${query}`); + console.log(`[${name}] Searching for anime: ${query}`); const matches = await ext.search({ query: query, media: { @@ -86,10 +82,47 @@ export async function searchAnimeInExtension( return []; } -export async function searchAnimeExtensions(query: string): Promise { - const extensions = getAllExtensions(); +export async function searchChaptersInExtension(ext: Extension | null, name: string, query: string): Promise { + if (!ext) return []; - for (const [name, ext] of extensions) { + if (ext.type === "anime-board" && ext.search && typeof ext.findEpisodes === "function") { + try { + + const matches = await ext.search({ + query, + media: { + romajiTitle: query, + englishTitle: query, + startDate: { year: 0, month: 0, day: 0 } + } + }); + + if (!matches || matches.length === 0) return []; + + const res = matches[0]; + if (!res?.id) return []; + + const chapterList = await ext.findEpisodes(res.id); + if (!Array.isArray(chapterList)) return []; + + return chapterList.map(ep => ({ + id: ep.id, + number: ep.number, + url: ep.url, + title: ep.title + })); + } catch (e) { + console.error(`Extension search failed for ${name}:`, e); + } + } + + return []; +} + +export async function searchAnimeExtensions(query: string): Promise { + const animeExtensions = getAnimeExtensionsMap(); + + for (const [name, ext] of animeExtensions) { const results = await searchAnimeInExtension(ext, name, query); if (results.length > 0) return results; } @@ -97,13 +130,7 @@ export async function searchAnimeExtensions(query: string): Promise { return []; } -export async function getStreamData( - extension: Extension, - animeData: Anime, - episode: string, - server?: string, - category?: string -): Promise { +export async function getStreamData(extension: Extension, animeData: Anime, episode: string, server?: string, category?: string): Promise { const searchOptions = { query: animeData.title.english || animeData.title.romaji, dub: category === 'dub', diff --git a/src/books/books.controller.ts b/src/books/books.controller.ts index 32dae8f..8980e2c 100644 --- a/src/books/books.controller.ts +++ b/src/books/books.controller.ts @@ -15,7 +15,7 @@ export async function getBook(req: BookRequest, reply: FastifyReply) { const extensionName = source; const ext = getExtension(extensionName); - const results = await booksService.searchBooksInExtension(ext, extensionName, id.replaceAll("-", " ")); + const results = await booksService.searchBooksInExtension(ext, extensionName, id.replace(/--/g, '\u0000').replace(/-/g, ' ').replace(new RegExp('\u0000', 'g'), '-').trim()); book = results[0] || null; } diff --git a/src/books/books.service.ts b/src/books/books.service.ts index 605b9dd..55b6a30 100644 --- a/src/books/books.service.ts +++ b/src/books/books.service.ts @@ -1,5 +1,5 @@ import { queryOne, queryAll } from '../shared/database'; -import { getAllExtensions } from '../shared/extensions'; +import { getAllExtensions, getBookExtensionsMap } from '../shared/extensions'; import { Book, Extension, ChapterWithProvider, ChapterContent } from '../types'; export async function getBookById(id: string | number): Promise { @@ -144,9 +144,9 @@ export async function searchBooksInExtension(ext: Extension | null, name: string } export async function searchBooksExtensions(query: string): Promise { - const extensions = getAllExtensions(); + const bookExtensions = getBookExtensionsMap(); - for (const [name, ext] of extensions) { + for (const [name, ext] of bookExtensions) { const results = await searchBooksInExtension(ext, name, query); if (results.length > 0) return results; } @@ -154,12 +154,80 @@ export async function searchBooksExtensions(query: string): Promise { return []; } +async function fetchBookMetadata(id: string): Promise { + try { + const query = `query ($id: Int) { + Media(id: $id, type: MANGA) { + title { romaji english } + startDate { year month day } + } + }`; + + const res = await fetch('https://graphql.anilist.co', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query, variables: { id: parseInt(id) } }) + }); + + const d = await res.json(); + return d.data?.Media || null; + } catch (e) { + console.error("Failed to fetch book metadata:", e); + return null; + } +} + +async function searchChaptersInExtension(ext: Extension, name: string, searchTitle: string, bookData: Book | null): Promise { + try { + console.log(`[${name}] Searching chapters for: ${searchTitle}`); + + const matches = await ext.search!({ + query: searchTitle, + media: bookData ? { + romajiTitle: bookData.title.romaji, + englishTitle: bookData.title.english || "", + startDate: bookData.startDate || { year: 0, month: 0, day: 0 } + } : { + romajiTitle: searchTitle, + englishTitle: searchTitle, + startDate: { year: 0, month: 0, day: 0 } + } + }); + + if (!matches?.length) { + console.log(`[${name}] No matches found for book.`); + return []; + } + + const best = matches[0]; + const chaps = await ext.findChapters!(best.id); + + if (!chaps?.length) { + return []; + } + + console.log(`[${name}] Found ${chaps.length} chapters.`); + + return chaps.map((ch) => ({ + id: ch.id, + number: parseFloat(ch.number.toString()), + title: ch.title, + date: ch.releaseDate, + provider: name + })); + } catch (e) { + const error = e as Error; + console.error(`Failed to fetch chapters from ${name}:`, error.message); + return []; + } +} + export async function getChaptersForBook(id: string): Promise<{ chapters: ChapterWithProvider[] }> { let bookData: Book | null = null; let searchTitle: string | null = null; - if (typeof id === "string" && isNaN(Number(id))) { - searchTitle = id.replaceAll("-", " "); + if (isNaN(Number(id))) { + searchTitle = id.replace(/--/g, '\u0000').replace(/-/g, ' ').replace(new RegExp('\u0000', 'g'), '-').trim(); } else { const result = await getBookById(id); if (!('error' in result)) { @@ -167,80 +235,32 @@ export async function getChaptersForBook(id: string): Promise<{ chapters: Chapte } if (!bookData) { - try { - const query = `query ($id: Int) { - Media(id: $id, type: MANGA) { - title { romaji english } - startDate { year month day } - } - }`; - - const res = await fetch('https://graphql.anilist.co', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query, variables: { id: parseInt(id) } }) - }); - - const d = await res.json(); - if (d.data?.Media) bookData = d.data.Media; - } catch (e) { } + bookData = await fetchBookMetadata(id); } - if (!bookData) return { chapters: [] }; + if (!bookData) { + return { chapters: [] }; + } const titles = [bookData.title.english, bookData.title.romaji].filter(Boolean) as string[]; searchTitle = titles[0]; } const allChapters: ChapterWithProvider[] = []; - const extensions = getAllExtensions(); + const bookExtensions = getBookExtensionsMap(); - const searchPromises = Array.from(extensions.entries()) - .filter(([_, ext]) => - (ext.type === 'book-board' || ext.type === 'manga-board') && - ext.search && ext.findChapters - ) + const searchPromises = Array.from(bookExtensions.entries()) + .filter(([_, ext]) => ext.search && ext.findChapters) .map(async ([name, ext]) => { - try { - console.log(`[${name}] Searching chapters for: ${searchTitle}`); - - const matches = await ext.search!({ - query: searchTitle!, - media: bookData ? { - romajiTitle: bookData.title.romaji, - englishTitle: bookData.title.english || "", - startDate: bookData.startDate || { year: 0, month: 0, day: 0 } - } : { romajiTitle: searchTitle!, englishTitle: searchTitle!, startDate: { year: 0, month: 0, day: 0 } } - }); - - if (matches?.length) { - const best = matches[0]; - const chaps = await ext.findChapters!(best.id); - - if (chaps?.length) { - console.log(`[${name}] Found ${chaps.length} chapters.`); - chaps.forEach((ch: { id: any; number: { toString: () => string; }; title: any; releaseDate: any; }) => { - allChapters.push({ - id: ch.id, - number: parseFloat(ch.number.toString()), - title: ch.title, - date: ch.releaseDate, - provider: name - }); - }); - } - } else { - console.log(`[${name}] No matches found for book.`); - } - } catch (e) { - const error = e as Error; - console.error(`Failed to fetch chapters from ${name}:`, error.message); - } + const chapters = await searchChaptersInExtension(ext, name, searchTitle!, bookData); + allChapters.push(...chapters); }); await Promise.all(searchPromises); - return { chapters: allChapters.sort((a, b) => Number(a.number) - Number(b.number)) }; + return { + chapters: allChapters.sort((a, b) => Number(a.number) - Number(b.number)) + }; } export async function getChapterContent(bookId: string, chapterIndex: string, providerName: string): Promise { diff --git a/src/extensions/extensions.controller.ts b/src/extensions/extensions.controller.ts new file mode 100644 index 0000000..3f5500c --- /dev/null +++ b/src/extensions/extensions.controller.ts @@ -0,0 +1,32 @@ +import { FastifyReply, FastifyRequest } from 'fastify'; +import { getExtension, getExtensionsList, getAllExtensions, getBookExtensionsMap, getAnimeExtensionsMap } from '../shared/extensions'; +import { ExtensionNameRequest } from '../types'; + +export async function getExtensions(req: FastifyRequest, reply: FastifyReply) { + return { extensions: getExtensionsList() }; +} + +export async function getAnimeExtensions(req: FastifyRequest, reply: FastifyReply) { + const animeExtensions = getAnimeExtensionsMap(); + return { extensions: Array.from(animeExtensions.keys()) }; +} + +export async function getBookExtensions(req: FastifyRequest, reply: FastifyReply) { + const bookExtensions = getBookExtensionsMap(); + return { extensions: Array.from(bookExtensions.keys()) }; +} + +export async function getExtensionSettings(req: ExtensionNameRequest, reply: FastifyReply) { + const { name } = req.params; + const ext = getExtension(name); + + if (!ext) { + return { error: "Extension not found" }; + } + + if (!ext.getSettings) { + return { episodeServers: ["default"], supportsDub: false }; + } + + return ext.getSettings(); +} \ No newline at end of file diff --git a/src/extensions/extensions.routes.ts b/src/extensions/extensions.routes.ts new file mode 100644 index 0000000..44dc385 --- /dev/null +++ b/src/extensions/extensions.routes.ts @@ -0,0 +1,11 @@ +import { FastifyInstance } from 'fastify'; +import * as controller from './extensions.controller'; + +async function extensionsRoutes(fastify: FastifyInstance) { + fastify.get('/extensions', controller.getExtensions); + fastify.get('/extensions/anime', controller.getAnimeExtensions); + fastify.get('/extensions/book', controller.getBookExtensions); + fastify.get('/extensions/:name/settings', controller.getExtensionSettings); +} + +export default extensionsRoutes; \ No newline at end of file diff --git a/src/scripts/anime/anime.js b/src/scripts/anime/anime.js index 7f7817d..ebc720c 100644 --- a/src/scripts/anime/anime.js +++ b/src/scripts/anime/anime.js @@ -134,12 +134,25 @@ async function loadAnime() { } } - if (data.nextAiringEpisode?.episode) { - totalEpisodes = data.nextAiringEpisode.episode - 1; - } else if (data.episodes) { - totalEpisodes = data.episodes; + let extensionEpisodes = []; + + if (extensionName) { + extensionEpisodes = await loadExtensionEpisodes(animeId, extensionName); + + if (extensionEpisodes.length > 0) { + totalEpisodes = extensionEpisodes.length; + } else { + totalEpisodes = 1; + } } else { - totalEpisodes = 12; + // MODO NORMAL (AniList) + if (data.nextAiringEpisode?.episode) { + totalEpisodes = data.nextAiringEpisode.episode - 1; + } else if (data.episodes) { + totalEpisodes = data.episodes; + } else { + totalEpisodes = 12; + } } totalEpisodes = Math.min(Math.max(totalEpisodes, 1), 5000); @@ -153,6 +166,27 @@ async function loadAnime() { } } +async function loadExtensionEpisodes(animeId, extName) { + try { + const url = `/api/anime/${animeId}/episodes?ext=${extName}`; + const res = await fetch(url); + const data = await res.json(); + + if (!Array.isArray(data)) return []; + + return data.map(ep => ({ + id: ep.id, + number: ep.number, + title: ep.title || `Episode ${ep.number}`, + url: ep.url + })); + } catch (err) { + console.error("Failed to fetch extension episodes:", err); + return []; + } +} + + function handleDescription(text) { const tmp = document.createElement("DIV"); tmp.innerHTML = text; diff --git a/src/scripts/anime/animes.js b/src/scripts/anime/animes.js index d355984..637efd2 100644 --- a/src/scripts/anime/animes.js +++ b/src/scripts/anime/animes.js @@ -34,12 +34,11 @@ async function fetchSearh(query) { function createSlug(text) { if (!text) return ''; return text - .replace(/([a-z])([A-Z])/g, '$1 $2') // separa CamelCase - .replace(/([a-z])(\d)/g, '$1 $2') // separa letras de números .toLowerCase() .trim() - .replace(/[^a-z0-9\s-]/g, '') - .replace(/[\s-]+/g, '-'); + .replace(/-/g, '--') + .replace(/\s+/g, '-') + .replace(/[^a-z0-9\-]/g, ''); } function renderSearchResults(results) { diff --git a/src/scripts/anime/player.js b/src/scripts/anime/player.js index 8db8949..8220721 100644 --- a/src/scripts/anime/player.js +++ b/src/scripts/anime/player.js @@ -6,6 +6,7 @@ let audioMode = 'sub'; let currentExtension = ''; let plyrInstance; let hlsInstance; +let totalEpisodes = 0; const params = new URLSearchParams(window.location.search); const firstKey = params.keys().next().value; @@ -21,42 +22,130 @@ document.getElementById('episode-label').innerText = `Episode ${currentEpisode}` async function loadMetadata() { try { - const res = await fetch(`/api/anime/${animeId}`); + const extQuery = extName ? `?ext=${extName}` : ""; + + const res = await fetch(`/api/anime/${animeId}${extQuery}`); const data = await res.json(); - if(!data.error) { - const title = data.title.english || data.title.romaji; - document.getElementById('anime-title').innerText = title; - document.title = `Watching ${title} - Ep ${currentEpisode}`; + + if (!data.error) { + const romajiTitle = data.title.romaji || data.title.english || 'Anime Title'; + + document.getElementById('anime-title').innerText = romajiTitle; + document.title = `Watching ${romajiTitle} - Ep ${currentEpisode}`; + + document.getElementById('detail-anime-title').innerText = romajiTitle; + + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = data.description || 'No description available.'; + document.getElementById('detail-description').innerText = + tempDiv.textContent || tempDiv.innerText; + + document.getElementById('detail-format').innerText = data.format || '--'; + document.getElementById('detail-score').innerText = + data.averageScore ? `${data.averageScore}%` : '--'; + + const season = data.season + ? data.season.charAt(0) + data.season.slice(1).toLowerCase() + : ''; + document.getElementById('detail-season').innerText = + data.seasonYear ? `${season} ${data.seasonYear}` : '--'; + + document.getElementById('detail-cover-image').src = + data.coverImage.large || data.coverImage.medium || ''; + + if (!extName) { + + totalEpisodes = data.episodes || 0; + } else { + + try { + const res2 = await fetch(`/api/anime/${animeId}/episodes${extQuery}`); + const data2 = await res2.json(); + totalEpisodes = Array.isArray(data2) ? data2.length : 0; + } catch (e) { + console.error("Error cargando episodios por extensión:", e); + totalEpisodes = 0; + } + } + + populateEpisodeSelectors(totalEpisodes); + + if (currentEpisode >= totalEpisodes && totalEpisodes > 0) { + document.getElementById('next-btn').disabled = true; + } } - } catch(e) { console.error(e); } + } catch (error) { + console.error('Error loading metadata:', error); + } +} + +function populateEpisodeSelectors(count) { + const list = document.getElementById('episode-list'); + list.innerHTML = ''; + + for (let i = 1; i <= count; i++) { + const extParam = extName ? `?${extName}` : ""; + + const btn = document.createElement('a'); + btn.href = `/watch/${animeId}/${i}${extParam}`; + btn.classList.add('episode-btn'); + btn.dataset.episode = i; + btn.innerText = i; + + if (i === currentEpisode) { + btn.classList.add('active-ep'); + } + + list.appendChild(btn); + } +} + +function filterEpisodes() { + const searchInput = document.getElementById('episode-search'); + const filter = searchInput.value.toUpperCase().trim(); + const episodeList = document.getElementById('episode-list'); + const buttons = episodeList.getElementsByClassName('episode-btn'); + + for (let i = 0; i < buttons.length; i++) { + const episodeNumber = buttons[i].dataset.episode; + + if (episodeNumber.startsWith(filter) || filter === "") { + buttons[i].style.display = ""; + } else { + buttons[i].style.display = "none"; + } + } } async function loadExtensions() { try { - const res = await fetch('/api/extensions'); + const res = await fetch('/api/extensions/anime'); const data = await res.json(); const select = document.getElementById('extension-select'); if (data.extensions && data.extensions.length > 0) { + select.innerHTML = ''; data.extensions.forEach(ext => { const opt = document.createElement('option'); opt.value = opt.innerText = ext; select.appendChild(opt); }); - if (data.extensions.includes(extName ?? "")) { + if (typeof extName === 'string' && data.extensions.includes(extName)) { select.value = extName; - currentExtension = extName; - onExtensionChange(); + } else { + select.selectedIndex = 0; } + + currentExtension = select.value; + onExtensionChange(); } else { select.innerHTML = ''; select.disabled = true; - setLoading("No extensions found in WaifuBoards folder."); + setLoading("No anime extensions found."); } - - } catch(e) { - console.error("Extension Error:", e); + } catch (error) { + console.error("Extension Error:", error); } } @@ -66,7 +155,7 @@ async function onExtensionChange() { setLoading("Fetching extension settings..."); try { - const res = await fetch(`/api/extension/${currentExtension}/settings`); + const res = await fetch(`/api/extensions/${currentExtension}/settings`); const settings = await res.json(); const toggle = document.getElementById('sd-toggle'); @@ -93,9 +182,8 @@ async function onExtensionChange() { } loadStream(); - - } catch (err) { - console.error(err); + } catch (error) { + console.error(error); setLoading("Failed to load extension settings."); } } @@ -113,6 +201,7 @@ function setAudioMode(mode) { const dubOpt = document.getElementById('opt-dub'); toggle.setAttribute('data-state', mode); + if (mode === 'sub') { subOpt.classList.add('active'); dubOpt.classList.remove('active'); @@ -128,7 +217,7 @@ async function loadStream() { const serverSelect = document.getElementById('server-select'); const server = serverSelect.value || "default"; - setLoading(`Searching & Resolving Stream (${audioMode})...`); + setLoading(`Loading stream (${audioMode})...`); try { const url = `/api/watch/stream?animeId=${animeId.slice(0, 30)}&episode=${currentEpisode}&server=${server}&category=${audioMode}&ext=${currentExtension}`; @@ -146,21 +235,18 @@ async function loadStream() { } const source = data.videoSources.find(s => s.type === 'm3u8') || data.videoSources[0]; - const headers = data.headers || {}; - let proxyUrl = `/api/proxy?url=${encodeURIComponent(source.url)}`; + let proxyUrl = `/api/proxy?url=${encodeURIComponent(source.url)}`; if (headers['Referer']) proxyUrl += `&referer=${encodeURIComponent(headers['Referer'])}`; if (headers['Origin']) proxyUrl += `&origin=${encodeURIComponent(headers['Origin'])}`; if (headers['User-Agent']) proxyUrl += `&userAgent=${encodeURIComponent(headers['User-Agent'])}`; playVideo(proxyUrl, data.videoSources[0].subtitles); - document.getElementById('loading-overlay').style.display = 'none'; - - } catch (err) { - setLoading("Stream Error. Check console."); - console.error(err); + } catch (error) { + setLoading("Stream error. Check console."); + console.error(error); } } @@ -169,6 +255,7 @@ function playVideo(url, subtitles) { if (Hls.isSupported()) { if (hlsInstance) hlsInstance.destroy(); + hlsInstance = new Hls({ xhrSetup: (xhr, url) => { xhr.withCredentials = false; @@ -193,30 +280,52 @@ function playVideo(url, subtitles) { track.label = sub.language; track.srclang = sub.language.slice(0, 2).toLowerCase(); track.src = sub.url; + if (sub.default || sub.language.toLowerCase().includes('english')) { track.default = true; } + video.appendChild(track); }); } plyrInstance = new Plyr(video, { - captions: { active: true, update: true, language: 'en' }, - controls: ['play-large', 'play', 'progress', 'current-time', 'duration', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'], + captions: { + active: true, + update: true, + language: 'en' + }, + controls: [ + 'play-large', + 'play', + 'progress', + 'current-time', + 'duration', + 'mute', + 'volume', + 'captions', + 'settings', + 'pip', + 'airplay', + 'fullscreen' + ], settings: ['captions', 'quality', 'speed'] }); - video.play().catch(e => console.log("Auto-play blocked")); + video.play().catch(error => { + console.log("Autoplay blocked:", error); + }); } -function setLoading(msg) { +function setLoading(message) { const overlay = document.getElementById('loading-overlay'); const text = document.getElementById('loading-text'); overlay.style.display = 'flex'; - text.innerText = msg; + text.innerText = message; } const extParam = extName ? `?${extName}` : ""; + document.getElementById('prev-btn').onclick = () => { if (currentEpisode > 1) { window.location.href = `/watch/${animeId}/${currentEpisode - 1}${extParam}`; @@ -226,7 +335,10 @@ document.getElementById('prev-btn').onclick = () => { document.getElementById('next-btn').onclick = () => { window.location.href = `/watch/${animeId}/${currentEpisode + 1}${extParam}`; }; -if(currentEpisode <= 1) document.getElementById('prev-btn').disabled = true; + +if (currentEpisode <= 1) { + document.getElementById('prev-btn').disabled = true; +} loadMetadata(); loadExtensions(); \ No newline at end of file diff --git a/src/scripts/books/book.js b/src/scripts/books/book.js index b9377ab..eece63a 100644 --- a/src/scripts/books/book.js +++ b/src/scripts/books/book.js @@ -104,7 +104,6 @@ async function loadChapters() { ? `/api/book/${bookId.slice(0, 40)}/chapters` : `/api/book/${bookId}/chapters`; - console.log(fetchUrl) const res = await fetch(fetchUrl); const data = await res.json(); diff --git a/src/scripts/books/books.js b/src/scripts/books/books.js index 47c2059..4f5d2f7 100644 --- a/src/scripts/books/books.js +++ b/src/scripts/books/books.js @@ -49,11 +49,11 @@ async function fetchBookSearch(query) { function createSlug(text) { if (!text) return ''; return text - .toString() .toLowerCase() .trim() - .replace(/[^a-z0-9\s-]/g, '') - .replace(/[\s-]+/g, '-'); + .replace(/-/g, '--') + .replace(/\s+/g, '-') + .replace(/[^a-z0-9\-]/g, ''); } function renderSearchResults(results) { diff --git a/src/shared/extensions.js b/src/shared/extensions.js index 7ced629..b5fd166 100644 --- a/src/shared/extensions.js +++ b/src/shared/extensions.js @@ -57,9 +57,31 @@ function getExtensionsList() { return Array.from(extensions.keys()); } +function getAnimeExtensionsMap() { + const animeExts = new Map(); + for (const [name, ext] of extensions) { + if (ext.type === 'anime-board') { + animeExts.set(name, ext); + } + } + return animeExts; +} + +function getBookExtensionsMap() { + const bookExts = new Map(); + for (const [name, ext] of extensions) { + if (ext.type === 'book-board' || ext.type === 'manga-board') { + bookExts.set(name, ext); + } + } + return bookExts; +} + module.exports = { loadExtensions, getExtension, getAllExtensions, - getExtensionsList + getExtensionsList, + getAnimeExtensionsMap, + getBookExtensionsMap }; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index b0751ce..deb7fe4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -69,6 +69,7 @@ export interface ExtensionSearchResult { } export interface Episode { + url: string; id: string; number: number; title?: string; diff --git a/views/css/anime/watch.css b/views/css/anime/watch.css index 57d1e12..5539c27 100644 --- a/views/css/anime/watch.css +++ b/views/css/anime/watch.css @@ -1,152 +1,175 @@ :root { - --bg-base: #000000; - --bg-overlay: #101012; - --accent: #8b5cf6; - --accent-dark: #7c3aed; - --text-primary: #ffffff; - --text-secondary: #a1a1aa; + + --color-bg-base: #000000; + --color-bg-elevated: #0a0a0b; + --color-bg-card: #121214; + --color-primary: #8b5cf6; + --color-primary-hover: #7c3aed; + --color-primary-glow: rgba(139, 92, 246, 0.4); + --color-text-primary: #ffffff; + --color-text-secondary: #a1a1aa; + --color-text-muted: #71717a; + + --border-subtle: 1px solid rgba(255, 255, 255, 0.08); + --border-medium: 1px solid rgba(255, 255, 255, 0.12); + --glass-bg: rgba(18, 18, 20, 0.8); + --glass-border: rgba(255, 255, 255, 0.1); + + --spacing-xs: 0.5rem; + --spacing-sm: 0.75rem; + --spacing-md: 1rem; + --spacing-lg: 1.5rem; + --spacing-xl: 2rem; + --spacing-2xl: 3rem; + + --radius-sm: 0.5rem; + --radius-md: 1rem; + --radius-lg: 1.25rem; + --radius-xl: 1.5rem; --radius-full: 9999px; - --radius-md: 16px; - --glass-border: 1px solid rgba(255, 255, 255, 0.1); - --glass-bg: rgba(20, 20, 23, 0.7); + --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3); + --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.5); + --shadow-glow: 0 8px 32px var(--color-primary-glow); - --plyr-color-main: #8b5cf6; - --plyr-video-control-color: #ffffff; - --plyr-video-control-background-hover: rgba(255, 255, 255, 0.1); - --plyr-menu-background: rgba(28, 28, 30, 0.95); - --plyr-menu-color: #ffffff; - --plyr-menu-border-color: rgba(255, 255, 255, 0.1); - --plyr-font-family: 'Inter', sans-serif; + --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-smooth: 350ms cubic-bezier(0.4, 0, 0.2, 1); + --plyr-color-main: var(--color-primary); + --plyr-video-control-color: var(--color-text-primary); + --plyr-video-control-background-hover: rgba(255, 255, 255, 0.12); + --plyr-menu-background: rgba(18, 18, 20, 0.95); + --plyr-menu-color: var(--color-text-primary); + --plyr-menu-border-color: var(--glass-border); +} - --plyr-control-icon-size: 18px; - --plyr-control-spacing: 10px; +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; } body { margin: 0; - background-color: var(--bg-base); - color: var(--text-primary); + background-color: var(--color-bg-base); + color: var(--color-text-primary); font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - height: 100vh; - overflow: hidden; - display: flex; - flex-direction: column; + line-height: 1.6; + overflow-x: hidden; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.ui-scale { + transform: scale(0.90); + transform-origin: top center; } .top-bar { - padding: 1.5rem 2rem; - display: flex; - justify-content: space-between; - align-items: center; position: fixed; - top: 0; left: 0; right: 0; - z-index: 100; - background: linear-gradient(to bottom, rgba(0,0,0,0.8) 0%, transparent 100%); + top: 0; + left: 0; + right: 0; + padding: var(--spacing-lg) var(--spacing-xl); + background: linear-gradient(180deg, rgba(0, 0, 0, 0.8) 0%, transparent 100%); + z-index: 1000; pointer-events: none; } .back-btn { pointer-events: auto; - display: flex; + display: inline-flex; align-items: center; - gap: 0.6rem; - padding: 0.8rem 1.8rem; + gap: var(--spacing-sm); + padding: 0.7rem 1.5rem; background: var(--glass-bg); - backdrop-filter: blur(12px); - border: var(--glass-border); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid var(--glass-border); border-radius: var(--radius-full); - color: white; + color: var(--color-text-primary); text-decoration: none; font-weight: 600; - font-size: 0.95rem; - transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1); - box-shadow: 0 4px 20px rgba(0,0,0,0.2); + font-size: 0.9rem; + transition: all var(--transition-smooth); + box-shadow: var(--shadow-sm); } .back-btn:hover { - background: rgba(255, 255, 255, 0.15); - transform: scale(1.02); - box-shadow: 0 0 15px rgba(139, 92, 246, 0.3); - border-color: rgba(139, 92, 246, 0.3); + background: rgba(255, 255, 255, 0.12); + border-color: var(--color-primary); + transform: translateY(-2px); + box-shadow: var(--shadow-glow); } -.theater-container { - flex: 1; +.watch-container { + max-width: 1800px; + margin: 0 auto; + padding: 1rem var(--spacing-xl) var(--spacing-xl); + display: grid; + grid-template-columns: 1fr 420px; + gap: var(--spacing-xl); + align-items: start; +} + +.watch-container.sidebar-hidden { + grid-template-columns: 1fr; +} + +.player-section { display: flex; flex-direction: column; - justify-content: center; - align-items: center; - width: 100%; - max-width: 1300px; - margin: 0 auto; - padding: 0 2rem; - position: relative; - z-index: 10; + gap: var(--spacing-lg); } .player-toolbar { - width: 100%; display: flex; - justify-content: flex-end; align-items: center; - gap: 1rem; - margin-bottom: 1rem; - position: relative; - z-index: 50; + gap: var(--spacing-md); + flex-wrap: wrap; + background: var(--glass-bg); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid var(--glass-border); + border-radius: var(--radius-lg); + padding: var(--spacing-md); + box-shadow: var(--shadow-sm); } -.extension-select { - appearance: none; - -webkit-appearance: none; - background-color: var(--glass-bg); - backdrop-filter: blur(12px); - border: var(--glass-border); - color: var(--text-primary); - padding: 0.7rem 2.5rem 0.7rem 1.5rem; - border-radius: var(--radius-full); - font-size: 0.9rem; - font-weight: 500; - cursor: pointer; - outline: none; - min-width: 180px; - transition: all 0.2s ease; - background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 1rem center; - box-shadow: 0 4px 10px rgba(0,0,0,0.3); -} - -.extension-select:hover { - background-color: rgba(255,255,255,0.1); - border-color: var(--accent); +.control-group { + display: flex; + align-items: center; + gap: var(--spacing-md); } .sd-toggle { display: flex; - background: var(--glass-bg); - border: var(--glass-border); + background: var(--color-bg-elevated); + border: var(--border-subtle); border-radius: var(--radius-full); padding: 4px; position: relative; cursor: pointer; - backdrop-filter: blur(12px); } .sd-option { - padding: 0.5rem 1.2rem; - font-size: 0.85rem; + padding: 0.6rem 1.5rem; + font-size: 0.875rem; font-weight: 700; - color: var(--text-secondary); + color: var(--color-text-muted); z-index: 2; - transition: color 0.3s; + transition: color var(--transition-base); text-transform: uppercase; - letter-spacing: 0.5px; + letter-spacing: 0.05em; + position: relative; } .sd-option.active { - color: white; + color: var(--color-text-primary); } .sd-bg { @@ -155,10 +178,10 @@ body { left: 4px; bottom: 4px; width: calc(50% - 4px); - background: var(--accent); + background: var(--color-primary); border-radius: var(--radius-full); - transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1); - box-shadow: 0 2px 10px rgba(139, 92, 246, 0.4); + transition: transform var(--transition-smooth); + box-shadow: 0 4px 12px var(--color-primary-glow); z-index: 1; } @@ -166,22 +189,74 @@ body { transform: translateX(100%); } -.video-wrapper { - width: 100%; +.source-select { + appearance: none; + background-color: var(--color-bg-elevated); + background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 1.2rem center; + border: var(--border-subtle); + color: var(--color-text-primary); + padding: 0.7rem 2.8rem 0.7rem 1.2rem; + border-radius: var(--radius-full); + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + min-width: 160px; + transition: all var(--transition-base); +} + +.source-select:hover { + border-color: var(--color-primary); + background-color: var(--color-bg-card); +} + +.source-select:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px var(--color-primary-glow); +} + +.toggle-episodes-btn { + display: flex; + align-items: center; + gap: var(--spacing-sm); + background: var(--color-bg-elevated); + border: var(--border-subtle); + color: var(--color-text-primary); + padding: 0.7rem 1.2rem; + border-radius: var(--radius-full); + font-weight: 600; + cursor: pointer; + transition: all var(--transition-base); + margin-left: auto; +} + +.toggle-episodes-btn:hover { + background: var(--color-primary); + border-color: var(--color-primary); +} + +.toggle-episodes-btn.active { + background: var(--color-primary); + border-color: var(--color-primary); +} + +.video-container { aspect-ratio: 16/9; - background: #000; - border-radius: 20px; + background: var(--color-bg-base); + border-radius: var(--radius-xl); overflow: hidden; - box-shadow: 0 20px 60px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.08); + box-shadow: var(--shadow-lg), 0 0 0 1px var(--glass-border); position: relative; - transition: box-shadow 0.3s ease; + transition: box-shadow var(--transition-smooth); } -.video-wrapper:hover { - box-shadow: 0 25px 70px rgba(139, 92, 246, 0.15), 0 0 0 1px rgba(139, 92, 246, 0.3); +.video-container:hover { + box-shadow: var(--shadow-lg), 0 0 0 1px var(--color-primary), var(--shadow-glow); } -video { +#player { width: 100%; height: 100%; object-fit: contain; @@ -190,79 +265,91 @@ video { .loading-overlay { position: absolute; inset: 0; - background: #000; + background: var(--color-bg-base); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 20; - color: var(--text-secondary); - gap: 1rem; + gap: var(--spacing-lg); } .spinner { - width: 40px; - height: 40px; - border: 3px solid rgba(255,255,255,0.1); + width: 48px; + height: 48px; + border: 3px solid rgba(255, 255, 255, 0.1); + border-top-color: var(--color-primary); border-radius: 50%; - border-top-color: var(--accent); - animation: spin 1s linear infinite; + animation: spin 0.8s linear infinite; } -@keyframes spin { 100% { transform: rotate(360deg); } } +@keyframes spin { + to { transform: rotate(360deg); } +} -.controls-area { - width: 100%; +.loading-overlay p { + color: var(--color-text-secondary); + font-size: 0.95rem; + font-weight: 500; +} + +.episode-controls { display: flex; justify-content: space-between; - align-items: center; - margin-top: 1.5rem; - padding: 0 0.5rem; + align-items: flex-start; + gap: var(--spacing-lg); + background: var(--glass-bg); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid var(--glass-border); + border-radius: var(--radius-lg); + padding: var(--spacing-lg); + box-shadow: var(--shadow-sm); } -.episode-info h2 { - margin: 0; - font-size: 1.4rem; - font-weight: 700; - letter-spacing: -0.5px; - text-shadow: 0 2px 10px rgba(0,0,0,0.5); +.episode-info h1 { + font-size: 1.75rem; + font-weight: 800; + line-height: 1.2; + margin: 0 0 var(--spacing-xs); + color: var(--color-text-primary); } -.episode-info span { - color: var(--accent); - font-size: 0.95rem; +.episode-info p { + color: var(--color-primary); font-weight: 600; + font-size: 1rem; text-transform: uppercase; - letter-spacing: 1px; - margin-top: 4px; - display: block; + letter-spacing: 0.05em; + margin: 0; } -.nav-controls { +.navigation-buttons { display: flex; - gap: 1rem; + gap: var(--spacing-md); } .nav-btn { - background: rgba(255,255,255,0.05); - border: 1px solid rgba(255,255,255,0.1); - color: white; - padding: 0.8rem 1.6rem; - border-radius: var(--radius-full); - font-weight: 600; - cursor: pointer; display: flex; align-items: center; - gap: 0.6rem; - transition: all 0.2s ease; - backdrop-filter: blur(10px); + gap: var(--spacing-sm); + background: var(--color-bg-elevated); + border: var(--border-subtle); + color: var(--color-text-primary); + padding: 0.75rem 1.5rem; + border-radius: var(--radius-full); + font-weight: 600; + font-size: 0.9rem; + cursor: pointer; + transition: all var(--transition-base); + white-space: nowrap; } .nav-btn:hover:not(:disabled) { - background: var(--accent); - border-color: var(--accent); + background: var(--color-primary); + border-color: var(--color-primary); transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(139, 92, 246, 0.3); + box-shadow: var(--shadow-glow); } .nav-btn:disabled { @@ -270,144 +357,335 @@ video { cursor: not-allowed; } +.episodes-sidebar { + position: sticky; + top: 6rem; + height: calc(100vh - 7rem); + background: var(--glass-bg); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid var(--glass-border); + border-radius: var(--radius-lg); + padding: var(--spacing-lg); + box-shadow: var(--shadow-md); + display: flex; + flex-direction: column; +} + +.watch-container.sidebar-hidden > .episodes-sidebar { + display: none; +} + +.sidebar-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--spacing-lg); + padding-bottom: var(--spacing-md); + border-bottom: 1px solid var(--glass-border); +} + +.sidebar-header h3 { + font-size: 1.5rem; + font-weight: 700; + color: var(--color-primary); + margin: 0; +} + +.close-sidebar-btn { + display: none; + background: transparent; + border: none; + color: var(--color-text-secondary); + width: 36px; + height: 36px; + border-radius: 50%; + cursor: pointer; + transition: all var(--transition-base); + padding: 0; + align-items: center; + justify-content: center; +} + +.close-sidebar-btn:hover { + background: rgba(255, 255, 255, 0.08); + color: var(--color-text-primary); +} + +.sidebar-search { + margin-bottom: var(--spacing-md); +} + +.episode-search-input { + width: 100%; + appearance: none; + background-color: var(--color-bg-elevated); + border: var(--border-subtle); + color: var(--color-text-primary); + padding: 0.75rem 1.2rem; + border-radius: var(--radius-full); + font-size: 0.9rem; + font-weight: 500; + transition: all var(--transition-base); +} + +.episode-search-input:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px var(--color-primary-glow); +} + +.episode-search-input::placeholder { + color: var(--color-text-muted); +} + +.episode-list { + flex: 1; + overflow-y: auto; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(70px, 1fr)); + gap: var(--spacing-sm); + padding: var(--spacing-xs); + align-content: start; +} + +.episode-list::-webkit-scrollbar { + width: 6px; +} + +.episode-list::-webkit-scrollbar-track { + background: transparent; +} + +.episode-list::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.1); + border-radius: var(--radius-full); +} + +.episode-list::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.2); +} + +.episode-btn { + display: flex; + align-items: center; + justify-content: center; + background: var(--color-bg-elevated); + border: var(--border-subtle); + border-radius: var(--radius-md); + padding: 0.85rem; + color: var(--color-text-primary); + font-weight: 700; + font-size: 1rem; + text-decoration: none; + transition: all var(--transition-base); + box-shadow: var(--shadow-sm); + aspect-ratio: 1; + min-height: 52px; +} + +.episode-btn:hover { + background: rgba(255, 255, 255, 0.1); + border-color: var(--color-primary); + transform: scale(1.05); +} + +.episode-btn.active-ep { + background: var(--color-primary); + color: var(--color-text-primary); + border-color: var(--color-primary); + box-shadow: var(--shadow-glow); + font-weight: 800; + position: relative; +} + +.episode-btn.active-ep::after { + content: ''; + position: absolute; + inset: -2px; + border: 2px solid var(--color-primary); + border-radius: var(--radius-md); + opacity: 0.5; +} + +.anime-details { + max-width: 1800px; + margin: var(--spacing-2xl) auto; + padding: 0 var(--spacing-xl) 1rem; +} + +.details-container { + display: flex; + gap: var(--spacing-xl); + background: var(--glass-bg); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid var(--glass-border); + border-radius: var(--radius-lg); + padding: var(--spacing-xl); + box-shadow: var(--shadow-md); +} + +.details-cover { + flex-shrink: 0; +} + +.cover-image { + width: 220px; + height: auto; + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); +} + +.details-content h2 { + font-size: 2rem; + font-weight: 800; + margin: 0 0 var(--spacing-md); + color: var(--color-text-primary); +} + +.details-meta { + display: flex; + gap: var(--spacing-md); + flex-wrap: wrap; + margin-bottom: var(--spacing-lg); +} + +.meta-badge { + background: rgba(139, 92, 246, 0.12); + color: var(--color-primary); + padding: 0.5rem 1rem; + border-radius: var(--radius-sm); + font-size: 0.875rem; + font-weight: 600; + border: 1px solid rgba(139, 92, 246, 0.2); +} + +.meta-badge.meta-score { + background: var(--color-primary); + color: var(--color-text-primary); + border-color: var(--color-primary); +} + +.details-description { + font-size: 1rem; + line-height: 1.7; + color: var(--color-text-secondary); + margin: 0; +} + .plyr--video { - border-radius: 20px; - font-family: 'Inter', sans-serif; + border-radius: var(--radius-xl); } .plyr__controls { - background: linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.4) 50%, transparent 100%) !important; - padding: 10px 20px 20px 20px !important; - margin: 0 !important; - border-radius: 0 0 20px 20px !important; -} - -.plyr__progress input[type=range] { - cursor: pointer; + background: linear-gradient(to top, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.5) 50%, transparent 100%) !important; + padding: 1rem 1.5rem 1.5rem !important; + border-radius: 0 0 var(--radius-xl) var(--radius-xl) !important; } .plyr--full-ui input[type=range] { - color: var(--accent); - height: 4px; - transition: height 0.1s ease; -} - -.plyr__progress__container:hover input[type=range] { - height: 6px; -} - -.plyr__control { - background: transparent !important; - border-radius: 4px; - transition: transform 0.1s, opacity 0.2s; - padding: 7px !important; + color: var(--color-primary); } .plyr__control:hover { - background: rgba(255,255,255,0.1) !important; - opacity: 1; -} - -.plyr__control svg { - width: 24px; - height: 24px; - filter: drop-shadow(0 1px 2px rgba(0,0,0,0.5)); -} - -.plyr__control--overlaid { - background: rgba(0, 0, 0, 0.6) !important; - border: 2px solid white; - border-radius: 50%; - padding: 1.5rem !important; - opacity: 0.9; - transition: all 0.2s ease; -} - -.plyr__control--overlaid svg { - width: 32px; - height: 32px; -} - -.plyr__control--overlaid:hover { - background: var(--accent) !important; - border-color: var(--accent); - transform: scale(1.1); -} - -.plyr__time { - font-size: 13px; - font-weight: 500; - text-shadow: 0 1px 2px rgba(0,0,0,0.8); + background: rgba(255, 255, 255, 0.12) !important; } .plyr__menu__container { - background: rgba(28, 28, 30, 0.95) !important; - backdrop-filter: blur(12px); - border: 1px solid rgba(255,255,255,0.1); - border-radius: 12px !important; - padding: 8px; - box-shadow: 0 10px 40px rgba(0,0,0,0.5) !important; - bottom: 60px !important; + background: var(--glass-bg) !important; + backdrop-filter: blur(16px); + border: 1px solid var(--glass-border); + border-radius: var(--radius-md) !important; + box-shadow: var(--shadow-lg) !important; } -.plyr__menu__container .plyr__control { - font-size: 13px; - font-weight: 500; - padding: 8px 12px !important; - border-radius: 6px; - justify-content: flex-start; +@media (max-width: 1100px) { + .watch-container { + grid-template-columns: 1fr; + padding-top: 5rem; + } + + .episodes-sidebar { + position: fixed; + top: 5rem; + right: 0; + left: 0; + width: auto; + height: auto; + max-height: 80vh; + margin: 0 1rem; + z-index: 999; + display: none; + } + + .episodes-sidebar.sidebar-open { + display: flex; + } + + .close-sidebar-btn { + display: flex; + } + + .episode-list { + grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); + } } -.plyr__menu__container .plyr__control:hover { - background: rgba(255,255,255,0.1) !important; +@media (max-width: 768px) { + .watch-container { + padding: 4.5rem 1rem 1rem; + } + + .anime-details { + padding: 0 1rem var(--spacing-xl); + } + + .details-container { + flex-direction: column; + text-align: center; + } + + .cover-image { + width: 180px; + margin: 0 auto; + } + + .player-toolbar { + justify-content: center; + } + + .episode-controls { + flex-direction: column; + gap: var(--spacing-md); + } + + .navigation-buttons { + width: 100%; + justify-content: center; + } + + .nav-btn { + flex: 1; + justify-content: center; + } + + .episodes-sidebar { + margin: 0 0.5rem; + padding: var(--spacing-md); + } } -.plyr__menu__container .plyr__control[aria-checked="true"] { - color: var(--accent); -} -.plyr__menu__container .plyr__control[aria-checked="true"]::after { - background: var(--accent); -} +@media (max-width: 480px) { + .episode-info h1 { + font-size: 1.25rem; + } -.plyr__tooltip { - background: rgba(28, 28, 30, 0.9); - border: 1px solid rgba(255,255,255,0.1); - border-radius: 4px; - font-size: 12px; - font-weight: 600; - padding: 4px 8px; - box-shadow: 0 2px 10px rgba(0,0,0,0.5); -} + .details-content h2 { + font-size: 1.5rem; + } -.plyr__cues { - margin-bottom: 50px !important; -} - -.plyr__cues span { - background-color: rgba(0, 0, 0, 0.75) !important; - font-family: 'Inter', sans-serif; - font-weight: 600; - font-size: 18px; - padding: 4px 12px; - border-radius: 4px; - text-shadow: 0 2px 4px rgba(0,0,0,0.8); -} - -.extension-select option { - background: var(--bg-overlay); - color: var(--text-primary); - padding: 0.8rem 1rem; - font-size: 0.9rem; - font-weight: 500; - border: none; -} - -.extension-select option:hover { - background: rgba(139, 92, 246, 0.2); - color: white; -} - -.extension-select option:checked { - background: var(--accent); - color: white; + .nav-btn span { + display: none; + } } \ No newline at end of file diff --git a/views/watch.html b/views/watch.html index 3c5cd66..9ae823f 100644 --- a/views/watch.html +++ b/views/watch.html @@ -6,69 +6,176 @@ WaifuBoard Watch - - + + + + - + -
+
- -
- +
+ +
+
+

Select a source...

+
+
-
-
-

Loading...

- Episode -- -
+
+
+

Loading...

+

Episode --

+
+ +
+ + + + - -
+ \ No newline at end of file