/* global navHome, searchClear, searchWrapper, skeletonCardTemplate, bareCardTemplate, fullCardTemplate, home, searchText, searchGenre, searchYear, searchSeason, searchFormat, searchStatus, searchSort, navSchedule, homeContinueMore, homeReleasesMore, homePlanningMore, homeTrendingMore, homeRomanceMore, homeActionMore, homeContinue, homeReleases, homePlanning, homeTrending, homeRomance, homeAction */ // THIS IS WHY YOU FUCKING USE FRAMEWORKS import { alRequest } from './anilist.js' import { resolveFileMedia, relations, nyaaSearch } from './anime.js' import { getRSSurl, getRSSContent } from './rss.js' import { settings } from './settings.js' import { client } from './main.js' import { countdown, flattenObj, userBrowser } from './util.js' import halfmoon from 'halfmoon' export function loadHomePage () { const homeLoadElements = [navSchedule, homeContinueMore, homeReleasesMore, homePlanningMore, homeTrendingMore, homeRomanceMore, homeActionMore] const homePreviewElements = [homeContinue, homeReleases, homePlanning, homeTrending, homeRomance, homeAction] const homeSearchElements = [searchText, searchGenre, searchYear, searchSeason, searchFormat, searchStatus, searchSort] const browseGallery = document.querySelector('.browse') const homeLoadFunctions = { continue: async function (page) { if (!page) gallerySkeleton(browseGallery) const res = await alRequest({ method: 'UserLists', status_in: 'CURRENT', id: alID, page: page || 1 }) const mediaList = res.data.Page.mediaList.map(i => i.media).filter(media => { return media.status !== 'RELEASING' || media.mediaListEntry?.progress < media.nextAiringEpisode?.episode - 1 }) galleryAppend({ media: mediaList, gallery: browseGallery, method: 'continue', page: page || 1 }) return res.data.Page.pageInfo.hasNextPage }, releases: async function () { gallerySkeleton(browseGallery) const cards = await releasesCards((await getRSSContent(getRSSurl())).querySelectorAll('item')) browseGallery.textContent = '' browseGallery.append(...cards) home.classList.remove('loading') home.onscroll = undefined return false }, planning: async function (page) { if (!page) gallerySkeleton(browseGallery) const res = await alRequest({ method: 'UserLists', status_in: 'PLANNING', id: alID, page: page || 1 }) const mediaList = res.data.Page.mediaList galleryAppend({ media: mediaList.map(i => i.media), gallery: browseGallery, method: 'planning', page: page || 1 }) return res.data.Page.pageInfo.hasNextPage }, trending: async function () { gallerySkeleton(browseGallery) clearSearch() searchSort.value = 'TRENDING_DESC' return await homeLoadFunctions.search() }, romance: async function () { gallerySkeleton(browseGallery) clearSearch() searchGenre.value = 'romance' searchSort.value = 'TRENDING_DESC' return await homeLoadFunctions.search() }, action: async function () { gallerySkeleton(browseGallery) clearSearch() searchGenre.value = 'action' searchSort.value = 'TRENDING_DESC' return await homeLoadFunctions.search() }, schedule: async function (page) { if (!page) gallerySkeleton(browseGallery) const res = await alRequest({ method: 'AiringSchedule', page: page || 1 }) const mediaList = res.data.Page.airingSchedules galleryAppend({ media: mediaList.filter(entry => entry.media.countryOfOrigin !== 'CN' && entry.media.isAdult === false), gallery: browseGallery, method: 'schedule', page: page || 1, schedule: true }) return res.data.Page.pageInfo.hasNextPage }, search: async function (page) { const def = { method: 'Search', page: page || 1 } const opts = {} Object.assign(opts, def) for (const element of homeSearchElements) { if (element.value) opts[element.dataset.option] = element.value } if (Object.keys(def).length !== Object.keys(opts).length) { const res = await alRequest(opts) const mediaList = res.data.Page.media galleryAppend({ media: mediaList, gallery: browseGallery, method: 'search', page: page || 1 }) return res.data.Page.pageInfo.hasNextPage } else { searchClear.classList.remove('text-primary') home.classList.remove('loading') reloadHome() return false } } } const homePreviewFunctions = { continue: function () { alRequest({ method: 'UserLists', status_in: 'CURRENT', id: alID, perPage: 50 }).then(res => { const mediaList = res.data.Page.mediaList.filter(({ media }) => { return media.status !== 'RELEASING' || media.mediaListEntry?.progress < media.nextAiringEpisode?.episode - 1 }).slice(0, 5).map(i => i.media) galleryAppend({ media: mediaList, gallery: homeContinue }) }) }, releases: async function () { // this could be cleaner, but oh well const doc = await getRSSContent(getRSSurl()) if (doc) { const pubDate = doc.querySelector('pubDate').textContent if (lastRSSDate !== pubDate) { if (lastRSSDate) { homeReleases.textContent = '' homeReleases.append(...gallerySkeletonFrag(5)) if (settings.other1) { resolveFileMedia({ fileName: doc.querySelector('item title').textContent, isRelease: true }).then(mediaInformation => { const notification = new Notification(mediaInformation.media.title.userPreferred, { body: `Episode ${mediaInformation.episodeNumber} was just released!`, icon: mediaInformation.media.coverImage.medium }) notification.onclick = async () => { window.parent.focus() relations[mediaInformation.parseObject.anime_title] = (await alRequest({ id: mediaInformation.media.id, method: 'SearchIDSingle' })).data.Media.id client.playTorrent(doc.querySelector('item link').textContent, { media: mediaInformation, episode: mediaInformation.episode }) } }) } } lastRSSDate = pubDate const cards = await releasesCards(doc.querySelectorAll('item'), 5) homeReleases.textContent = '' homeReleases.append(...cards) } } }, planning: function () { alRequest({ method: 'UserLists', status_in: 'PLANNING', id: alID, perPage: 5 }).then(res => { galleryAppend({ media: res.data.Page.mediaList.map(i => i.media), gallery: homePlanning }) }) }, trending: function () { alRequest({ method: 'Search', id: alID, perPage: 5, sort: 'TRENDING_DESC' }).then(res => { galleryAppend({ media: res.data.Page.media, gallery: homeTrending }) }) }, romance: function () { alRequest({ method: 'Search', genre: 'Romance', perPage: 5, sort: 'TRENDING_DESC' }).then(res => { galleryAppend({ media: res.data.Page.media, gallery: homeRomance }) }) }, action: function () { alRequest({ method: 'Search', genre: 'Action', perPage: 5, sort: 'TRENDING_DESC' }).then(res => { galleryAppend({ media: res.data.Page.media, gallery: homeAction }) }) } } const gallerySkeletonFrag = function (limit) { const cards = [] while (limit--) { cards.push(skeletonCard.cloneNode(true)) } return cards } let loadTimeout let lastDate let lastRSSDate let canScroll = true function gallerySkeleton (gallery) { home.classList.add('loading') gallery.textContent = '' gallery.append(...gallerySkeletonFrag(10)) } function galleryAppend (opts) { if (opts.page === 1) { home.classList.remove('loading') } if (opts.method) { canScroll = false home.onscroll = function () { if (this.scrollTop + this.clientHeight > this.scrollHeight - 800 && !loadTimeout && canScroll) { canScroll = false loadTimeout = setTimeout(function () { loadTimeout = undefined }, 1000) homeLoadFunctions[opts.method](opts.page + 1).then((hasNextPage) => { canScroll = hasNextPage }) } } } if (!opts.page || opts.page === 1) { opts.gallery.textContent = '' } const date = new Date() const cards = [] opts.media.forEach(media => { if (opts.schedule) { if (media.timeUntilAiring && (!lastDate || (new Date(+date + media.timeUntilAiring * 1000).getDay() !== lastDate.getDay()))) { const div = document.createElement('div') lastDate = new Date(+date + media.timeUntilAiring * 1000) div.classList.add('day-row', 'font-size-24', 'font-weight-bold', 'h-50', 'd-flex', 'align-items-end') div.textContent = lastDate.toLocaleDateString('en-US', { weekday: 'long' }) cards.push(div) } media = media.media } cards.push(cardCreator({ media: media, schedule: opts.schedule, onclick: () => viewMedia(media) })) }) opts.gallery.append(...cards) canScroll = true } for (const item of homePreviewElements) { homePreviewFunctions[item.dataset.function]() } setInterval(homePreviewFunctions.releases, 30000) for (const item of homeLoadElements) { item.onclick = function () { home.scrollTop = 0 home.classList.add('browsing') searchClear.classList.add('text-primary') homeLoadFunctions[item.dataset.function]() } } function reloadHome () { clearSearch() searchClear.classList.remove('text-primary') home.classList.remove('browsing') lastRSSDate = undefined home.onscroll = undefined home.scrollTop = 0 for (const item of homePreviewElements) { item.textContent = '' item.append(...gallerySkeletonFrag(5)) homePreviewFunctions[item.dataset.function]() } document.querySelector('.browse').textContent = '' } navHome.onclick = reloadHome searchClear.onclick = reloadHome function clearSearch () { for (const element of homeSearchElements) { element.value = '' } } let searchTimeout searchWrapper.oninput = () => { if (!searchTimeout) { home.classList.add('browsing') gallerySkeleton(browseGallery) home.scrollTop = 0 } else { clearTimeout(searchTimeout) } searchClear.classList.add('text-primary') searchTimeout = setTimeout(() => { homeLoadFunctions.search() searchTimeout = undefined }, 500) } } const skeletonCard = skeletonCardTemplate.cloneNode(true).content const bareCard = bareCardTemplate.cloneNode(true).content const fullCard = fullCardTemplate.cloneNode(true).content export function cardCreator (opts) { if (opts.media) { const card = fullCard.cloneNode(true) const nodes = card.querySelectorAll('*') nodes[0].onclick = opts.onclick nodes[0].style = `--color:${opts.media.coverImage.color || '#1890ff'};` nodes[3].src = opts.media.coverImage.extraLarge || '' nodes[6].textContent = [opts.media.title.userPreferred, opts.episodeNumber].filter(s => s).join(' - ') if (opts.schedule && opts.media.nextAiringEpisode) nodes[7].textContent = 'EP ' + opts.media.nextAiringEpisode.episode + ' in ' + countdown(opts.media.nextAiringEpisode.timeUntilAiring) nodes[8].innerHTML = '' + [ opts.media.format === 'TV' ? 'TV Show' : opts.media.format?.toLowerCase().replace(/_/g, ' '), opts.media.episodes ? opts.media.episodes + ' Episodes' : opts.media.duration ? opts.media.duration + ' Minutes' : undefined, opts.media.status?.toLowerCase().replace(/_/g, ' '), [opts.media.season?.toLowerCase(), opts.media.seasonYear].filter(s => s).join(' ') ].filter(s => s).join('') + '' nodes[13].innerHTML = opts.media.description nodes[14].innerHTML = opts.media.genres.map(key => (`${key} `)).join('') return card } else if (opts.parseObject) { const card = bareCard.cloneNode(true) card.querySelector('h5').textContent = [opts.parseObject.anime_title, opts.parseObject.episode_number].filter(s => s).join(' - ') card.firstElementChild.onclick = opts.onclick return card } return skeletonCard.cloneNode(true) } async function releasesCards (items, limit) { const cards = [] const media = await resolveFileMedia({ fileName: [...items].map(item => item.querySelector('title').textContent).slice(0, limit), isRelease: true }) media.forEach((mediaInformation, index) => { mediaInformation.onclick = () => client.playTorrent(items[index].querySelector('link').textContent, { media: mediaInformation, episode: mediaInformation.episode }) cards.push(cardCreator(mediaInformation)) }) localStorage.setItem('relations', JSON.stringify(relations)) return cards } const genreBadge = document.createElement('span') genreBadge.classList.add('badge', 'badge-pill', 'shadow') function genreBadges (genres = []) { const badges = [] for (const genre of genres) { const badge = genreBadge.cloneNode(true) badge.textContent = genre badges.push(badge) } return badges } const detailsMap = [ { property: 'episode', label: 'Airing', icon: 'schedule', custom: 'property' }, { property: 'genres', label: 'Genres', icon: 'theater_comedy' }, { property: 'season', label: 'Season', icon: 'spa', custom: 'property' }, { property: 'episodes', label: 'Episodes', icon: 'theaters', custom: 'property' }, { property: 'duration', label: 'Duration', icon: 'timer' }, { property: 'format', label: 'Format', icon: 'monitor' }, { property: 'status', label: 'Status', icon: 'live_tv' }, { property: 'nodes', label: 'Studio', icon: 'business' }, { property: 'source', label: 'Source', icon: 'source' }, { property: 'averageScore', label: 'Rating', icon: 'trending_up', custom: 'property' }, { property: 'english', label: 'English', icon: 'title' }, { property: 'romaji', label: 'Romaji', icon: 'translate' }, { property: 'native', label: 'Native', icon: '語', custom: 'icon' } ] /* global detailTemplate */ const detailTemp = detailTemplate.cloneNode(true).content function mediaDetails (media) { const details = [] for (const detail of detailsMap) { if (media[detail.property]) { const clone = detailTemp.cloneNode(true) const nodes = clone.querySelectorAll('*') switch (media[detail.property].constructor) { case String: { nodes[4].textContent = media[detail.property].replace(/_/g, ' ').toLowerCase() break } case Number: { nodes[4].textContent = media[detail.property] break } case Array: { if (detail.property === 'nodes') { nodes[4].textContent = media[detail.property][0] && media[detail.property][0].name } else { nodes[4].textContent = media[detail.property].join(', ').replace(/_/g, ' ').toLowerCase() } } } nodes[1].textContent = detail.icon nodes[3].textContent = detail.label switch (detail.custom) { case 'icon': { nodes[1].classList.remove('material-icons', 'font-size-24') nodes[1].classList.add('d-flex', 'align-items-center', 'text-nowrap', 'font-size-12', 'font-weight-bold') break } case 'property': { if (detail.property === 'episodes' && media.progress) { nodes[4].innerHTML = `Watched ${media.progress} of ${media.episodes}` } else if (detail.property === 'averageScore') { nodes[4].textContent = media.averageScore + '%' } else if (detail.property === 'season') { nodes[4].textContent = [media.season?.toLowerCase(), media.seasonYear].filter(f => f).join(' ') } else if (detail.property === 'episode') { nodes[4].textContent = `Ep ${media.episode}: ${countdown(media.timeUntilAiring)}` } else { nodes[4].textContent = media[detail.property] } } } details.push(clone) } } return details } function trailerPopup (trailer) { trailerVideo.src = '' halfmoon.toggleModal('trailer') switch (trailer.site) { // should support the other possible sites too, but i cant find any examples case 'youtube': trailerVideo.src = 'https://www.youtube.com/embed/' + trailer.id break } } trailerClose.onclick = () => { trailerVideo.src = '' } navNowPlaying.onclick = () => { if (client.nowPlaying?.media) viewMedia(client.nowPlaying.media) } /* global trailerVideo, viewAnime, trailerClose, navNowPlaying */ const viewNodes = viewAnime.querySelectorAll('*') export function viewMedia (input, episode) { halfmoon.showModal('viewAnime') const media = flattenObj(input) viewNodes[9].src = media.extraLarge || media.medium viewNodes[13].textContent = media.userPreferred viewNodes[17].textContent = media.averageScore + '%' viewNodes[20].textContent = media.format === 'TV' ? media.format : media.format?.toLowerCase() if (media.episodes === 1 || (!media.episodes && !media.episode)) { viewNodes[14].classList.add('movie') viewNodes[31].classList.add('movie') viewNodes[28].textContent = media.duration + ' min' } else { viewNodes[31].classList.remove('movie') viewNodes[14].classList.remove('movie') viewNodes[24].textContent = media.episodes } viewNodes[29].textContent = '' viewNodes[29].append(...genreBadges(media.genres)) if (media.episodes || media.episode) { viewNodes[31].classList.remove('hidden') viewNodes[33].onclick = () => { nyaaSearch(input, Number(viewNodes[44].value) || 1); halfmoon.hideModal('viewAnime') } viewNodes[44].value = episode || Math.min(Number(media.progress) + 1 || 1, media.episodes) viewNodes[35].textContent = media.progress && media.progress !== media.episodes ? 'Continue' : 'Play' viewNodes[46].onclick = () => { nyaaSearch(input, Number(viewNodes[56].value) || 1, true); halfmoon.hideModal('viewAnime') } viewNodes[56].value = episode || Math.min(Number(media.progress) + 1 || 1, media.episodes) } else { viewNodes[31].classList.add('hidden') } if (media.bannerImage) { viewNodes[4].style = `background-image: linear-gradient(0deg, rgba(17,20,23,1) 0%, rgba(17,20,23,0.80) 25%, rgba(17,20,23,0.40) 50%, rgba(37,40,44,0) 100%), url('${media.bannerImage}') !important` } else { viewNodes[4].style = 'background-image: linear-gradient(0deg, rgba(17,20,23,1) 0%, rgba(17,20,23,0.80) 25%, rgba(17,20,23,0.40) 50%, rgba(37,40,44,0) 100%) !important' } viewNodes[57].onclick = () => trailerPopup(input.trailer) viewNodes[63].innerHTML = media.description viewNodes[66].textContent = '' viewNodes[66].append(...mediaDetails(media)) } export let alID // login icon /* global oauth */ export function initMenu () { if (localStorage.getItem('ALtoken')) { alRequest({ method: 'Viewer' }).then(result => { oauth.removeAttribute('href') oauth.setAttribute('data-title', `${result.data.Viewer.name}\nClick To Logout`) oauth.innerHTML = ` ${result.data.Viewer.name} logout ` oauth.onclick = () => { localStorage.removeItem('ALtoken') location.reload() } alID = result.data.Viewer.id loadHomePage() }) } else { loadHomePage() home.classList.add('noauth') } } if (userBrowser === 'firefox') { halfmoon.initStickyAlert({ content: 'Your browser will likely not support many video formats, containers and features. Please use Chromium!', title: 'Bad Browser', alertType: 'alert-danger', fillType: '' }) }