mirror of
https://github.com/NoCrypt/migu.git
synced 2026-03-11 17:45:32 +00:00
paste a bunch of code
This commit is contained in:
parent
70f804ea7f
commit
ce393b8fc3
5 changed files with 541 additions and 4 deletions
|
|
@ -1,11 +1,13 @@
|
|||
<script>
|
||||
import Search from './Search.svelte'
|
||||
import Section from './Section.svelte'
|
||||
// TODO: add AL account detection for hiding
|
||||
const sections = [
|
||||
{
|
||||
title: 'Continue Watching',
|
||||
click: () => {},
|
||||
cards: new Promise(() => {})
|
||||
cards: new Promise(() => {}),
|
||||
hide: true
|
||||
},
|
||||
{
|
||||
title: 'New Releases',
|
||||
|
|
@ -15,7 +17,8 @@
|
|||
{
|
||||
title: 'Your List',
|
||||
click: () => {},
|
||||
cards: new Promise(() => {})
|
||||
cards: new Promise(() => {}),
|
||||
hide: true
|
||||
},
|
||||
{
|
||||
title: 'Trending Now',
|
||||
|
|
@ -39,8 +42,10 @@
|
|||
<div class="h-full py-10">
|
||||
<Search />
|
||||
<div>
|
||||
{#each sections as opts, i (i)}
|
||||
<Section {opts} />
|
||||
{#each sections as opts (opts.title)}
|
||||
{#if !opts.hide}
|
||||
<Section {opts} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,282 @@
|
|||
/* global halfmoon */
|
||||
|
||||
const alID = 'TODO: add al id'
|
||||
|
||||
async function handleRequest (opts) {
|
||||
return await fetch('https://graphql.anilist.co', opts).then(async res => {
|
||||
const json = await res.json()
|
||||
if (!res.ok) {
|
||||
for (const error of json.errors) {
|
||||
halfmoon.initStickyAlert({
|
||||
content: `Failed making request to anilist!<br>${error.status} - ${error.message}`,
|
||||
title: 'Search Failed',
|
||||
alertType: 'alert-danger',
|
||||
fillType: ''
|
||||
})
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
return json
|
||||
})
|
||||
}
|
||||
|
||||
export function alEntry (filemedia) {
|
||||
if (filemedia.media && localStorage.getItem('ALtoken')) {
|
||||
alRequest({ method: 'SearchIDStatus', id: filemedia.media.id }).then(res => {
|
||||
if ((res.errors && res.errors[0].status === 404) || res.data.MediaList.progress <= filemedia.episodeNumber || filemedia.episodes === 1) {
|
||||
const query = `
|
||||
mutation ($id: Int, $status: MediaListStatus, $episode: Int, $repeat: Int) {
|
||||
SaveMediaListEntry (mediaId: $id, status: $status, progress: $episode, repeat: $repeat) {
|
||||
id,
|
||||
status,
|
||||
progress,
|
||||
repeat
|
||||
}
|
||||
}`
|
||||
const variables = {
|
||||
repeat: 0,
|
||||
id: filemedia.media.id,
|
||||
status: 'CURRENT',
|
||||
episode: filemedia.episodeNumber || 1
|
||||
}
|
||||
if (filemedia.episodeNumber === filemedia.media.episodes || filemedia.episodes === 1) {
|
||||
variables.status = 'COMPLETED'
|
||||
if (res.data.MediaList.status === 'COMPLETED') {
|
||||
variables.repeat = res.data.MediaList.repeat + 1
|
||||
}
|
||||
}
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + localStorage.getItem('ALtoken'),
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: query,
|
||||
variables: variables
|
||||
})
|
||||
}
|
||||
handleRequest(options)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function alRequest (opts) {
|
||||
let query
|
||||
const variables = {
|
||||
type: 'ANIME',
|
||||
sort: opts.sort || 'TRENDING_DESC',
|
||||
page: opts.page || 1,
|
||||
perPage: opts.perPage || 30,
|
||||
status_in: opts.status_in || '[CURRENT,PLANNING]',
|
||||
chunk: opts.chunk || 1,
|
||||
perchunk: opts.perChunk || 30
|
||||
}
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json'
|
||||
}
|
||||
}
|
||||
const queryObjects = `
|
||||
id,
|
||||
title {
|
||||
romaji,
|
||||
english,
|
||||
native,
|
||||
userPreferred
|
||||
},
|
||||
description(
|
||||
asHtml: true
|
||||
),
|
||||
season,
|
||||
seasonYear,
|
||||
format,
|
||||
status,
|
||||
episodes,
|
||||
duration,
|
||||
averageScore,
|
||||
genres,
|
||||
coverImage {
|
||||
extraLarge,
|
||||
medium,
|
||||
color
|
||||
},
|
||||
countryOfOrigin,
|
||||
isAdult,
|
||||
bannerImage,
|
||||
synonyms,
|
||||
nextAiringEpisode {
|
||||
timeUntilAiring,
|
||||
episode
|
||||
},
|
||||
trailer {
|
||||
id,
|
||||
site
|
||||
},
|
||||
streamingEpisodes {
|
||||
title,
|
||||
thumbnail
|
||||
},
|
||||
mediaListEntry {
|
||||
progress
|
||||
},
|
||||
source,
|
||||
studios(isMain: true) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
},
|
||||
relations {
|
||||
edges {
|
||||
relationType(version:2)
|
||||
node {
|
||||
id,
|
||||
title {
|
||||
userPreferred
|
||||
},
|
||||
coverImage {
|
||||
medium
|
||||
},
|
||||
type,
|
||||
status,
|
||||
format,
|
||||
episodes
|
||||
}
|
||||
}
|
||||
}`
|
||||
if (opts.status) variables.status = opts.status
|
||||
if (localStorage.getItem('ALtoken')) options.headers.Authorization = localStorage.getItem('ALtoken')
|
||||
switch (opts.method) {
|
||||
case 'SearchName': {
|
||||
variables.search = opts.name
|
||||
query = `
|
||||
query ($page: Int, $perPage: Int, $sort: [MediaSort], $type: MediaType, $search: String, $status: [MediaStatus]) {
|
||||
Page (page: $page, perPage: $perPage) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
},
|
||||
media(type: $type, search: $search, sort: $sort, status_in: $status) {
|
||||
${queryObjects}
|
||||
}
|
||||
}
|
||||
}`
|
||||
break
|
||||
} case 'SearchIDSingle': {
|
||||
variables.id = opts.id
|
||||
query = `
|
||||
query ($id: Int, $type: MediaType) {
|
||||
Media (id: $id, type: $type) {
|
||||
${queryObjects}
|
||||
}
|
||||
}`
|
||||
break
|
||||
} case 'SearchIDS': {
|
||||
variables.id = opts.id
|
||||
query = `
|
||||
query ($id: [Int], $type: MediaType, $page: Int, $perPage: Int) {
|
||||
Page (page: $page, perPage: $perPage) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
},
|
||||
media (id_in: $id, type: $type) {
|
||||
${queryObjects}
|
||||
}
|
||||
}
|
||||
}`
|
||||
break
|
||||
} case 'Viewer': {
|
||||
query = `
|
||||
query {
|
||||
Viewer {
|
||||
avatar {
|
||||
medium
|
||||
},
|
||||
name,
|
||||
id
|
||||
}
|
||||
}`
|
||||
break
|
||||
} case 'UserLists': {
|
||||
variables.id = opts.id
|
||||
query = `
|
||||
query ($page: Int, $perPage: Int, $id: Int, $type: MediaType, $status_in: [MediaListStatus]){
|
||||
Page (page: $page, perPage: $perPage) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
},
|
||||
mediaList (userId: $id, type: $type, status_in: $status_in) {
|
||||
media {
|
||||
${queryObjects}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
break
|
||||
} case 'SearchIDStatus': {
|
||||
variables.id = alID
|
||||
variables.mediaId = opts.id
|
||||
query = `
|
||||
query ($id: Int, $mediaId: Int){
|
||||
MediaList(userId: $id, mediaId: $mediaId) {
|
||||
status,
|
||||
progress,
|
||||
repeat
|
||||
}
|
||||
}`
|
||||
break
|
||||
} case 'AiringSchedule': {
|
||||
const date = new Date()
|
||||
const diff = date.getDay() >= 1 ? date.getDay() - 1 : 6 - date.getDay()
|
||||
date.setDate(date.getDate() - diff)
|
||||
date.setHours(0, 0, 0, 0)
|
||||
variables.from = date.getTime() / 1000
|
||||
variables.to = (date.getTime() + 7 * 24 * 60 * 60 * 1000) / 1000
|
||||
query = `
|
||||
query ($page: Int, $perPage: Int, $from: Int, $to: Int) {
|
||||
Page (page: $page, perPage: $perPage) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
},
|
||||
airingSchedules(airingAt_greater: $from, airingAt_lesser: $to) {
|
||||
episode,
|
||||
timeUntilAiring,
|
||||
airingAt,
|
||||
media{
|
||||
${queryObjects}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
break
|
||||
} case 'Search': {
|
||||
variables.genre = opts.genre
|
||||
variables.search = opts.search
|
||||
variables.year = opts.year
|
||||
variables.season = opts.season
|
||||
variables.format = opts.format
|
||||
variables.status = opts.status
|
||||
variables.sort = opts.sort || 'SEARCH_MATCH'
|
||||
query = `
|
||||
query ($page: Int, $perPage: Int, $sort: [MediaSort], $type: MediaType, $search: String, $status: MediaStatus, $season: MediaSeason, $year: Int, $genre: String, $format: MediaFormat, $startDate: FuzzyDateInt) {
|
||||
Page (page: $page, perPage: $perPage) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
},
|
||||
media(type: $type, search: $search, sort: $sort, status: $status, season: $season, seasonYear: $year, genre: $genre, format: $format, startDate_greater: $startDate) {
|
||||
${queryObjects}
|
||||
}
|
||||
}
|
||||
}`
|
||||
}
|
||||
}
|
||||
options.body = JSON.stringify({
|
||||
query: query.replace(/\s/g, ''),
|
||||
variables: variables
|
||||
})
|
||||
|
||||
return await handleRequest(options)
|
||||
}
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
/* global halfmoon */
|
||||
import { client } from './torrent.js'
|
||||
import { DOMPARSER } from './util.js'
|
||||
import { alRequest } from './anilist.js'
|
||||
import { nyaaRss } from './rss.js'
|
||||
import anitomyscript from 'anitomyscript'
|
||||
|
||||
const torrentRx = /(^magnet:){1}|(^[A-F\d]{8,40}$){1}|(.*\.torrent$){1}/i
|
||||
const imageRx = /\.(jpeg|jpg|gif|png|webp)/i
|
||||
window.addEventListener('paste', async e => { // WAIT image lookup on paste, or add torrent on paste
|
||||
const item = e.clipboardData.items[0]
|
||||
if (item && item.type.indexOf('image') === 0) {
|
||||
e.preventDefault()
|
||||
const formData = new FormData()
|
||||
formData.append('image', item.getAsFile())
|
||||
traceAnime(formData, 'file')
|
||||
} else if (item && item.type === 'text/plain') {
|
||||
item.getAsString(text => {
|
||||
if (torrentRx.exec(text)) {
|
||||
e.preventDefault()
|
||||
client.playTorrent(text)
|
||||
} else if (imageRx.exec(text)) {
|
||||
e.preventDefault()
|
||||
traceAnime(text)
|
||||
}
|
||||
})
|
||||
} else if (item && item.type === 'text/html') {
|
||||
item.getAsString(text => {
|
||||
const img = DOMPARSER(text, 'text/html').querySelectorAll('img')[0]
|
||||
if (img) {
|
||||
e.preventDefault()
|
||||
traceAnime(img.src)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
function traceAnime (image, type) { // WAIT lookup logic
|
||||
if (type === 'file') {
|
||||
const reader = new FileReader()
|
||||
reader.onload = e => {
|
||||
halfmoon.initStickyAlert({
|
||||
content: `Looking up anime for image.<br><img class="w-200 rounded pt-5" src="${e.target.result}">`
|
||||
})
|
||||
}
|
||||
reader.readAsDataURL(image.get('image'))
|
||||
} else {
|
||||
halfmoon.initStickyAlert({
|
||||
content: `Looking up anime for image.<br><img class="w-200 rounded pt-5" src="${image}">`
|
||||
})
|
||||
}
|
||||
let options
|
||||
let url = `https://api.trace.moe/search?cutBorders&url=${image}`
|
||||
if (type === 'file') {
|
||||
options = {
|
||||
method: 'POST',
|
||||
body: image
|
||||
}
|
||||
url = 'https://api.trace.moe/search'
|
||||
}
|
||||
fetch(url, options).then(res => res.json()).then(async ({ result }) => {
|
||||
if (result && result[0].similarity >= 0.85) {
|
||||
// const res = await alRequest({ method: 'SearchIDSingle', id: result[0].anilist })
|
||||
// viewMedia(res.data.Media, result[0].episode)
|
||||
// TODO: view media
|
||||
} else {
|
||||
halfmoon.initStickyAlert({
|
||||
content: 'Couldn\'t find anime for specified image! Try to remove black bars, or use a more detailed image.',
|
||||
title: 'Search Failed',
|
||||
alertType: 'alert-danger',
|
||||
fillType: ''
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
export const episodeRx = /Episode (\d+) - (.*)/
|
||||
|
||||
export async function nyaaSearch (media, episode, isOffline) {
|
||||
if (parseInt(episode) < 10) {
|
||||
episode = `0${episode}`
|
||||
}
|
||||
|
||||
const table = document.querySelector('tbody.results')
|
||||
const results = await nyaaRss(media, episode, isOffline)
|
||||
|
||||
if (results.children.length === 0) {
|
||||
halfmoon.initStickyAlert({
|
||||
content: `Couldn't find torrent for ${media.title.userPreferred} Episode ${parseInt(episode)}! Try specifying a torrent manually.`,
|
||||
title: 'Search Failed',
|
||||
alertType: 'alert-danger',
|
||||
fillType: ''
|
||||
})
|
||||
} else {
|
||||
table.innerHTML = ''
|
||||
table.appendChild(results)
|
||||
halfmoon.toggleModal('tsearch')
|
||||
}
|
||||
}
|
||||
|
||||
// resolve anime name based on file name and store it
|
||||
|
||||
export async function resolveFileMedia (opts) {
|
||||
// opts.fileName opts.isRelease
|
||||
|
||||
async function resolveTitle (title) {
|
||||
if (!(title in relations)) {
|
||||
// resolve name and shit
|
||||
const method = { name: title, method: 'SearchName', perPage: 1, status: ['RELEASING', 'FINISHED'], sort: 'SEARCH_MATCH', startDate: 10000000 }
|
||||
let res = await alRequest(method)
|
||||
if (!res.data.Page.media[0]) {
|
||||
const index = method.name.search(/S\d/)
|
||||
method.name = ((index !== -1 && method.name.slice(0, index) + method.name.slice(index + 1, method.name.length)) || method.name).replace('(TV)', '').replace(/ (19[5-9]\d|20[0-6]\d)/, '').replace('-', '')
|
||||
method.status = ['RELEASING', 'FINISHED']
|
||||
res = await alRequest(method)
|
||||
}
|
||||
if (res.data.Page.media[0]) {
|
||||
relations[title] = res.data.Page.media[0].id
|
||||
} else {
|
||||
relations[title] = null
|
||||
}
|
||||
}
|
||||
}
|
||||
const parsePromises = opts.fileName.constructor === Array
|
||||
? opts.fileName.map(name => anitomyscript(name))
|
||||
: [anitomyscript(opts.fileName)]
|
||||
const parseObjs = await Promise.all(parsePromises)
|
||||
await Promise.all([...new Set(parseObjs.map(obj => obj.anime_title))].map(title => resolveTitle(title)))
|
||||
const assoc = {}
|
||||
for (let ids = [...new Set(parseObjs.map(obj => relations[obj.anime_title]))]; ids.length; ids = ids.slice(50)) {
|
||||
for await (const media of (await alRequest({ method: 'SearchIDS', id: ids.slice(0, 50), perPage: 50 })).data.Page.media) {
|
||||
assoc[media.id] = media
|
||||
}
|
||||
}
|
||||
const fileMedias = []
|
||||
for (const praseObj of parseObjs) {
|
||||
let episode
|
||||
let media = assoc[relations[praseObj.anime_title]]
|
||||
async function resolveSeason (opts) {
|
||||
// opts.media, opts.episode, opts.increment, opts.offset
|
||||
let epMin, epMax
|
||||
if (opts.episode.constructor === Array) { // support batch episode ranges
|
||||
epMin = Number(opts.episode[0])
|
||||
epMax = Number(opts.episode[opts.episode.length - 1])
|
||||
} else {
|
||||
epMin = epMax = Number(opts.episode)
|
||||
}
|
||||
let tempMedia, increment
|
||||
if (opts.media.relations.edges.some(edge => edge.relationType === 'PREQUEL' && (edge.node.format === 'TV' || 'TV_SHORT')) && !opts.increment) {
|
||||
// media has prequel and we dont want to move up in the tree
|
||||
tempMedia = opts.media.relations.edges.filter(edge => edge.relationType === 'PREQUEL' && (edge.node.format === 'TV' || 'TV_SHORT'))[0].node
|
||||
} else if (opts.media.relations.edges.some(edge => edge.relationType === 'SEQUEL' && (edge.node.format === 'TV' || 'TV_SHORT'))) {
|
||||
// media doesnt have prequel, or we want to move up in the tree
|
||||
tempMedia = opts.media.relations.edges.filter(edge => edge.relationType === 'SEQUEL' && (edge.node.format === 'TV' || 'TV_SHORT'))[0].node
|
||||
increment = true
|
||||
}
|
||||
if (tempMedia?.episodes && epMax - (opts.offset + media.episodes) > (media.nextAiringEpisode?.episode || media.episodes)) {
|
||||
// episode is still out of bounds
|
||||
const nextEdge = await alRequest({ method: 'SearchIDSingle', id: tempMedia.id })
|
||||
await resolveSeason({ media: nextEdge.data.Media, episode: opts.episode, offset: opts.offset + nextEdge.data.Media.episodes, increment: increment })
|
||||
} else if (tempMedia?.episodes && epMax - (opts.offset + media.episodes) <= (media.nextAiringEpisode?.episode || media.episodes) && epMin - (opts.offset + media.episodes) > 0) {
|
||||
// episode is in range, seems good! overwriting media to count up "seasons"
|
||||
if (opts.episode.constructor === Array) {
|
||||
episode = `${praseObj.episode_number[0] - (opts.offset + media.episodes)} ~ ${praseObj.episode_number[praseObj.episode_number.length - 1] - (opts.offset + media.episodes)}`
|
||||
} else {
|
||||
episode = opts.episode - (opts.offset + media.episodes)
|
||||
}
|
||||
if (opts.increment || increment) {
|
||||
const nextEdge = await alRequest({ method: 'SearchIDSingle', id: tempMedia.id })
|
||||
media = nextEdge.data.Media
|
||||
}
|
||||
} else {
|
||||
console.log('error in parsing!', opts.media, tempMedia)
|
||||
halfmoon.initStickyAlert({
|
||||
content: `Failed resolving anime episode!<br>${opts.media.title.userPreferred} - ${epMax}`,
|
||||
title: 'Parsing Error',
|
||||
alertType: 'alert-secondary',
|
||||
fillType: ''
|
||||
})
|
||||
// something failed, most likely couldnt find an edge or processing failed, force episode number even if its invalid/out of bounds, better than nothing
|
||||
if (opts.episode.constructor === Array) {
|
||||
episode = `${Number(praseObj.episode_number[0])} ~ ${Number(praseObj.episode_number[praseObj.episode_number.length - 1])}`
|
||||
} else {
|
||||
episode = Number(opts.episode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// resolve episode, if movie, dont.
|
||||
if ((media?.format !== 'MOVIE' || (media.episodes || media.nextAiringEpisode.episode)) && praseObj.episode_number) {
|
||||
if (praseObj.episode_number.constructor === Array) {
|
||||
// is an episode range
|
||||
if (parseInt(praseObj.episode_number[0]) === 1) {
|
||||
// if it starts with #1 and overflows then it includes more than 1 season in a batch, cant fix this cleanly, name is parsed per file basis so this shouldnt be an issue
|
||||
episode = `${praseObj.episode_number[0]} ~ ${praseObj.episode_number[praseObj.episode_number.length - 1]}`
|
||||
} else {
|
||||
if ((media?.episodes || media?.nextAiringEpisode?.episode) && parseInt(praseObj.episode_number[praseObj.episode_number.length - 1]) > (media.episodes || media.nextAiringEpisode.episode)) {
|
||||
// if highest value is bigger than episode count or latest streamed episode +1 for safety, parseint to math.floor a number like 12.5 - specials - in 1 go
|
||||
await resolveSeason({ media: media, episode: praseObj.episode_number, offset: 0 })
|
||||
} else {
|
||||
// cant find ep count or range seems fine
|
||||
episode = `${Number(praseObj.episode_number[0])} ~ ${Number(praseObj.episode_number[praseObj.episode_number.length - 1])}`
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ((media?.episodes || media?.nextAiringEpisode?.episode) && parseInt(praseObj.episode_number) > (media.episodes || media.nextAiringEpisode.episode)) {
|
||||
// value bigger than episode count
|
||||
await resolveSeason({ media: media, episode: praseObj.episode_number, offset: 0 })
|
||||
} else {
|
||||
// cant find ep count or episode seems fine
|
||||
episode = Number(praseObj.episode_number)
|
||||
}
|
||||
}
|
||||
}
|
||||
const streamingEpisode = media?.streamingEpisodes.filter(episode => episodeRx.exec(episode.title) && Number(episodeRx.exec(episode.title)[1]) === Number(praseObj.episode_number))[0]
|
||||
fileMedias.push({
|
||||
mediaTitle: media?.title.userPreferred,
|
||||
episodeNumber: episode,
|
||||
episodeTitle: streamingEpisode && episodeRx.exec(streamingEpisode.title)[2],
|
||||
episodeThumbnail: streamingEpisode?.thumbnail,
|
||||
mediaCover: media?.coverImage.medium,
|
||||
name: 'Miru',
|
||||
parseObject: praseObj,
|
||||
media: media
|
||||
})
|
||||
}
|
||||
return fileMedias.length === 1 ? fileMedias[0] : fileMedias
|
||||
}
|
||||
|
||||
export const relations = JSON.parse(localStorage.getItem('relations')) || {}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export const client = null
|
||||
|
|
@ -1,3 +1,20 @@
|
|||
/* global halfmoon */
|
||||
halfmoon.showModal = id => {
|
||||
const t = document.getElementById(id)
|
||||
t && t.classList.add('show')
|
||||
}
|
||||
|
||||
halfmoon.hideModal = id => {
|
||||
const t = document.getElementById(id)
|
||||
t && t.classList.remove('show')
|
||||
}
|
||||
|
||||
// export const searchParams = new URLSearchParams(location.href)
|
||||
// if (searchParams.get('access_token')) {
|
||||
// localStorage.setItem('ALtoken', searchParams.get('access_token'))
|
||||
// window.location = '/app/#home'
|
||||
// }
|
||||
|
||||
export function countdown (s) {
|
||||
const d = Math.floor(s / (3600 * 24))
|
||||
s -= d * 3600 * 24
|
||||
|
|
@ -11,3 +28,7 @@ export function countdown (s) {
|
|||
if (d || h || m) tmp.push(m + 'm')
|
||||
return tmp.join(' ')
|
||||
}
|
||||
|
||||
export const DOMPARSER = new DOMParser().parseFromString.bind(new DOMParser())
|
||||
|
||||
export const sleep = t => new Promise(resolve => setTimeout(resolve, t))
|
||||
|
|
|
|||
Loading…
Reference in a new issue