mirror of
https://github.com/NoCrypt/migu.git
synced 2026-04-18 23:22:05 +00:00
galery, main menu cards, searching
This commit is contained in:
parent
ce393b8fc3
commit
42edf08372
12 changed files with 323 additions and 51 deletions
28
src/index.js
28
src/index.js
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
<script context="module">
|
||||
export const alToken = localStorage.getItem('ALtoken') || null
|
||||
</script>
|
||||
44
src/renderer/src/lib/pages/home/Gallery.svelte
Normal file
44
src/renderer/src/lib/pages/home/Gallery.svelte
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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){
|
||||
|
|
|
|||
103
src/renderer/src/modules/rss.js
Normal file
103
src/renderer/src/modules/rss.js
Normal 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
|
||||
// }
|
||||
}
|
||||
|
|
@ -1 +1,3 @@
|
|||
export const client = null
|
||||
const WebTorrent = require('webtorrent')
|
||||
export const client = new WebTorrent()
|
||||
console.log(client)
|
||||
|
|
|
|||
Loading…
Reference in a new issue