code organisation & refactor
This commit is contained in:
178
src/scripts/anime/anime.js
Normal file
178
src/scripts/anime/anime.js
Normal file
@@ -0,0 +1,178 @@
|
||||
const animeId = window.location.pathname.split('/').pop();
|
||||
let player;
|
||||
|
||||
let totalEpisodes = 0;
|
||||
let currentPage = 1;
|
||||
const itemsPerPage = 12;
|
||||
|
||||
var tag = document.createElement('script');
|
||||
tag.src = "https://www.youtube.com/iframe_api";
|
||||
var firstScriptTag = document.getElementsByTagName('script')[0];
|
||||
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
|
||||
|
||||
async function loadAnime() {
|
||||
try {
|
||||
const res = await fetch(`/api/anime/${animeId}`);
|
||||
const data = await res.json();
|
||||
|
||||
if(data.error) {
|
||||
document.getElementById('title').innerText = "Anime Not Found";
|
||||
return;
|
||||
}
|
||||
|
||||
const title = data.title.english || data.title.romaji;
|
||||
document.title = `${title} | WaifuBoard`;
|
||||
document.getElementById('title').innerText = title;
|
||||
document.getElementById('poster').src = data.coverImage.extraLarge;
|
||||
|
||||
const rawDesc = data.description || "No description available.";
|
||||
handleDescription(rawDesc);
|
||||
|
||||
document.getElementById('score').innerText = (data.averageScore || '?') + '% Score';
|
||||
document.getElementById('year').innerText = data.seasonYear || '????';
|
||||
document.getElementById('genres').innerText = data.genres ? data.genres.slice(0, 3).join(' • ') : '';
|
||||
|
||||
document.getElementById('format').innerText = data.format || 'TV';
|
||||
document.getElementById('episodes').innerText = data.episodes || '?';
|
||||
document.getElementById('status').innerText = data.status || 'Unknown';
|
||||
document.getElementById('season').innerText = `${data.season || ''} ${data.seasonYear || ''}`;
|
||||
|
||||
if (data.studios && data.studios.nodes.length > 0) {
|
||||
document.getElementById('studio').innerText = data.studios.nodes[0].name;
|
||||
}
|
||||
|
||||
if (data.characters && data.characters.nodes) {
|
||||
const charContainer = document.getElementById('char-list');
|
||||
data.characters.nodes.slice(0, 5).forEach(char => {
|
||||
charContainer.innerHTML += `
|
||||
<div class="character-item">
|
||||
<div class="char-dot"></div> ${char.name.full}
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('watch-btn').onclick = () => {
|
||||
window.location.href = `/watch/${animeId}/1`;
|
||||
};
|
||||
|
||||
if (data.trailer && data.trailer.site === 'youtube') {
|
||||
window.onYouTubeIframeAPIReady = function() {
|
||||
player = new YT.Player('player', {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
videoId: data.trailer.id,
|
||||
playerVars: {
|
||||
'autoplay': 1, 'controls': 0, 'mute': 1,
|
||||
'loop': 1, 'playlist': data.trailer.id,
|
||||
'showinfo': 0, 'modestbranding': 1, 'disablekb': 1
|
||||
},
|
||||
events: { 'onReady': (e) => e.target.playVideo() }
|
||||
});
|
||||
};
|
||||
} else {
|
||||
const banner = data.bannerImage || data.coverImage.extraLarge;
|
||||
document.querySelector('.video-background').innerHTML = `<img src="${banner}" style="width:100%; height:100%; object-fit:cover;">`;
|
||||
}
|
||||
|
||||
|
||||
if (data.nextAiringEpisode) {
|
||||
totalEpisodes = data.nextAiringEpisode.episode - 1;
|
||||
} else {
|
||||
totalEpisodes = data.episodes || 12;
|
||||
}
|
||||
|
||||
totalEpisodes = Math.min(Math.max(totalEpisodes, 1), 5000);
|
||||
|
||||
renderEpisodes();
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDescription(text) {
|
||||
const tmp = document.createElement("DIV");
|
||||
tmp.innerHTML = text;
|
||||
const cleanText = tmp.textContent || tmp.innerText || "";
|
||||
|
||||
const sentences = cleanText.match(/[^\.!\?]+[\.!\?]+/g) || [cleanText];
|
||||
|
||||
document.getElementById('full-description').innerHTML = text;
|
||||
|
||||
if (sentences.length > 4) {
|
||||
const shortText = sentences.slice(0, 4).join(' ');
|
||||
document.getElementById('description-preview').innerText = shortText + '...';
|
||||
document.getElementById('read-more-btn').style.display = 'inline-flex';
|
||||
} else {
|
||||
document.getElementById('description-preview').innerHTML = text;
|
||||
document.getElementById('read-more-btn').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
document.getElementById('desc-modal').classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('desc-modal').classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
document.getElementById('desc-modal').addEventListener('click', (e) => {
|
||||
if (e.target.id === 'desc-modal') closeModal();
|
||||
});
|
||||
|
||||
function renderEpisodes() {
|
||||
const grid = document.getElementById('episodes-grid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
const start = (currentPage - 1) * itemsPerPage + 1;
|
||||
const end = Math.min(start + itemsPerPage - 1, totalEpisodes);
|
||||
|
||||
for(let i = start; i <= end; i++) {
|
||||
createEpisodeButton(i, grid);
|
||||
}
|
||||
updatePaginationControls();
|
||||
}
|
||||
|
||||
function createEpisodeButton(num, container) {
|
||||
const btn = document.createElement('div');
|
||||
btn.className = 'episode-btn';
|
||||
btn.innerText = `Ep ${num}`;
|
||||
btn.onclick = () => window.location.href = `/watch/${animeId}/${num}`;
|
||||
container.appendChild(btn);
|
||||
}
|
||||
|
||||
function updatePaginationControls() {
|
||||
const totalPages = Math.ceil(totalEpisodes / itemsPerPage);
|
||||
document.getElementById('page-info').innerText = `Page ${currentPage} of ${totalPages}`;
|
||||
document.getElementById('prev-page').disabled = currentPage === 1;
|
||||
document.getElementById('next-page').disabled = currentPage === totalPages;
|
||||
|
||||
document.getElementById('pagination-controls').style.display = 'flex';
|
||||
}
|
||||
|
||||
function changePage(delta) {
|
||||
currentPage += delta;
|
||||
renderEpisodes();
|
||||
}
|
||||
|
||||
const searchInput = document.getElementById('ep-search');
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
const val = parseInt(e.target.value);
|
||||
const grid = document.getElementById('episodes-grid');
|
||||
|
||||
if (val > 0 && val <= totalEpisodes) {
|
||||
grid.innerHTML = '';
|
||||
createEpisodeButton(val, grid);
|
||||
document.getElementById('pagination-controls').style.display = 'none';
|
||||
} else if (!e.target.value) {
|
||||
renderEpisodes();
|
||||
} else {
|
||||
grid.innerHTML = '<div style="color:#666; width:100%; text-align:center;">Episode not found</div>';
|
||||
document.getElementById('pagination-controls').style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
loadAnime();
|
||||
210
src/scripts/anime/animes.js
Normal file
210
src/scripts/anime/animes.js
Normal file
@@ -0,0 +1,210 @@
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const searchResults = document.getElementById('search-results');
|
||||
let searchTimeout;
|
||||
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
const query = e.target.value;
|
||||
clearTimeout(searchTimeout);
|
||||
if (query.length < 2) {
|
||||
searchResults.classList.remove('active');
|
||||
searchResults.innerHTML = '';
|
||||
searchInput.style.borderRadius = '99px';
|
||||
return;
|
||||
}
|
||||
searchTimeout = setTimeout(() => {
|
||||
fetchLocalSearch(query);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.search-wrapper')) {
|
||||
searchResults.classList.remove('active');
|
||||
searchInput.style.borderRadius = '99px';
|
||||
}
|
||||
});
|
||||
|
||||
async function fetchLocalSearch(query) {
|
||||
try {
|
||||
const res = await fetch(`/api/search/local?q=${encodeURIComponent(query)}`);
|
||||
const data = await res.json();
|
||||
renderSearchResults(data.results);
|
||||
} catch (err) { console.error("Search Error:", err); }
|
||||
}
|
||||
|
||||
function renderSearchResults(results) {
|
||||
searchResults.innerHTML = '';
|
||||
if (results.length === 0) {
|
||||
searchResults.innerHTML = '<div style="padding:1rem; color:#888; text-align:center">No results found</div>';
|
||||
} else {
|
||||
results.forEach(anime => {
|
||||
const title = getTitle(anime);
|
||||
const img = anime.coverImage.medium || anime.coverImage.large;
|
||||
const rating = anime.averageScore ? `${anime.averageScore}%` : 'N/A';
|
||||
const year = anime.seasonYear || '';
|
||||
const format = anime.format || 'TV';
|
||||
|
||||
const item = document.createElement('a');
|
||||
item.className = 'search-item';
|
||||
item.href = `/anime/${anime.id}`;
|
||||
item.innerHTML = `
|
||||
<img src="${img}" class="search-poster" alt="${title}">
|
||||
<div class="search-info">
|
||||
<div class="search-title">${title}</div>
|
||||
<div class="search-meta">
|
||||
<span class="rating-pill">${rating}</span>
|
||||
<span>• ${year}</span>
|
||||
<span>• ${format}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
searchResults.appendChild(item);
|
||||
});
|
||||
}
|
||||
searchResults.classList.add('active');
|
||||
searchInput.style.borderRadius = '12px 12px 0 0';
|
||||
}
|
||||
|
||||
function scrollCarousel(id, direction) {
|
||||
const container = document.getElementById(id);
|
||||
if(container) {
|
||||
const scrollAmount = container.clientWidth * 0.75;
|
||||
container.scrollBy({ left: direction * scrollAmount, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
let trendingAnimes = [];
|
||||
let currentHeroIndex = 0;
|
||||
let player;
|
||||
let heroInterval;
|
||||
|
||||
var tag = document.createElement('script');
|
||||
tag.src = "https://www.youtube.com/iframe_api";
|
||||
var firstScriptTag = document.getElementsByTagName('script')[0];
|
||||
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
|
||||
|
||||
function onYouTubeIframeAPIReady() {
|
||||
player = new YT.Player('player', {
|
||||
height: '100%', width: '100%',
|
||||
playerVars: { 'autoplay': 1, 'controls': 0, 'mute': 1, 'loop': 1, 'showinfo': 0, 'modestbranding': 1 },
|
||||
events: { 'onReady': (e) => { e.target.mute(); if(trendingAnimes.length) updateHeroVideo(trendingAnimes[currentHeroIndex]); } }
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchContent(isUpdate = false) {
|
||||
try {
|
||||
const trendingRes = await fetch('/api/trending');
|
||||
const trendingData = await trendingRes.json();
|
||||
|
||||
if (trendingData.results && trendingData.results.length > 0) {
|
||||
trendingAnimes = trendingData.results;
|
||||
if (!isUpdate) {
|
||||
updateHeroUI(trendingAnimes[0]);
|
||||
startHeroCycle();
|
||||
}
|
||||
renderList('trending', trendingAnimes);
|
||||
} else if (!isUpdate) {
|
||||
setTimeout(() => fetchContent(false), 2000);
|
||||
}
|
||||
|
||||
const topRes = await fetch('/api/top-airing');
|
||||
const topData = await topRes.json();
|
||||
if (topData.results && topData.results.length > 0) {
|
||||
renderList('top-airing', topData.results);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error("Fetch Error:", e);
|
||||
if(!isUpdate) setTimeout(() => fetchContent(false), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function startHeroCycle() {
|
||||
if(heroInterval) clearInterval(heroInterval);
|
||||
heroInterval = setInterval(() => {
|
||||
if(trendingAnimes.length > 0) {
|
||||
currentHeroIndex = (currentHeroIndex + 1) % trendingAnimes.length;
|
||||
updateHeroUI(trendingAnimes[currentHeroIndex]);
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
function getTitle(anime) {
|
||||
return anime.title.english || anime.title.romaji || "Unknown Title";
|
||||
}
|
||||
|
||||
function updateHeroUI(anime) {
|
||||
if(!anime) return;
|
||||
const title = getTitle(anime);
|
||||
const score = anime.averageScore ? anime.averageScore + '% Match' : 'N/A';
|
||||
const year = anime.seasonYear || '';
|
||||
const type = anime.format || 'TV';
|
||||
const desc = anime.description || 'No description available.';
|
||||
const poster = anime.coverImage ? anime.coverImage.extraLarge : '';
|
||||
const banner = anime.bannerImage || poster;
|
||||
|
||||
document.getElementById('hero-title').innerText = title;
|
||||
document.getElementById('hero-desc').innerHTML = desc;
|
||||
document.getElementById('hero-score').innerText = score;
|
||||
document.getElementById('hero-year').innerText = year;
|
||||
document.getElementById('hero-type').innerText = type;
|
||||
document.getElementById('hero-poster').src = poster;
|
||||
|
||||
const watchBtn = document.getElementById('watch-btn');
|
||||
if(watchBtn) watchBtn.onclick = () => window.location.href = `/anime/${anime.id}`;
|
||||
|
||||
const bgImg = document.getElementById('hero-bg-media');
|
||||
if(bgImg && bgImg.src !== banner) bgImg.src = banner;
|
||||
|
||||
updateHeroVideo(anime);
|
||||
|
||||
document.getElementById('hero-loading-ui').style.display = 'none';
|
||||
document.getElementById('hero-real-ui').style.display = 'block';
|
||||
}
|
||||
|
||||
function updateHeroVideo(anime) {
|
||||
if (!player || !player.loadVideoById) return;
|
||||
const videoContainer = document.getElementById('player');
|
||||
if (anime.trailer && anime.trailer.site === 'youtube' && anime.trailer.id) {
|
||||
if(player.getVideoData && player.getVideoData().video_id !== anime.trailer.id) {
|
||||
player.loadVideoById(anime.trailer.id);
|
||||
player.mute();
|
||||
}
|
||||
videoContainer.style.opacity = "1";
|
||||
} else {
|
||||
videoContainer.style.opacity = "0";
|
||||
player.stopVideo();
|
||||
}
|
||||
}
|
||||
|
||||
function renderList(id, list) {
|
||||
const container = document.getElementById(id);
|
||||
const firstId = list.length > 0 ? list[0].id : null;
|
||||
const currentFirstId = container.firstElementChild?.dataset?.id;
|
||||
if (currentFirstId && parseInt(currentFirstId) === firstId && container.children.length === list.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '';
|
||||
list.forEach(anime => {
|
||||
const title = getTitle(anime);
|
||||
const cover = anime.coverImage ? anime.coverImage.large : '';
|
||||
const ep = anime.nextAiringEpisode ? 'Ep ' + anime.nextAiringEpisode.episode : (anime.episodes ? anime.episodes + ' Eps' : 'TV');
|
||||
const score = anime.averageScore || '--';
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.className = 'card';
|
||||
el.dataset.id = anime.id;
|
||||
el.onclick = () => window.location.href = `/anime/${anime.id}`;
|
||||
el.innerHTML = `
|
||||
<div class="card-img-wrap"><img src="${cover}" loading="lazy"></div>
|
||||
<div class="card-content">
|
||||
<h3>${title}</h3>
|
||||
<p>${score}% • ${ep}</p>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
fetchContent();
|
||||
setInterval(() => fetchContent(true), 60000);
|
||||
211
src/scripts/anime/player.js
Normal file
211
src/scripts/anime/player.js
Normal file
@@ -0,0 +1,211 @@
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const animeId = pathParts[2];
|
||||
const currentEpisode = parseInt(pathParts[3]);
|
||||
|
||||
let audioMode = 'sub';
|
||||
let currentExtension = '';
|
||||
let plyrInstance;
|
||||
let hlsInstance;
|
||||
|
||||
document.getElementById('back-link').href = `/anime/${animeId}`;
|
||||
document.getElementById('episode-label').innerText = `Episode ${currentEpisode}`;
|
||||
|
||||
async function loadMetadata() {
|
||||
try {
|
||||
const res = await fetch(`/api/anime/${animeId}`);
|
||||
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}`;
|
||||
}
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
async function loadExtensions() {
|
||||
try {
|
||||
const res = await fetch('/api/extensions');
|
||||
const data = await res.json();
|
||||
const select = document.getElementById('extension-select');
|
||||
|
||||
if (data.extensions && data.extensions.length > 0) {
|
||||
data.extensions.forEach(extName => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = extName;
|
||||
opt.innerText = extName;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
} else {
|
||||
select.innerHTML = '<option>No Extensions</option>';
|
||||
select.disabled = true;
|
||||
setLoading("No extensions found in WaifuBoards folder.");
|
||||
}
|
||||
} catch(e) { console.error("Extension Error:", e); }
|
||||
}
|
||||
|
||||
async function onExtensionChange() {
|
||||
const select = document.getElementById('extension-select');
|
||||
currentExtension = select.value;
|
||||
setLoading("Fetching extension settings...");
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/extension/${currentExtension}/settings`);
|
||||
const settings = await res.json();
|
||||
|
||||
const toggle = document.getElementById('sd-toggle');
|
||||
if (settings.supportsDub) {
|
||||
toggle.style.display = 'flex';
|
||||
setAudioMode('sub');
|
||||
} else {
|
||||
toggle.style.display = 'none';
|
||||
setAudioMode('sub');
|
||||
}
|
||||
|
||||
const serverSelect = document.getElementById('server-select');
|
||||
serverSelect.innerHTML = '';
|
||||
if (settings.episodeServers && settings.episodeServers.length > 0) {
|
||||
settings.episodeServers.forEach(srv => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = srv;
|
||||
opt.innerText = srv;
|
||||
serverSelect.appendChild(opt);
|
||||
});
|
||||
serverSelect.style.display = 'block';
|
||||
} else {
|
||||
serverSelect.style.display = 'none';
|
||||
}
|
||||
|
||||
loadStream();
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setLoading("Failed to load extension settings.");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAudioMode() {
|
||||
const newMode = audioMode === 'sub' ? 'dub' : 'sub';
|
||||
setAudioMode(newMode);
|
||||
loadStream();
|
||||
}
|
||||
|
||||
function setAudioMode(mode) {
|
||||
audioMode = mode;
|
||||
const toggle = document.getElementById('sd-toggle');
|
||||
const subOpt = document.getElementById('opt-sub');
|
||||
const dubOpt = document.getElementById('opt-dub');
|
||||
|
||||
toggle.setAttribute('data-state', mode);
|
||||
if (mode === 'sub') {
|
||||
subOpt.classList.add('active');
|
||||
dubOpt.classList.remove('active');
|
||||
} else {
|
||||
subOpt.classList.remove('active');
|
||||
dubOpt.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStream() {
|
||||
if (!currentExtension) return;
|
||||
|
||||
const serverSelect = document.getElementById('server-select');
|
||||
const server = serverSelect.value || "default";
|
||||
|
||||
setLoading(`Searching & Resolving Stream (${audioMode})...`);
|
||||
|
||||
try {
|
||||
const url = `/api/watch/stream?animeId=${animeId}&episode=${currentEpisode}&server=${server}&category=${audioMode}&ext=${currentExtension}`;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.error) {
|
||||
setLoading(`Error: ${data.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.videoSources || data.videoSources.length === 0) {
|
||||
setLoading("No video sources found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const source = data.videoSources.find(s => s.type === 'm3u8') || data.videoSources[0];
|
||||
|
||||
const headers = data.headers || {};
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function playVideo(url, subtitles) {
|
||||
const video = document.getElementById('player');
|
||||
|
||||
if (Hls.isSupported()) {
|
||||
if (hlsInstance) hlsInstance.destroy();
|
||||
hlsInstance = new Hls({
|
||||
xhrSetup: (xhr, url) => {
|
||||
xhr.withCredentials = false;
|
||||
}
|
||||
});
|
||||
hlsInstance.loadSource(url);
|
||||
hlsInstance.attachMedia(video);
|
||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
video.src = url;
|
||||
}
|
||||
|
||||
if (plyrInstance) plyrInstance.destroy();
|
||||
|
||||
while (video.firstChild) {
|
||||
video.removeChild(video.firstChild);
|
||||
}
|
||||
|
||||
if (subtitles && subtitles.length > 0) {
|
||||
subtitles.forEach(sub => {
|
||||
const track = document.createElement('track');
|
||||
track.kind = 'captions';
|
||||
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'],
|
||||
settings: ['captions', 'quality', 'speed']
|
||||
});
|
||||
|
||||
video.play().catch(e => console.log("Auto-play blocked"));
|
||||
}
|
||||
|
||||
function setLoading(msg) {
|
||||
const overlay = document.getElementById('loading-overlay');
|
||||
const text = document.getElementById('loading-text');
|
||||
overlay.style.display = 'flex';
|
||||
text.innerText = msg;
|
||||
}
|
||||
|
||||
document.getElementById('prev-btn').onclick = () => {
|
||||
if(currentEpisode > 1) window.location.href = `/watch/${animeId}/${currentEpisode - 1}`;
|
||||
};
|
||||
document.getElementById('next-btn').onclick = () => {
|
||||
window.location.href = `/watch/${animeId}/${currentEpisode + 1}`;
|
||||
};
|
||||
if(currentEpisode <= 1) document.getElementById('prev-btn').disabled = true;
|
||||
|
||||
loadMetadata();
|
||||
loadExtensions();
|
||||
207
src/scripts/books/book.js
Normal file
207
src/scripts/books/book.js
Normal file
@@ -0,0 +1,207 @@
|
||||
const bookId = window.location.pathname.split('/').pop();
|
||||
let allChapters = [];
|
||||
let filteredChapters = [];
|
||||
let currentPage = 1;
|
||||
const itemsPerPage = 12;
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
const res = await fetch(`/api/book/${bookId}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.error) {
|
||||
const titleEl = document.getElementById('title');
|
||||
if (titleEl) titleEl.innerText = "Book Not Found";
|
||||
return;
|
||||
}
|
||||
|
||||
const title = data.title.english || data.title.romaji;
|
||||
document.title = `${title} | WaifuBoard Books`;
|
||||
|
||||
const titleEl = document.getElementById('title');
|
||||
if (titleEl) titleEl.innerText = title;
|
||||
|
||||
const descEl = document.getElementById('description');
|
||||
if (descEl) descEl.innerHTML = data.description || "No description available.";
|
||||
|
||||
const scoreEl = document.getElementById('score');
|
||||
if (scoreEl) scoreEl.innerText = (data.averageScore || '?') + '% Score';
|
||||
|
||||
const pubEl = document.getElementById('published-date');
|
||||
if (pubEl) {
|
||||
if (data.startDate && data.startDate.year) {
|
||||
const y = data.startDate.year;
|
||||
const m = data.startDate.month ? `-${data.startDate.month.toString().padStart(2, '0')}` : '';
|
||||
const d = data.startDate.day ? `-${data.startDate.day.toString().padStart(2, '0')}` : '';
|
||||
pubEl.innerText = `${y}${m}${d}`;
|
||||
} else {
|
||||
pubEl.innerText = '????';
|
||||
}
|
||||
}
|
||||
|
||||
const statusEl = document.getElementById('status');
|
||||
if (statusEl) statusEl.innerText = data.status || 'Unknown';
|
||||
|
||||
const formatEl = document.getElementById('format');
|
||||
if (formatEl) formatEl.innerText = data.format || 'MANGA';
|
||||
|
||||
const chaptersEl = document.getElementById('chapters');
|
||||
if (chaptersEl) chaptersEl.innerText = data.chapters || '?';
|
||||
|
||||
const genresEl = document.getElementById('genres');
|
||||
if(genresEl && data.genres) {
|
||||
genresEl.innerText = data.genres.slice(0, 3).join(' • ');
|
||||
}
|
||||
|
||||
const img = data.coverImage.extraLarge || data.coverImage.large;
|
||||
|
||||
const posterEl = document.getElementById('poster');
|
||||
if (posterEl) posterEl.src = img;
|
||||
|
||||
const heroBgEl = document.getElementById('hero-bg');
|
||||
if (heroBgEl) heroBgEl.src = data.bannerImage || img;
|
||||
|
||||
loadChapters();
|
||||
|
||||
} catch (err) {
|
||||
console.error("Metadata Error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChapters() {
|
||||
const tbody = document.getElementById('chapters-body');
|
||||
if (!tbody) return;
|
||||
|
||||
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 2rem;">Searching extensions for chapters...</td></tr>';
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/book/${bookId}/chapters`);
|
||||
const data = await res.json();
|
||||
|
||||
allChapters = data.chapters || [];
|
||||
filteredChapters = [...allChapters];
|
||||
|
||||
const totalEl = document.getElementById('total-chapters');
|
||||
|
||||
if (allChapters.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 2rem;">No chapters found on loaded extensions.</td></tr>';
|
||||
if (totalEl) totalEl.innerText = "0 Found";
|
||||
return;
|
||||
}
|
||||
|
||||
if (totalEl) totalEl.innerText = `${allChapters.length} Found`;
|
||||
|
||||
populateProviderFilter();
|
||||
|
||||
const readBtn = document.getElementById('read-start-btn');
|
||||
if (readBtn && filteredChapters.length > 0) {
|
||||
readBtn.onclick = () => openReader(filteredChapters[0].id);
|
||||
}
|
||||
|
||||
renderTable();
|
||||
|
||||
} catch (err) {
|
||||
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; color: #ef4444;">Error loading chapters.</td></tr>';
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
function populateProviderFilter() {
|
||||
const select = document.getElementById('provider-filter');
|
||||
if (!select) return;
|
||||
|
||||
const providers = [...new Set(allChapters.map(ch => ch.provider))];
|
||||
|
||||
if (providers.length > 0) {
|
||||
select.style.display = 'inline-block';
|
||||
|
||||
select.innerHTML = '<option value="all">All Providers</option>';
|
||||
|
||||
providers.forEach(prov => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = prov;
|
||||
opt.innerText = prov;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
|
||||
select.onchange = (e) => {
|
||||
const selected = e.target.value;
|
||||
if (selected === 'all') {
|
||||
filteredChapters = [...allChapters];
|
||||
} else {
|
||||
filteredChapters = allChapters.filter(ch => ch.provider === selected);
|
||||
}
|
||||
currentPage = 1;
|
||||
renderTable();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
const tbody = document.getElementById('chapters-body');
|
||||
if (!tbody) return;
|
||||
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (filteredChapters.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 2rem;">No chapters match this filter.</td></tr>';
|
||||
updatePagination();
|
||||
return;
|
||||
}
|
||||
|
||||
const start = (currentPage - 1) * itemsPerPage;
|
||||
const end = start + itemsPerPage;
|
||||
const pageItems = filteredChapters.slice(start, end);
|
||||
|
||||
pageItems.forEach((ch, idx) => {
|
||||
const realIndex = start + idx;
|
||||
|
||||
const row = document.createElement('tr');
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${ch.number}</td>
|
||||
<td>${ch.title || 'Chapter ' + ch.number}</td>
|
||||
<td><span class="pill" style="font-size:0.75rem;">${ch.provider}</span></td>
|
||||
<td>
|
||||
<button class="read-btn-small" onclick="openReader('${bookId}', '${realIndex}', '${ch.provider}')">
|
||||
Read
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
updatePagination();
|
||||
}
|
||||
|
||||
function updatePagination() {
|
||||
const totalPages = Math.ceil(filteredChapters.length / itemsPerPage);
|
||||
const pagination = document.getElementById('pagination');
|
||||
|
||||
if (!pagination) return;
|
||||
|
||||
if (totalPages <= 1) {
|
||||
pagination.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
pagination.style.display = 'flex';
|
||||
document.getElementById('page-info').innerText = `Page ${currentPage} of ${totalPages}`;
|
||||
|
||||
const prevBtn = document.getElementById('prev-page');
|
||||
const nextBtn = document.getElementById('next-page');
|
||||
|
||||
prevBtn.disabled = currentPage === 1;
|
||||
nextBtn.disabled = currentPage >= totalPages;
|
||||
|
||||
prevBtn.onclick = () => { currentPage--; renderTable(); };
|
||||
nextBtn.onclick = () => { currentPage++; renderTable(); };
|
||||
}
|
||||
|
||||
function openReader(bookId, chapterId, provider) {
|
||||
const c = encodeURIComponent(chapterId);
|
||||
const p = encodeURIComponent(provider);
|
||||
window.location.href = `/read/${bookId}/${c}/${p}`;
|
||||
}
|
||||
|
||||
init();
|
||||
176
src/scripts/books/books.js
Normal file
176
src/scripts/books/books.js
Normal file
@@ -0,0 +1,176 @@
|
||||
let trendingBooks = [];
|
||||
let currentHeroIndex = 0;
|
||||
let heroInterval;
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
const nav = document.getElementById('navbar');
|
||||
if (window.scrollY > 50) nav.classList.add('scrolled');
|
||||
else nav.classList.remove('scrolled');
|
||||
});
|
||||
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const searchResults = document.getElementById('search-results');
|
||||
let searchTimeout;
|
||||
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
const query = e.target.value;
|
||||
clearTimeout(searchTimeout);
|
||||
|
||||
if (query.length < 2) {
|
||||
searchResults.classList.remove('active');
|
||||
searchResults.innerHTML = '';
|
||||
searchInput.style.borderRadius = '99px';
|
||||
return;
|
||||
}
|
||||
|
||||
searchTimeout = setTimeout(() => {
|
||||
fetchBookSearch(query);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.search-wrapper')) {
|
||||
searchResults.classList.remove('active');
|
||||
searchInput.style.borderRadius = '99px';
|
||||
}
|
||||
});
|
||||
|
||||
async function fetchBookSearch(query) {
|
||||
try {
|
||||
const res = await fetch(`/api/search/books?q=${encodeURIComponent(query)}`);
|
||||
const data = await res.json();
|
||||
renderSearchResults(data.results || []);
|
||||
} catch (err) {
|
||||
console.error("Search Error:", err);
|
||||
renderSearchResults([]);
|
||||
}
|
||||
}
|
||||
|
||||
function renderSearchResults(results) {
|
||||
searchResults.innerHTML = '';
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
searchResults.innerHTML = '<div style="padding:1rem; color:#888; text-align:center">No results found</div>';
|
||||
} else {
|
||||
results.forEach(book => {
|
||||
const title = book.title.english || book.title.romaji || "Unknown";
|
||||
const img = (book.coverImage && (book.coverImage.medium || book.coverImage.large)) || '';
|
||||
const rating = book.averageScore ? `${book.averageScore}%` : 'N/A';
|
||||
const year = book.seasonYear || (book.startDate ? book.startDate.year : '') || '????';
|
||||
const format = book.format || 'MANGA';
|
||||
|
||||
const item = document.createElement('a');
|
||||
item.className = 'search-item';
|
||||
item.href = `/book/${book.id}`;
|
||||
|
||||
item.innerHTML = `
|
||||
<img src="${img}" class="search-poster" alt="${title}">
|
||||
<div class="search-info">
|
||||
<div class="search-title">${title}</div>
|
||||
<div class="search-meta">
|
||||
<span class="rating-pill">${rating}</span>
|
||||
<span>• ${year}</span>
|
||||
<span>• ${format}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
searchResults.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
searchResults.classList.add('active');
|
||||
searchInput.style.borderRadius = '12px 12px 0 0';
|
||||
}
|
||||
|
||||
function scrollCarousel(id, direction) {
|
||||
const container = document.getElementById(id);
|
||||
if(container) {
|
||||
const scrollAmount = container.clientWidth * 0.75;
|
||||
container.scrollBy({ left: direction * scrollAmount, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
const res = await fetch('/api/books/trending');
|
||||
const data = await res.json();
|
||||
|
||||
if (data.results && data.results.length > 0) {
|
||||
trendingBooks = data.results;
|
||||
updateHeroUI(trendingBooks[0]);
|
||||
renderList('trending', trendingBooks);
|
||||
startHeroCycle();
|
||||
}
|
||||
|
||||
const resPop = await fetch('/api/books/popular');
|
||||
const dataPop = await resPop.json();
|
||||
if (dataPop.results) renderList('popular', dataPop.results);
|
||||
|
||||
} catch (e) {
|
||||
console.error("Books Error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function startHeroCycle() {
|
||||
if(heroInterval) clearInterval(heroInterval);
|
||||
heroInterval = setInterval(() => {
|
||||
if(trendingBooks.length > 0) {
|
||||
currentHeroIndex = (currentHeroIndex + 1) % trendingBooks.length;
|
||||
updateHeroUI(trendingBooks[currentHeroIndex]);
|
||||
}
|
||||
}, 8000);
|
||||
}
|
||||
|
||||
function updateHeroUI(book) {
|
||||
if(!book) return;
|
||||
const title = book.title.english || book.title.romaji;
|
||||
const desc = book.description || "No description available.";
|
||||
const poster = (book.coverImage && (book.coverImage.extraLarge || book.coverImage.large)) || '';
|
||||
const banner = book.bannerImage || poster;
|
||||
|
||||
document.getElementById('hero-title').innerText = title;
|
||||
document.getElementById('hero-desc').innerHTML = desc;
|
||||
document.getElementById('hero-score').innerText = (book.averageScore || '?') + '% Score';
|
||||
document.getElementById('hero-year').innerText = (book.startDate && book.startDate.year) ? book.startDate.year : '????';
|
||||
document.getElementById('hero-type').innerText = book.format || 'MANGA';
|
||||
|
||||
const heroPoster = document.getElementById('hero-poster');
|
||||
if(heroPoster) heroPoster.src = poster;
|
||||
|
||||
const bg = document.getElementById('hero-bg-media');
|
||||
if(bg) bg.src = banner;
|
||||
|
||||
const readBtn = document.getElementById('read-btn');
|
||||
if (readBtn) {
|
||||
readBtn.onclick = () => window.location.href = `/book/${book.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderList(id, list) {
|
||||
const container = document.getElementById(id);
|
||||
container.innerHTML = '';
|
||||
|
||||
list.forEach(book => {
|
||||
const title = book.title.english || book.title.romaji;
|
||||
const cover = book.coverImage ? book.coverImage.large : '';
|
||||
const score = book.averageScore || '--';
|
||||
const type = book.format || 'Book';
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.className = 'card';
|
||||
el.onclick = () => {
|
||||
window.location.href = `/book/${book.id}`;
|
||||
};
|
||||
el.innerHTML = `
|
||||
<div class="card-img-wrap"><img src="${cover}" loading="lazy"></div>
|
||||
<div class="card-content">
|
||||
<h3>${title}</h3>
|
||||
<p>${score}% • ${type}</p>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
592
src/scripts/books/reader.js
Normal file
592
src/scripts/books/reader.js
Normal file
@@ -0,0 +1,592 @@
|
||||
const reader = document.getElementById('reader');
|
||||
const panel = document.getElementById('settings-panel');
|
||||
const overlay = document.getElementById('overlay');
|
||||
const settingsBtn = document.getElementById('settings-btn');
|
||||
const closePanel = document.getElementById('close-panel');
|
||||
const chapterLabel = document.getElementById('chapter-label');
|
||||
const prevBtn = document.getElementById('prev-chapter');
|
||||
const nextBtn = document.getElementById('next-chapter');
|
||||
|
||||
const lnSettings = document.getElementById('ln-settings');
|
||||
const mangaSettings = document.getElementById('manga-settings');
|
||||
|
||||
const config = {
|
||||
ln: {
|
||||
fontSize: 18,
|
||||
lineHeight: 1.8,
|
||||
maxWidth: 750,
|
||||
fontFamily: '"Georgia", serif',
|
||||
textColor: '#e5e7eb',
|
||||
bg: '#14141b',
|
||||
textAlign: 'justify'
|
||||
},
|
||||
manga: {
|
||||
direction: 'rtl',
|
||||
mode: 'auto',
|
||||
spacing: 16,
|
||||
imageFit: 'screen',
|
||||
preloadCount: 3
|
||||
}
|
||||
};
|
||||
|
||||
let currentType = null;
|
||||
let currentPages = [];
|
||||
let observer = null;
|
||||
|
||||
const parts = window.location.pathname.split('/');
|
||||
|
||||
const bookId = parts[2];
|
||||
let chapter = parts[3];
|
||||
let provider = parts[4];
|
||||
|
||||
function loadConfig() {
|
||||
try {
|
||||
const saved = localStorage.getItem('readerConfig');
|
||||
if (saved) {
|
||||
const parsed = JSON.parse(saved);
|
||||
Object.assign(config.ln, parsed.ln || {});
|
||||
Object.assign(config.manga, parsed.manga || {});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading config:', e);
|
||||
}
|
||||
updateUIFromConfig();
|
||||
}
|
||||
|
||||
function saveConfig() {
|
||||
try {
|
||||
localStorage.setItem('readerConfig', JSON.stringify(config));
|
||||
} catch (e) {
|
||||
console.error('Error saving config:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function updateUIFromConfig() {
|
||||
document.getElementById('font-size').value = config.ln.fontSize;
|
||||
document.getElementById('font-size-value').textContent = config.ln.fontSize + 'px';
|
||||
|
||||
document.getElementById('line-height').value = config.ln.lineHeight;
|
||||
document.getElementById('line-height-value').textContent = config.ln.lineHeight;
|
||||
|
||||
document.getElementById('max-width').value = config.ln.maxWidth;
|
||||
document.getElementById('max-width-value').textContent = config.ln.maxWidth + 'px';
|
||||
|
||||
document.getElementById('font-family').value = config.ln.fontFamily;
|
||||
document.getElementById('text-color').value = config.ln.textColor;
|
||||
document.getElementById('bg-color').value = config.ln.bg;
|
||||
|
||||
document.querySelectorAll('[data-align]').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.align === config.ln.textAlign);
|
||||
});
|
||||
|
||||
document.getElementById('display-mode').value = config.manga.mode;
|
||||
document.getElementById('image-fit').value = config.manga.imageFit;
|
||||
document.getElementById('page-spacing').value = config.manga.spacing;
|
||||
document.getElementById('page-spacing-value').textContent = config.manga.spacing + 'px';
|
||||
document.getElementById('preload-count').value = config.manga.preloadCount;
|
||||
|
||||
document.querySelectorAll('[data-direction]').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.direction === config.manga.direction);
|
||||
});
|
||||
}
|
||||
|
||||
function applyStyles() {
|
||||
if (currentType === 'ln') {
|
||||
document.documentElement.style.setProperty('--ln-font-size', config.ln.fontSize + 'px');
|
||||
document.documentElement.style.setProperty('--ln-line-height', config.ln.lineHeight);
|
||||
document.documentElement.style.setProperty('--ln-max-width', config.ln.maxWidth + 'px');
|
||||
document.documentElement.style.setProperty('--ln-font-family', config.ln.fontFamily);
|
||||
document.documentElement.style.setProperty('--ln-text-color', config.ln.textColor);
|
||||
document.documentElement.style.setProperty('--bg-base', config.ln.bg);
|
||||
document.documentElement.style.setProperty('--ln-text-align', config.ln.textAlign);
|
||||
}
|
||||
|
||||
if (currentType === 'manga') {
|
||||
document.documentElement.style.setProperty('--page-spacing', config.manga.spacing + 'px');
|
||||
document.documentElement.style.setProperty('--page-max-width', 900 + 'px');
|
||||
document.documentElement.style.setProperty('--manga-max-width', 1400 + 'px');
|
||||
|
||||
const viewportHeight = window.innerHeight - 64 - 32;
|
||||
document.documentElement.style.setProperty('--viewport-height', viewportHeight + 'px');
|
||||
}
|
||||
}
|
||||
|
||||
function updateSettingsVisibility() {
|
||||
lnSettings.classList.toggle('hidden', currentType !== 'ln');
|
||||
mangaSettings.classList.toggle('hidden', currentType !== 'manga');
|
||||
}
|
||||
|
||||
async function loadChapter() {
|
||||
reader.innerHTML = `
|
||||
<div class="loading-container">
|
||||
<div class="loading-spinner"></div>
|
||||
<span>Loading chapter...</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/book/${bookId}/${chapter}/${provider}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.title) {
|
||||
chapterLabel.textContent = data.title;
|
||||
document.title = data.title;
|
||||
} else {
|
||||
chapterLabel.textContent = `Chapter ${chapter}`;
|
||||
document.title = `Chapter ${chapter}`;
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
reader.innerHTML = `
|
||||
<div class="loading-container">
|
||||
<span style="color: #ef4444;">Error: ${data.error}</span>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
currentType = data.type;
|
||||
updateSettingsVisibility();
|
||||
applyStyles();
|
||||
reader.innerHTML = '';
|
||||
|
||||
if (data.type === 'manga') {
|
||||
currentPages = data.pages || [];
|
||||
loadManga(currentPages);
|
||||
} else if (data.type === 'ln') {
|
||||
loadLN(data.content);
|
||||
}
|
||||
} catch (error) {
|
||||
reader.innerHTML = `
|
||||
<div class="loading-container">
|
||||
<span style="color: #ef4444;">Error loading chapter: ${error.message}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function loadManga(pages) {
|
||||
if (!pages || pages.length === 0) {
|
||||
reader.innerHTML = '<div class="loading-container"><span>No pages found</span></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.className = 'manga-container';
|
||||
|
||||
let isLongStrip = false;
|
||||
|
||||
if (config.manga.mode === 'longstrip') {
|
||||
isLongStrip = true;
|
||||
} else if (config.manga.mode === 'auto' && detectLongStrip(pages)) {
|
||||
isLongStrip = true;
|
||||
}
|
||||
|
||||
|
||||
const useDouble = config.manga.mode === 'double' ||
|
||||
(config.manga.mode === 'auto' && !isLongStrip && shouldUseDoublePage(pages));
|
||||
|
||||
if (useDouble) {
|
||||
loadDoublePage(container, pages);
|
||||
} else {
|
||||
loadSinglePage(container, pages);
|
||||
}
|
||||
|
||||
reader.appendChild(container);
|
||||
setupLazyLoading();
|
||||
enableMangaPageNavigation();
|
||||
}
|
||||
|
||||
function shouldUseDoublePage(pages) {
|
||||
if (pages.length <= 5) return false;
|
||||
|
||||
const widePages = pages.filter(p => {
|
||||
if (!p.height || !p.width) return false;
|
||||
const ratio = p.width / p.height;
|
||||
return ratio > 1.3;
|
||||
});
|
||||
|
||||
if (widePages.length > pages.length * 0.3) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function loadSinglePage(container, pages) {
|
||||
pages.forEach((page, index) => {
|
||||
const img = createImageElement(page, index);
|
||||
container.appendChild(img);
|
||||
});
|
||||
}
|
||||
|
||||
function loadDoublePage(container, pages) {
|
||||
let i = 0;
|
||||
while (i < pages.length) {
|
||||
const currentPage = pages[i];
|
||||
const nextPage = pages[i + 1];
|
||||
|
||||
const isWide = currentPage.width && currentPage.height &&
|
||||
(currentPage.width / currentPage.height) > 1.1;
|
||||
|
||||
if (isWide) {
|
||||
const img = createImageElement(currentPage, i);
|
||||
container.appendChild(img);
|
||||
i++;
|
||||
} else {
|
||||
const doubleContainer = document.createElement('div');
|
||||
doubleContainer.className = 'double-container';
|
||||
|
||||
const leftPage = createImageElement(currentPage, i);
|
||||
|
||||
if (nextPage) {
|
||||
const nextIsWide = nextPage.width && nextPage.height &&
|
||||
(nextPage.width / nextPage.height) > 1.3;
|
||||
|
||||
if (nextIsWide) {
|
||||
const singleImg = createImageElement(currentPage, i);
|
||||
container.appendChild(singleImg);
|
||||
i++;
|
||||
} else {
|
||||
const rightPage = createImageElement(nextPage, i + 1);
|
||||
|
||||
if (config.manga.direction === 'rtl') {
|
||||
doubleContainer.appendChild(rightPage);
|
||||
doubleContainer.appendChild(leftPage);
|
||||
} else {
|
||||
doubleContainer.appendChild(leftPage);
|
||||
doubleContainer.appendChild(rightPage);
|
||||
}
|
||||
|
||||
container.appendChild(doubleContainer);
|
||||
i += 2;
|
||||
}
|
||||
} else {
|
||||
const singleImg = createImageElement(currentPage, i);
|
||||
container.appendChild(singleImg);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createImageElement(page, index) {
|
||||
const img = document.createElement('img');
|
||||
img.className = 'page-img';
|
||||
img.dataset.index = index;
|
||||
|
||||
const url = buildProxyUrl(page.url, page.headers);
|
||||
|
||||
if (config.manga.mode === 'longstrip' && index > 0) {
|
||||
img.classList.add('longstrip-fit');
|
||||
} else {
|
||||
if (config.manga.imageFit === 'width') img.classList.add('fit-width');
|
||||
else if (config.manga.imageFit === 'height') img.classList.add('fit-height');
|
||||
else if (config.manga.imageFit === 'screen') img.classList.add('fit-screen');
|
||||
}
|
||||
|
||||
if (index < config.manga.preloadCount) {
|
||||
img.src = url;
|
||||
} else {
|
||||
img.dataset.src = url;
|
||||
img.loading = 'lazy';
|
||||
}
|
||||
|
||||
img.alt = `Page ${index + 1}`;
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
function buildProxyUrl(url, headers = {}) {
|
||||
const params = new URLSearchParams({
|
||||
url
|
||||
});
|
||||
|
||||
if (headers.referer) params.append('referer', headers.referer);
|
||||
if (headers['user-agent']) params.append('ua', headers['user-agent']);
|
||||
if (headers.cookie) params.append('cookie', headers.cookie);
|
||||
|
||||
return `/api/proxy?${params.toString()}`;
|
||||
}
|
||||
|
||||
function detectLongStrip(pages) {
|
||||
if (!pages || pages.length === 0) return false;
|
||||
|
||||
const relevant = pages.slice(1);
|
||||
const tall = relevant.filter(p => p.height && p.width && (p.height / p.width) > 2);
|
||||
return tall.length >= 2 || (tall.length / relevant.length) > 0.3;
|
||||
}
|
||||
|
||||
function setupLazyLoading() {
|
||||
if (observer) observer.disconnect();
|
||||
|
||||
observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target;
|
||||
if (img.dataset.src) {
|
||||
img.src = img.dataset.src;
|
||||
delete img.dataset.src;
|
||||
observer.unobserve(img);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, {
|
||||
rootMargin: '200px'
|
||||
});
|
||||
|
||||
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));
|
||||
}
|
||||
|
||||
function loadLN(html) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'ln-content';
|
||||
div.innerHTML = html;
|
||||
reader.appendChild(div);
|
||||
}
|
||||
|
||||
document.getElementById('font-size').addEventListener('input', (e) => {
|
||||
config.ln.fontSize = parseInt(e.target.value);
|
||||
document.getElementById('font-size-value').textContent = e.target.value + 'px';
|
||||
applyStyles();
|
||||
saveConfig();
|
||||
});
|
||||
|
||||
document.getElementById('line-height').addEventListener('input', (e) => {
|
||||
config.ln.lineHeight = parseFloat(e.target.value);
|
||||
document.getElementById('line-height-value').textContent = e.target.value;
|
||||
applyStyles();
|
||||
saveConfig();
|
||||
});
|
||||
|
||||
document.getElementById('max-width').addEventListener('input', (e) => {
|
||||
config.ln.maxWidth = parseInt(e.target.value);
|
||||
document.getElementById('max-width-value').textContent = e.target.value + 'px';
|
||||
applyStyles();
|
||||
saveConfig();
|
||||
});
|
||||
|
||||
document.getElementById('font-family').addEventListener('change', (e) => {
|
||||
config.ln.fontFamily = e.target.value;
|
||||
applyStyles();
|
||||
saveConfig();
|
||||
});
|
||||
|
||||
document.getElementById('text-color').addEventListener('change', (e) => {
|
||||
config.ln.textColor = e.target.value;
|
||||
applyStyles();
|
||||
saveConfig();
|
||||
});
|
||||
|
||||
document.getElementById('bg-color').addEventListener('change', (e) => {
|
||||
config.ln.bg = e.target.value;
|
||||
applyStyles();
|
||||
saveConfig();
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-align]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('[data-align]').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
config.ln.textAlign = btn.dataset.align;
|
||||
applyStyles();
|
||||
saveConfig();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-preset]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const preset = btn.dataset.preset;
|
||||
|
||||
const presets = {
|
||||
dark: { bg: '#14141b', textColor: '#e5e7eb' },
|
||||
sepia: { bg: '#f4ecd8', textColor: '#5c472d' },
|
||||
light: { bg: '#fafafa', textColor: '#1f2937' },
|
||||
amoled: { bg: '#000000', textColor: '#ffffff' }
|
||||
};
|
||||
|
||||
if (presets[preset]) {
|
||||
Object.assign(config.ln, presets[preset]);
|
||||
document.getElementById('bg-color').value = config.ln.bg;
|
||||
document.getElementById('text-color').value = config.ln.textColor;
|
||||
applyStyles();
|
||||
saveConfig();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('display-mode').addEventListener('change', (e) => {
|
||||
config.manga.mode = e.target.value;
|
||||
saveConfig();
|
||||
loadChapter();
|
||||
});
|
||||
|
||||
document.getElementById('image-fit').addEventListener('change', (e) => {
|
||||
config.manga.imageFit = e.target.value;
|
||||
saveConfig();
|
||||
loadChapter();
|
||||
});
|
||||
|
||||
document.getElementById('page-spacing').addEventListener('input', (e) => {
|
||||
config.manga.spacing = parseInt(e.target.value);
|
||||
document.getElementById('page-spacing-value').textContent = e.target.value + 'px';
|
||||
applyStyles();
|
||||
saveConfig();
|
||||
});
|
||||
|
||||
document.getElementById('preload-count').addEventListener('change', (e) => {
|
||||
config.manga.preloadCount = parseInt(e.target.value);
|
||||
saveConfig();
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-direction]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('[data-direction]').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
config.manga.direction = btn.dataset.direction;
|
||||
saveConfig();
|
||||
loadChapter();
|
||||
});
|
||||
});
|
||||
|
||||
prevBtn.addEventListener('click', () => {
|
||||
const newChapter = String(parseInt(chapter) - 1);
|
||||
updateURL(newChapter);
|
||||
window.scrollTo(0, 0);
|
||||
loadChapter();
|
||||
});
|
||||
|
||||
nextBtn.addEventListener('click', () => {
|
||||
const newChapter = String(parseInt(chapter) + 1);
|
||||
updateURL(newChapter);
|
||||
window.scrollTo(0, 0);
|
||||
loadChapter();
|
||||
});
|
||||
|
||||
function updateURL(newChapter) {
|
||||
chapter = newChapter;
|
||||
const newUrl = `/reader/${bookId}/${chapter}/${provider}`;
|
||||
window.history.pushState({}, '', newUrl);
|
||||
}
|
||||
|
||||
document.getElementById('back-btn').addEventListener('click', () => {
|
||||
const parts = window.location.pathname.split('/');
|
||||
|
||||
const mangaId = parts[2];
|
||||
window.location.href = `/book/${mangaId}`;
|
||||
});
|
||||
|
||||
settingsBtn.addEventListener('click', () => {
|
||||
panel.classList.add('open');
|
||||
overlay.classList.add('active');
|
||||
});
|
||||
|
||||
closePanel.addEventListener('click', closeSettings);
|
||||
overlay.addEventListener('click', closeSettings);
|
||||
|
||||
function closeSettings() {
|
||||
panel.classList.remove('open');
|
||||
overlay.classList.remove('active');
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && panel.classList.contains('open')) {
|
||||
closeSettings();
|
||||
}
|
||||
});
|
||||
|
||||
function enableMangaPageNavigation() {
|
||||
if (currentType !== 'manga') return;
|
||||
const logicalPages = [];
|
||||
|
||||
document.querySelectorAll('.manga-container > *').forEach(el => {
|
||||
if (el.classList.contains('double-container')) {
|
||||
logicalPages.push(el);
|
||||
} else if (el.tagName === 'IMG') {
|
||||
logicalPages.push(el);
|
||||
}
|
||||
});
|
||||
|
||||
if (logicalPages.length === 0) return;
|
||||
|
||||
function scrollToLogical(index) {
|
||||
if (index < 0 || index >= logicalPages.length) return;
|
||||
|
||||
const topBar = document.querySelector('.top-bar');
|
||||
const offset = topBar ? -topBar.offsetHeight : 0;
|
||||
|
||||
const y = logicalPages[index].getBoundingClientRect().top
|
||||
+ window.pageYOffset
|
||||
+ offset;
|
||||
|
||||
window.scrollTo({
|
||||
top: y,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentLogicalIndex() {
|
||||
let closest = 0;
|
||||
let minDist = Infinity;
|
||||
|
||||
logicalPages.forEach((el, i) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const dist = Math.abs(rect.top);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
closest = i;
|
||||
}
|
||||
});
|
||||
|
||||
return closest;
|
||||
}
|
||||
|
||||
const rtl = () => config.manga.direction === 'rtl';
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (currentType !== 'manga') return;
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return;
|
||||
|
||||
const index = getCurrentLogicalIndex();
|
||||
|
||||
if (e.key === 'ArrowLeft') {
|
||||
scrollToLogical(rtl() ? index + 1 : index - 1);
|
||||
}
|
||||
if (e.key === 'ArrowRight') {
|
||||
scrollToLogical(rtl() ? index - 1 : index + 1);
|
||||
}
|
||||
});
|
||||
|
||||
reader.addEventListener('click', (e) => {
|
||||
if (currentType !== 'manga') return;
|
||||
|
||||
const bounds = reader.getBoundingClientRect();
|
||||
const x = e.clientX - bounds.left;
|
||||
const half = bounds.width / 2;
|
||||
|
||||
const index = getCurrentLogicalIndex();
|
||||
|
||||
if (x < half) {
|
||||
scrollToLogical(rtl() ? index + 1 : index - 1);
|
||||
} else {
|
||||
scrollToLogical(rtl() ? index - 1 : index + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let resizeTimer;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
applyStyles();
|
||||
}, 250);
|
||||
});
|
||||
|
||||
if (!bookId || !chapter || !provider) {
|
||||
reader.innerHTML = `
|
||||
<div class="loading-container">
|
||||
<span style="color: #ef4444;">Missing required parameters (bookId, chapter, provider)</span>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
loadConfig();
|
||||
loadChapter();
|
||||
}
|
||||
Reference in New Issue
Block a user