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:
ThaUnknown 2022-12-27 05:53:56 +01:00
parent b951910ea7
commit 9e5b239ea3
5 changed files with 673 additions and 912 deletions

View file

@ -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",

File diff suppressed because it is too large Load diff

View file

@ -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) })

View file

@ -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))
}
},

View file

@ -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);