galery, main menu cards, searching

This commit is contained in:
ThaUnknown 2022-03-09 19:31:18 +01:00
parent ce393b8fc3
commit 42edf08372
12 changed files with 323 additions and 51 deletions

View file

@ -5,6 +5,20 @@ const path = require('path')
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow
function UpsertKeyValue (obj, keyToChange, value) {
const keyToChangeLower = keyToChange.toLowerCase()
for (const key of Object.keys(obj)) {
if (key.toLowerCase() === keyToChangeLower) {
// Reassign old key
obj[key] = value
// Done
return
}
}
// Insert at end instead
obj[keyToChange] = value
}
function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({
@ -24,6 +38,20 @@ function createWindow () {
show: false
})
mainWindow.removeMenu()
mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
(details, callback) => {
const { requestHeaders } = details
UpsertKeyValue(requestHeaders, 'Access-Control-Allow-Origin', ['*'])
callback({ requestHeaders })
}
)
mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => {
const { responseHeaders } = details
UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Origin', ['*'])
UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Headers', ['*'])
callback({ responseHeaders })
})
// This block of code is intended for development purpose only.
// Delete this entire block of code when you are ready to package the application.

View file

@ -1,17 +1,13 @@
<script>
import Sidebar from './lib/Sidebar.svelte'
import Router from './lib/Router.svelte'
// import anitomyscript from 'anitomyscript'
// const WebTorrent = require('webtorrent')
// const wt = new WebTorrent()
// console.log(wt)
// anitomyscript('[GJM] Irozuku Sekai no Ashita kara - 08 [F0C587E8].mkv').then(console.log)
let page = 'home'
</script>
<div class="page-wrapper with-sidebar" data-sidebar-type="full-height overlayed-sm-and-down" data-sidebar-hidden="hidden">
<div class="sticky-alerts" />
<div class="sidebar-overlay" onclick="halfmoon.toggleSidebar()" />
<!-- svelte-ignore missing-declaration -->
<div class="sidebar-overlay" on:click={halfmoon.toggleSidebar.bind(halfmoon)} />
<Sidebar bind:page />
<Router bind:page />
</div>

View file

@ -98,6 +98,14 @@
height: 27rem !important;
box-shadow: rgba(0, 4, 12, 0.3) 0px 7px 15px, rgba(0, 4, 12, 0.05) 0px 4px 4px;
}
.card-grid {
display: grid;
grid-template-rows: auto 1fr auto;
}
.badge-color {
background-color: var(--color) !important;
border-color: var(--color) !important;
}
.card:hover {
transform: scale(1.05);
@ -112,6 +120,18 @@
left: 100%;
}
}
@keyframes load-in {
from {
bottom: -1.2rem;
transform: scale(0.95);
}
to {
bottom: 0;
transform: scale(1);
}
}
.skeloader {
position: relative;
overflow: hidden;
@ -126,4 +146,8 @@
background: linear-gradient(to right, transparent 0%, #25282c 50%, transparent 100%);
animation: load 1s infinite cubic-bezier(0.4, 0, 0.2, 1);
}
.cover-img {
object-fit: cover;
background-color: var(--color) !important;
}
</style>

View file

@ -0,0 +1,3 @@
<script context="module">
export const alToken = localStorage.getItem('ALtoken') || null
</script>

View file

@ -0,0 +1,44 @@
<script>
import Cards from '@/lib/Cards.svelte'
// TODO: infinite scrolling
export let media
$: update(media)
let loading = true
async function update(media) {
loading = true
await media
loading = false
}
</script>
<div class="gallery browse" class:loading>
<Cards cards={media} />
</div>
<style>
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, 50rem);
grid-auto-rows: auto;
justify-content: center;
grid-gap: 2rem;
padding: 2rem 4rem;
position: relative;
}
.loading {
height: 30rem !important;
overflow-y: hidden !important;
}
.loading:after {
content: '';
position: absolute;
bottom: 0;
height: 10rem;
width: 100%;
background: linear-gradient(0deg, rgba(37, 40, 44, 1) 0%, rgba(37, 40, 44, 1) 15%, rgba(37, 40, 44, 0.45) 70%, rgba(37, 40, 44, 0) 100%);
}
</style>

View file

@ -1,53 +1,115 @@
<script>
import Search from './Search.svelte'
import Section from './Section.svelte'
import Gallery from './Gallery.svelte'
import { alToken } from '../Settings.svelte'
import { alRequest } from '@/modules/anilist.js'
import { resolveFileMedia, relations } from '@/modules/anime.js'
import { getRSSContent, getRSSurl } from '@/modules/rss.js'
let media = null
let search
function processMedia(res) {
return res.data.Page.media.map(media => {
return { media }
})
}
setInterval(async () => {
const media = await releasesCards(5)
if (media) sections[1].cards = media
}, 30000)
let lastRSSDate = 0
async function releasesCards(limit, force) {
const doc = await getRSSContent(getRSSurl())
if (doc) {
const pubDate = doc.querySelector('pubDate').textContent
if (force || lastRSSDate !== pubDate) {
lastRSSDate = pubDate
const items = doc.querySelectorAll('item')
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 })
})
localStorage.setItem('relations', JSON.stringify(relations))
return media
}
}
}
// TODO: add AL account detection for hiding
const sections = [
{
title: 'Continue Watching',
click: () => {},
cards: new Promise(() => {}),
hide: true
click: () => {
media = alRequest({ method: 'UserLists', status_in: 'CURRENT' }).then(res => {
return res.data.Page.mediaList.filter(i => {
return i.media.status !== 'RELEASING' || i.media.mediaListEntry?.progress < i.media.nextAiringEpisode?.episode - 1
})
})
},
cards: alRequest({ method: 'UserLists', status_in: 'CURRENT' }).then(res => {
return res.data.Page.mediaList.filter(i => {
return i.media.status !== 'RELEASING' || i.media.mediaListEntry?.progress < i.media.nextAiringEpisode?.episode - 1
}).slice(0, 5)
}),
hide: !alToken
},
{
title: 'New Releases',
click: () => {},
cards: new Promise(() => {})
click: () => {
media = releasesCards(200, true)
},
cards: releasesCards(5)
},
{
title: 'Your List',
click: () => {},
cards: new Promise(() => {}),
hide: true
click: () => {
media = alRequest({ method: 'UserLists', status_in: 'PLANNING' }).then(res => res.data.Page.mediaList)
},
cards: alRequest({ method: 'UserLists', status_in: 'PLANNING', perPage: 5 }).then(res => res.data.Page.mediaList),
hide: !alToken
},
{
title: 'Trending Now',
click: () => {},
cards: new Promise(() => {})
click: () => {
search.sort = 'TRENDING_DESC'
},
cards: alRequest({ method: 'Search', perPage: 5, sort: 'TRENDING_DESC' }).then(res => processMedia(res))
},
{
title: 'Romance',
click: () => {},
cards: new Promise(() => {})
click: () => {
search.sort = 'TRENDING_DESC'
search.genre = 'romance'
},
cards: alRequest({ method: 'Search', perPage: 5, genre: 'Romance', sort: 'TRENDING_DESC' }).then(res => processMedia(res))
},
{
title: 'Action',
click: () => {},
cards: new Promise(() => {})
click: () => {
search.sort = 'TRENDING_DESC'
search.genre = 'action'
},
cards: alRequest({ method: 'Search', perPage: 5, genre: 'Action', sort: 'TRENDING_DESC' }).then(res => processMedia(res))
}
]
</script>
<div class="d-flex h-full flex-column overflow-y-scroll root">
<div class="h-full py-10">
<Search />
<div>
{#each sections as opts (opts.title)}
{#if !opts.hide}
<Section {opts} />
{/if}
{/each}
</div>
<Search bind:media bind:search />
{#if media}
<Gallery {media} />
{:else}
<div>
{#each sections as opts (opts.title)}
{#if !opts.hide}
<Section {opts} />
{/if}
{/each}
</div>
{/if}
</div>
</div>

View file

@ -1,6 +1,14 @@
<script>
import { alRequest } from '@/modules/anilist.js'
export let search = {}
export let media = null
let searchTimeout = null
function processMedia(res) {
return res.data.Page.media.map(media => {
return { media }
})
}
function searchClear() {
search = {
format: '',
@ -9,11 +17,12 @@
sort: '',
status: ''
}
console.log('// TODO: hide searching state')
media = null
}
$: input(search)
function input() {
if (!searchTimeout) {
console.log('// TODO: present searching state')
if (Object.values(search).filter(v => v).length) media = new Promise(() => {})
} else {
clearTimeout(searchTimeout)
}
@ -33,21 +42,17 @@
if (value) opts[key] = value
}
if (Object.keys(defaults).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 })
console.log('// TODO: search using `search` object', search)
// return res.data.Page.pageInfo.hasNextPage
media = alRequest(opts).then(res => {
return processMedia(res)
})
} else {
console.log('// TODO: hide searching state')
return false
media = null
}
}
</script>
<div class="container-fluid row p-20">
<!-- bad, bubbling cuz lazy applies input event to all child elements -->
<div class="col-lg col-4 p-10 d-flex flex-column justify-content-end" on:input={input}>
<div class="col-lg col-4 p-10 d-flex flex-column justify-content-end">
<div class="pb-10 font-size-24 font-weight-semi-bold d-flex">
<div class="material-icons mr-10 font-size-30">title</div>
Title
@ -60,7 +65,7 @@
type="search"
class="form-control bg-dark border-left-0 shadow-none text-capitalize"
autocomplete="off"
bind:value={search.text}
bind:value={search.search}
data-option="search"
placeholder="Any" />
</div>
@ -149,7 +154,7 @@
</select>
</div>
<div class="col-auto p-10 d-flex">
<button class="btn bg-dark material-icons font-size-18 px-5 align-self-end shadow-lg border-0" type="button" on:click={searchClear}>delete</button>
<button class="btn bg-dark material-icons font-size-18 px-5 align-self-end shadow-lg border-0" type="button" on:click={searchClear} class:text-primary={!!media}>delete</button>
</div>
</div>
@ -173,4 +178,7 @@
input:invalid {
box-shadow: 0 0 0 0.2rem var(--danger-color) !important;
}
select.form-control:invalid {
color: var(--dm-input-placeholder-text-color);
}
</style>

View file

@ -1,10 +1,6 @@
<script>
import Cards from '../../Cards.svelte'
export let opts = {
click: () => {},
title: '',
cards: undefined
}
import Cards from '@/lib/Cards.svelte'
export let opts = {}
</script>
<span class="d-flex px-20 align-items-end pointer text-decoration-none text-muted" on:click={opts.click}>

View file

@ -1,6 +1,11 @@
/* global halfmoon */
import { alToken } from '@/lib/pages/Settings.svelte'
const alID = 'TODO: add al id'
export const alID =
!!alToken &&
alRequest({ method: 'Viewer', token: alToken }).then(result => {
return result.data.Viewer.id
})
async function handleRequest (opts) {
return await fetch('https://graphql.anilist.co', opts).then(async res => {
@ -189,6 +194,7 @@ query ($id: [Int], $type: MediaType, $page: Int, $perPage: Int) {
}`
break
} case 'Viewer': {
variables.id = alToken
query = `
query {
Viewer {
@ -201,7 +207,7 @@ query {
}`
break
} case 'UserLists': {
variables.id = opts.id
variables.id = await alID
query = `
query ($page: Int, $perPage: Int, $id: Int, $type: MediaType, $status_in: [MediaListStatus]){
Page (page: $page, perPage: $perPage) {
@ -217,7 +223,7 @@ query ($page: Int, $perPage: Int, $id: Int, $type: MediaType, $status_in: [Media
}`
break
} case 'SearchIDStatus': {
variables.id = alID
variables.id = await alID
variables.mediaId = opts.id
query = `
query ($id: Int, $mediaId: Int){

View file

@ -0,0 +1,103 @@
/* global halfmoon */
// import { settings } from './settings.js'
import { DOMPARSER } from './util.js'
import { client } from './torrent.js'
import { episodeRx } from './anime.js'
const settings = {}
// TODO: settings
export function getRSSContent (url) {
return fetch(url).then(res => {
if (res.ok) {
return res.text().then(xmlTxt => {
return DOMPARSER(xmlTxt, 'text/xml')
})
}
throw Error(res.statusText)
}).catch(error => {
halfmoon.initStickyAlert({
content: 'Failed fetching RSS!<br>' + error,
title: 'Search Failed',
alertType: 'alert-danger',
fillType: ''
})
console.error(error)
})
}
export async function nyaaRss (media, episode, isOffline) {
const frag = document.createDocumentFragment()
const titles = [...new Set(Object.values(media.title).concat(media.synonyms).filter(name => name != null))].join(')|(').replace(/&/g, '%26')
const ep = (media.episodes !== 1 && ((media.status === 'FINISHED' && settings.torrent9) ? `"01-${media.episodes}"|"01~${media.episodes}"|"Batch"|"Complete"|"+${episode}+"|"+${episode}v"|"S01"` : `"+${episode}+"|"+${episode}v"`)) || ''
const excl = ['DTS', 'AC3', 'HEVC', 'x265', 'H.265'].join('|')
const quality = `${settings.torrent1}` || '"1080p"'
const trusted = settings.torrent3 === true ? 2 : 0
const url = new URL(`https://meow.miru.workers.dev/?page=rss&c=1_2&f=${trusted}&s=seeders&o=desc&q=(${titles})${ep}${quality}-(${excl})`)
const nodes = (await getRSSContent(url)).querySelectorAll('item *')
if (!nodes.length) return frag
const entries = []
for (let index = Math.floor(nodes.length / 15); index--;) {
const position = index * 15
entries[index] = {
title: nodes[position].textContent,
hash: nodes[position + 1].textContent,
seeders: nodes[position + 4].textContent,
leechers: nodes[position + 5].textContent,
downloads: nodes[position + 6].textContent,
size: nodes[position + 10].textContent
}
}
entries.sort((a, b) => b.seeders - a.seeders)
const streamingEpisode = media?.streamingEpisodes.filter(episode => episodeRx.exec(episode.title) && Number(episodeRx.exec(episode.title)[1]) === Number(episode))[0]
const fileMedia = {
mediaTitle: media?.title.userPreferred,
episodeNumber: Number(episode),
episodeTitle: streamingEpisode ? episodeRx.exec(streamingEpisode.title)[2] : undefined,
episodeThumbnail: streamingEpisode?.thumbnail,
mediaCover: media?.coverImage.medium,
name: 'Miru',
media: media
}
if (settings.torrent2) {
if (isOffline) {
client.offlineDownload(entries[0].hash)
} else {
client.playTorrent(entries[0].hash, { media: fileMedia, episode: episode })
}
halfmoon.toggleModal('tsearch')
}
entries.forEach((entry, index) => {
const template = document.createElement('tr')
template.innerHTML += `
<th>${(index + 1)}</th>
<td>${entry.title}</td>
<td>${entry.size}</td>
<td>${entry.seeders}</td>
<td>${entry.leechers}</td>
<td>${entry.downloads}</td>
<td class="pointer">Play</td>`
template.onclick = () => {
if (isOffline) {
client.offlineDownload(entry.hash)
} else {
client.playTorrent(entry.hash, { media: fileMedia, episode: episode })
}
halfmoon.hideModal('tsearch')
}
frag.appendChild(template)
})
return frag
}
export function getRSSurl () {
// TODO: settings shit
return 'https://nyaa.si/?page=rss&c=0_0&f=0&u=subsplease&q="1080"'
// if (Object.values(torrent4list.options).filter(item => item.value === settings.torrent4)[0]) {
// return new URL(Object.values(torrent4list.options).filter(item => item.value === settings.torrent4)[0].textContent + settings.torrent1)
// } else {
// return new URL(settings.torrent4 + settings.torrent1) // add custom RSS
// }
}

View file

@ -1 +1,3 @@
export const client = null
const WebTorrent = require('webtorrent')
export const client = new WebTorrent()
console.log(client)