mirror of
https://github.com/NoCrypt/migu.git
synced 2026-03-29 13:59:09 +00:00
ux: better main menu loading order [lazy fix]
fix: don't re-create torrents on reload feat: skip op/ed/recap button, chapter nubs
This commit is contained in:
parent
b951910ea7
commit
9e5b239ea3
5 changed files with 673 additions and 912 deletions
24
package.json
24
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "Miru",
|
||||
"version": "3.3.9",
|
||||
"version": "3.4.0",
|
||||
"author": "ThaUnknown_ <ThaUnknown@users.noreply.github.com>",
|
||||
"description": "Stream anime torrents, real-time with no waiting for downloads.",
|
||||
"main": "src/index.js",
|
||||
|
|
@ -13,14 +13,14 @@
|
|||
"publish": "vite build && electron-builder -p always"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^1.0.1",
|
||||
"concurrently": "^7.0.0",
|
||||
"electron": "20.1.1",
|
||||
"electron-builder": "^23.3.3",
|
||||
"electron-notarize": "^1.1.1",
|
||||
"svelte": "^3.47.0",
|
||||
"vite": "3.2.4",
|
||||
"vite-plugin-commonjs": "^0.5.2"
|
||||
"@sveltejs/vite-plugin-svelte": "^1.4.0",
|
||||
"concurrently": "^7.6.0",
|
||||
"electron": "22.0.0",
|
||||
"electron-builder": "^23.6.0",
|
||||
"electron-notarize": "^1.2.2",
|
||||
"svelte": "^3.55.0",
|
||||
"vite": "4.0.3",
|
||||
"vite-plugin-commonjs": "^0.5.3"
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
|
|
@ -107,15 +107,15 @@
|
|||
"bottleneck": "^2.19.5",
|
||||
"browser-event-target-emitter": "^1.0.0",
|
||||
"discord-rpc": "4.0.1",
|
||||
"electron-log": "^4.4.6",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-updater": "^4.6.5",
|
||||
"jassub": "1.2.1",
|
||||
"js-levenshtein": "^1.1.6",
|
||||
"matroska-subtitles": "github:ThaUnknown/matroska-subtitles",
|
||||
"matroska-subtitles": "github:ThaUnknown/matroska-subtitles#redist",
|
||||
"mime": "^3.0.0",
|
||||
"p2pcf": "github:ThaUnknown/p2pcf#no-remove",
|
||||
"pump": "^3.0.0",
|
||||
"quartermoon": "^1.2.1",
|
||||
"quartermoon": "^1.2.3",
|
||||
"range-parser": "^1.2.1",
|
||||
"svelte-keybinds": "1.0.5",
|
||||
"svelte-miniplayer": "1.0.3",
|
||||
|
|
|
|||
1384
pnpm-lock.yaml
1384
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -30,22 +30,7 @@ class TorrentClient extends WebTorrent {
|
|||
setInterval(() => {
|
||||
if (this.torrents[0]?.pieces) this.dispatch('pieces', [...this.torrents[0]?.pieces.map(piece => piece === null ? 77 : 33)])
|
||||
}, 2000)
|
||||
this.on('torrent', torrent => {
|
||||
const files = torrent.files.map(file => {
|
||||
return {
|
||||
infoHash: torrent.infoHash,
|
||||
name: file.name,
|
||||
type: file._getMimeType(),
|
||||
size: file.size,
|
||||
path: file.path,
|
||||
url: encodeURI(`http://localhost:${this.server.address().port}/webtorrent/${torrent.infoHash}/${file.path}`)
|
||||
}
|
||||
})
|
||||
this.dispatch('files', files)
|
||||
this.dispatch('pieces', torrent.pieces.length)
|
||||
this.dispatch('magnet', { magnet: torrent.magnetURI, hash: torrent.infoHash })
|
||||
this.dispatch('torrent', Array.from(torrent.torrentFile))
|
||||
})
|
||||
this.on('torrent', this.handleTorrent.bind(this))
|
||||
|
||||
this.server = http.createServer((request, response) => {
|
||||
if (!request.url) return null
|
||||
|
|
@ -100,6 +85,23 @@ class TorrentClient extends WebTorrent {
|
|||
this.server.listen(0)
|
||||
}
|
||||
|
||||
handleTorrent (torrent) {
|
||||
const files = torrent.files.map(file => {
|
||||
return {
|
||||
infoHash: torrent.infoHash,
|
||||
name: file.name,
|
||||
type: file._getMimeType(),
|
||||
size: file.size,
|
||||
path: file.path,
|
||||
url: encodeURI(`http://localhost:${this.server.address().port}/webtorrent/${torrent.infoHash}/${file.path}`)
|
||||
}
|
||||
})
|
||||
this.dispatch('files', files)
|
||||
this.dispatch('pieces', torrent.pieces.length)
|
||||
this.dispatch('magnet', { magnet: torrent.magnetURI, hash: torrent.infoHash })
|
||||
this.dispatch('torrent', Array.from(torrent.torrentFile))
|
||||
}
|
||||
|
||||
handleMessage ({ data }) {
|
||||
switch (data.type) {
|
||||
case 'current': {
|
||||
|
|
@ -121,9 +123,11 @@ class TorrentClient extends WebTorrent {
|
|||
break
|
||||
}
|
||||
case 'torrent': {
|
||||
const id = typeof data.data !== 'string' ? Buffer.from(data.data) : data.data
|
||||
const existing = this.get(id)
|
||||
if (existing) return this.handleTorrent(existing)
|
||||
if (this.torrents.length) this.remove(this.torrents[0].infoHash)
|
||||
|
||||
const id = typeof data.data !== 'string' ? Buffer.from(data.data) : data.data
|
||||
this.add(id, {
|
||||
private: this.settings.torrentPeX,
|
||||
path: this.settings.torrentPath,
|
||||
|
|
@ -200,6 +204,9 @@ class TorrentClient extends WebTorrent {
|
|||
this.dispatch('subtitle', { subtitle, trackNumber })
|
||||
})
|
||||
if (!skipFile) {
|
||||
parser.once('chapters', chapters => {
|
||||
this.dispatch('chapters', chapters)
|
||||
})
|
||||
parser.on('file', file => {
|
||||
if (file.mimetype === 'application/x-truetype-font' || file.mimetype === 'application/font-woff' || file.mimetype === 'application/vnd.ms-opentype' || file.mimetype === 'font/sfnt' || file.mimetype.startsWith('font/') || file.filename.toLowerCase().endsWith('.ttf')) {
|
||||
this.dispatch('file', { mimetype: file.mimetype, data: Array.from(file.data) })
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
import Gallery from './Gallery.svelte'
|
||||
import { add } from '@/modules/torrent.js'
|
||||
import { alToken, set } from '../Settings.svelte'
|
||||
import { alRequest } from '@/modules/anilist.js'
|
||||
import { alRequest, alID } from '@/modules/anilist.js'
|
||||
import { resolveFileMedia } from '@/modules/anime.js'
|
||||
import { getRSSContent, getReleasesRSSurl } from '@/lib/RSSView.svelte'
|
||||
|
||||
|
|
@ -159,20 +159,22 @@
|
|||
},
|
||||
trending: {
|
||||
title: 'Trending Now',
|
||||
load: (page = 1, perPage = 50, initial = false) => {
|
||||
load: async (page = 1, perPage = 50, initial = false) => {
|
||||
if (initial) search.sort = 'TRENDING_DESC'
|
||||
await alID
|
||||
return alRequest({ method: 'Search', page, perPage, sort: 'TRENDING_DESC', ...sanitiseObject(search) }).then(res => processMedia(res))
|
||||
}
|
||||
},
|
||||
seasonal: {
|
||||
title: 'Popular This Season',
|
||||
load: (page = 1, perPage = 50, initial = false) => {
|
||||
load: async (page = 1, perPage = 50, initial = false) => {
|
||||
const date = new Date()
|
||||
if (initial) {
|
||||
search.season = getSeason(date)
|
||||
search.year = date.getFullYear()
|
||||
search.sort = 'POPULARITY_DESC'
|
||||
}
|
||||
await alID
|
||||
return alRequest({ method: 'Search', page, perPage, year: date.getFullYear(), season: getSeason(date), sort: 'POPULARITY_DESC', ...sanitiseObject(search) }).then(res =>
|
||||
processMedia(res)
|
||||
)
|
||||
|
|
@ -180,58 +182,64 @@
|
|||
},
|
||||
popular: {
|
||||
title: 'All Time Popular',
|
||||
load: (page = 1, perPage = 50, initial = false) => {
|
||||
load: async (page = 1, perPage = 50, initial = false) => {
|
||||
if (initial) search.sort = 'POPULARITY_DESC'
|
||||
await alID
|
||||
return alRequest({ method: 'Search', page, perPage, sort: 'POPULARITY_DESC', ...sanitiseObject(search) }).then(res => processMedia(res))
|
||||
}
|
||||
},
|
||||
romance: {
|
||||
title: 'Romance',
|
||||
load: (page = 1, perPage = 50, initial = false) => {
|
||||
load: async (page = 1, perPage = 50, initial = false) => {
|
||||
if (initial) {
|
||||
search.sort = 'TRENDING_DESC'
|
||||
search.genre = 'Romance'
|
||||
}
|
||||
await alID
|
||||
return alRequest({ method: 'Search', page, perPage, sort: 'TRENDING_DESC', genre: 'Romance', ...sanitiseObject(search) }).then(res => processMedia(res))
|
||||
}
|
||||
},
|
||||
action: {
|
||||
title: 'Action',
|
||||
load: (page = 1, perPage = 50, initial = false) => {
|
||||
load: async (page = 1, perPage = 50, initial = false) => {
|
||||
if (initial) {
|
||||
search.sort = 'TRENDING_DESC'
|
||||
search.genre = 'Action'
|
||||
}
|
||||
await alID
|
||||
return alRequest({ method: 'Search', page, perPage, sort: 'TRENDING_DESC', genre: 'Action', ...sanitiseObject(search) }).then(res => processMedia(res))
|
||||
}
|
||||
},
|
||||
adventure: {
|
||||
title: 'Adventure',
|
||||
load: (page = 1, perPage = 50, initial = false) => {
|
||||
load: async (page = 1, perPage = 50, initial = false) => {
|
||||
if (initial) {
|
||||
search.sort = 'TRENDING_DESC'
|
||||
search.genre = 'Adventure'
|
||||
}
|
||||
await alID
|
||||
return alRequest({ method: 'Search', page, perPage, sort: 'TRENDING_DESC', genre: 'Adventure', ...sanitiseObject(search) }).then(res => processMedia(res))
|
||||
}
|
||||
},
|
||||
fantasy: {
|
||||
title: 'Fantasy',
|
||||
load: (page = 1, perPage = 50, initial = false) => {
|
||||
load: async (page = 1, perPage = 50, initial = false) => {
|
||||
if (initial) {
|
||||
search.sort = 'TRENDING_DESC'
|
||||
search.genre = 'Fantasy'
|
||||
}
|
||||
await alID
|
||||
return alRequest({ method: 'Search', page, perPage, sort: 'TRENDING_DESC', genre: 'Fantasy', ...sanitiseObject(search) }).then(res => processMedia(res))
|
||||
}
|
||||
},
|
||||
comedy: {
|
||||
title: 'Comedy',
|
||||
load: (page = 1, perPage = 50, initial = false) => {
|
||||
load: async (page = 1, perPage = 50, initial = false) => {
|
||||
if (initial) {
|
||||
search.sort = 'TRENDING_DESC'
|
||||
search.genre = 'Comedy'
|
||||
}
|
||||
await alID
|
||||
return alRequest({ method: 'Search', page, perPage, sort: 'TRENDING_DESC', genre: 'Comedy', ...sanitiseObject(search) }).then(res => processMedia(res))
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<script>
|
||||
/* eslint svelte/valid-compile: ["error", { ignoreWarnings: true }] */
|
||||
import { set } from '../Settings.svelte'
|
||||
import { playAnime } from '../RSSView.svelte'
|
||||
import { client } from '@/modules/torrent.js'
|
||||
import { onMount, createEventDispatcher, tick } from 'svelte'
|
||||
import { alEntry } from '@/modules/anilist.js'
|
||||
// import Peer from '@/modules/Peer.js'
|
||||
import Subtitles from '@/modules/subtitles.js'
|
||||
import { toTS, videoRx, fastPrettyBytes, throttle } from '@/modules/util.js'
|
||||
import { addToast } from '../Toasts.svelte'
|
||||
|
|
@ -127,6 +127,7 @@
|
|||
})
|
||||
currentTime = 0
|
||||
targetTime = 0
|
||||
chapters = []
|
||||
completed = false
|
||||
current = file
|
||||
emit('current', current)
|
||||
|
|
@ -251,17 +252,29 @@
|
|||
function toggleFullscreen () {
|
||||
document.fullscreenElement ? document.exitFullscreen() : container.requestFullscreen()
|
||||
}
|
||||
function seek (time) {
|
||||
if (time === 85 && currentTime < 10) {
|
||||
currentTime = currentTime = 90
|
||||
} else if (time === 85 && safeduration - currentTime < 90) {
|
||||
currentTime = currentTime = safeduration
|
||||
function skip () {
|
||||
const current = findChapter(currentTime)
|
||||
if (current) {
|
||||
if (!isChapterSkippable(current)) return
|
||||
const endtime = current.end / 1000
|
||||
if ((safeduration - endtime | 0) === 0) return playNext()
|
||||
currentTime = endtime
|
||||
currentSkippable = null
|
||||
} else if (currentTime < 10) {
|
||||
currentTime = 90
|
||||
} else if (safeduration - currentTime < 90) {
|
||||
currentTime = safeduration
|
||||
} else {
|
||||
currentTime = currentTime += time
|
||||
currentTime = currentTime + 85
|
||||
}
|
||||
targetTime = currentTime
|
||||
video.currentTime = targetTime
|
||||
}
|
||||
function seek (time) {
|
||||
currentTime = currentTime + time
|
||||
targetTime = currentTime
|
||||
video.currentTime = targetTime
|
||||
}
|
||||
function forward () {
|
||||
seek(2)
|
||||
}
|
||||
|
|
@ -368,10 +381,6 @@
|
|||
id: 'screenshot_monitor',
|
||||
type: 'icon'
|
||||
},
|
||||
KeyR: {
|
||||
fn: () => seek(-90),
|
||||
id: '-90'
|
||||
},
|
||||
KeyI: {
|
||||
fn: () => toggleStats(),
|
||||
id: 'list',
|
||||
|
|
@ -413,7 +422,7 @@
|
|||
type: 'icon'
|
||||
},
|
||||
KeyS: {
|
||||
fn: () => seek(85),
|
||||
fn: () => skip(),
|
||||
id: '+90'
|
||||
},
|
||||
KeyD: {
|
||||
|
|
@ -617,12 +626,44 @@
|
|||
}
|
||||
function getBufferHealth (time) {
|
||||
for (let index = video.buffered.length; index--;) {
|
||||
if (time < video.buffered.end(index) && time > video.buffered.start(index)) {
|
||||
if (time < video.buffered.end(index) && time >= video.buffered.start(index)) {
|
||||
return parseInt(video.buffered.end(index) - time)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
let chapters = []
|
||||
client.on('chapters', ({ detail }) => {
|
||||
chapters = detail
|
||||
})
|
||||
let hoverChapter = null
|
||||
|
||||
let currentSkippable = null
|
||||
function checkSkippableChapters () {
|
||||
const current = findChapter(currentTime)
|
||||
if (current) {
|
||||
currentSkippable = isChapterSkippable(current)
|
||||
}
|
||||
}
|
||||
const skippableChaptersRx = [
|
||||
['Opening', /^op$|opening$|^ncop/mi],
|
||||
['Ending', /^ed$|ending$|^nced/mi],
|
||||
['Recap', /recap/mi]
|
||||
]
|
||||
function isChapterSkippable (chapter) {
|
||||
for (const [name, regex] of skippableChaptersRx) {
|
||||
if (regex.test(chapter.text)) {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
function findChapter (time) {
|
||||
if (!chapters.length) return null
|
||||
for (const chapter of chapters) {
|
||||
if (time < (chapter.end / 1000) && time >= (chapter.start / 1000)) return chapter
|
||||
}
|
||||
}
|
||||
const thumbCanvas = document.createElement('canvas')
|
||||
thumbCanvas.width = 200
|
||||
const thumbnailData = {
|
||||
|
|
@ -638,6 +679,7 @@
|
|||
function handleHover ({ offsetX, target }) {
|
||||
hoverOffset = offsetX / target.clientWidth
|
||||
hoverTime = safeduration * hoverOffset
|
||||
hoverChapter = findChapter(hoverTime)
|
||||
hover.style.setProperty('left', hoverOffset * 100 + '%')
|
||||
thumbnail = thumbnailData.thumbnails[Math.floor(hoverTime / thumbnailData.interval)] || ' '
|
||||
}
|
||||
|
|
@ -861,6 +903,7 @@
|
|||
on:seeked={updatew2g}
|
||||
on:timeupdate={() => createThumbnail()}
|
||||
on:timeupdate={checkCompletion}
|
||||
on:timeupdate={checkSkippableChapters}
|
||||
on:waiting={showBuffering}
|
||||
on:loadeddata={hideBuffering}
|
||||
on:canplay={hideBuffering}
|
||||
|
|
@ -897,7 +940,7 @@
|
|||
</div>
|
||||
<span class='material-icons ctrl' title='Keybinds [`]' on:click={() => (showKeybinds = true)}> help_outline </span>
|
||||
</div>
|
||||
<div class='middle d-flex align-items-center justify-content-center flex-grow-1'>
|
||||
<div class='middle d-flex align-items-center justify-content-center flex-grow-1 position-relative'>
|
||||
<div class='w-full h-full position-absolute' on:dblclick={toggleFullscreen} on:click|self={() => { if (page === 'player') playPause(); page = 'player' }} />
|
||||
<span class='material-icons ctrl' class:text-muted={!hasLast} class:disabled={!hasLast} data-name='playLast' on:click={playLast}> skip_previous </span>
|
||||
<span class='material-icons ctrl' data-name='rewind' on:click={rewind}> fast_rewind </span>
|
||||
|
|
@ -905,6 +948,11 @@
|
|||
<span class='material-icons ctrl' data-name='forward' on:click={forward}> fast_forward </span>
|
||||
<span class='material-icons ctrl' class:text-muted={!hasNext} class:disabled={!hasNext} data-name='playNext' on:click={playNext}> skip_next </span>
|
||||
<div class='position-absolute bufferingDisplay' />
|
||||
{#if currentSkippable}
|
||||
<button class='skip btn text-dark position-absolute bottom-0 right-0 mr-20 mb-5 font-weight-bold' on:click={skip}>
|
||||
Skip {currentSkippable}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class='bottom d-flex z-40 flex-column px-20'>
|
||||
<div class='w-full d-flex align-items-center h-20 mb--5'>
|
||||
|
|
@ -925,8 +973,17 @@
|
|||
on:touchstart={handleMouseDown}
|
||||
on:touchend={handleMouseUp}
|
||||
on:keydown|preventDefault />
|
||||
<datalist class='d-flex position-absolute w-full'>
|
||||
{#each chapters.slice(1) as chapter}
|
||||
{@const value = chapter.start / 1000 / safeduration}
|
||||
<option {value} style:left={value * 100 + '%'} class='position-absolute' />
|
||||
{/each}
|
||||
</datalist>
|
||||
<div class='hover position-absolute d-flex flex-column align-items-center' bind:this={hover}>
|
||||
<img alt='thumbnail' class='w-full mb-5 shadow-lg' src={thumbnail} />
|
||||
{#if hoverChapter}
|
||||
<div class='ts'>{hoverChapter.text}</div>
|
||||
{/if}
|
||||
<div class='ts'>{toTS(hoverTime)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1025,7 +1082,16 @@
|
|||
background: #fff0;
|
||||
overflow: hidden;
|
||||
transition: all ease 100ms;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
datalist option {
|
||||
background: #ff3c00;
|
||||
top: 5px;
|
||||
min-height: unset;
|
||||
height: 6px;
|
||||
width: 2px;
|
||||
padding: 0
|
||||
}
|
||||
.custom-range:hover {
|
||||
--thumb-height: 12px;
|
||||
|
|
@ -1049,9 +1115,6 @@
|
|||
height: var(--thumb-height);
|
||||
width: var(--thumb-width, var(--thumb-height));
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.custom-range::-webkit-slider-thumb {
|
||||
--thumb-radius: calc((var(--target-height) * 0.5) - 1px);
|
||||
--clip-top: calc((var(--target-height) - var(--track-height)) * 0.5);
|
||||
--clip-bottom: calc(var(--target-height) - var(--clip-top));
|
||||
|
|
@ -1119,7 +1182,7 @@
|
|||
cursor: pointer !important;
|
||||
}
|
||||
.miniplayer .top,
|
||||
.miniplayer .bottom {
|
||||
.miniplayer .bottom, .miniplayer .skip {
|
||||
display: none !important;
|
||||
}
|
||||
.miniplayer video {
|
||||
|
|
@ -1173,7 +1236,7 @@
|
|||
|
||||
.immersed .middle .ctrl,
|
||||
.immersed .top,
|
||||
.immersed .bottom {
|
||||
.immersed .bottom, .immersed .skip {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
|
@ -1265,6 +1328,13 @@
|
|||
.bottom .hover .ts {
|
||||
filter: drop-shadow(0 0 8px #000);
|
||||
}
|
||||
.skip {
|
||||
transition: 0.5s opacity ease;
|
||||
background: #ececec;
|
||||
}
|
||||
.skip:hover {
|
||||
background-color: var(--lm-button-bg-color-hover);
|
||||
}
|
||||
|
||||
.bottom {
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.6) 25%, rgba(0, 0, 0, 0.4) 50%, rgba(0, 0, 0, 0.1) 75%, transparent);
|
||||
|
|
|
|||
Loading…
Reference in a new issue