UI work, new sidebar, update webtorrent-player

This commit is contained in:
ThaUnknown 2021-08-01 23:38:21 +02:00
parent edc6af7505
commit 27fb43502d
10 changed files with 614 additions and 311 deletions

View file

@ -51,7 +51,8 @@ input:invalid {
filter: invert(.942);
}
#home.browsing .home, #home:not(.browsing) .browse {
#home.browsing .home,
#home:not(.browsing) .browse {
display: none;
}
@ -250,15 +251,6 @@ section:target:not(#player) {
transition: top .4s cubic-bezier(.25, .8, .25, 1);
}
.nav-hidden>nav {
top: calc(-1* var(--navbar-height)) !important;
}
.nav-hidden>.content-wrapper {
top: 0 !important;
height: 100% !important;
}
.content-wrapper {
transition: top .4s cubic-bezier(.25, .8, .25, 1);
transition-property: top, height;
@ -302,4 +294,134 @@ nav {
#oauth::after {
white-space: pre !important
}
/* sidebar */
:root {
--sidebar-minimised: 7rem;
--sidebar-width: 22rem
}
.page-wrapper.with-sidebar[data-sidebar-hidden] {
--sidebar-width: var(--sidebar-minimised);
}
@media (max-width: 768px) {
.page-wrapper.with-sidebar[data-sidebar-type~="overlayed-sm-and-down"]>.content-wrapper {
left: var(--sidebar-minimised);
width: calc(100% - var(--sidebar-minimised));
}
}
.page-wrapper.with-sidebar[data-sidebar-hidden]>.content-wrapper {
left: var(--sidebar-width);
width: calc(100% - var(--sidebar-width));
}
.sidebar [data-toggle="tooltip"]::before,
.page-wrapper:not(.with-sidebar[data-sidebar-hidden]) .sidebar [data-toggle="tooltip"]::after {
display: none
}
.page-wrapper.with-sidebar[data-sidebar-hidden]>.sidebar {
left: 0;
}
.page-wrapper.with-sidebar .text {
opacity: 1;
transition: opacity .8s cubic-bezier(.25, .8, .25, 1);
margin-left: 1rem;
display: inline-flex;
justify-content: center;
align-items: center;
font-weight: 400;
}
.page-wrapper.with-sidebar[data-sidebar-hidden] .text {
opacity: 0;
}
.sidebar a>span {
transition: background 0.4s cubic-bezier(.25, .8, .25, 1), color 0.4s cubic-bezier(.25, .8, .25, 1);
color: #fff;
border-radius: 0.3rem;
width: 100%;
height: 100%;
overflow: hidden;
}
.sidebar a:not(.brand):hover>span {
background: #fff;
color: var(--dark-color)
}
.sidebar a {
width: 100%;
font-size: 1.4rem;
padding: 0.75rem 1.5rem;
height: 5.5rem;
}
.sidebar .material-icons {
font-size: 2.2rem;
min-width: 4rem;
width: 4rem;
height: 4rem;
display: inline-flex;
justify-content: center;
align-items: center;
}
.sidebar a:not(.brand) img {
font-size: 2.2rem;
width: 3rem;
height: 3rem;
margin: 0.5rem;
display: inline-flex;
justify-content: center;
align-items: center;
}
.sidebar .menu {
right: 1.5rem;
position: absolute;
}
.sidebar-link img {
height: 3rem;
margin-right: var(--sidebar-brand-image-margin-right);
}
.sidebar .brand {
height: 8rem;
}
.page-wrapper.with-transitions.with-sidebar>.sidebar {
transition: width .4s cubic-bezier(.25, .8, .25, 1), left .4s cubic-bezier(.25, .8, .25, 1);
overflow: visible;
}
.nav-hidden>.sidebar {
left: calc(-1* var(--sidebar-width)) !important;
}
.nav-hidden>.content-wrapper {
left: 0 !important;
width: 100% !important;
}
.sidebar [data-toggle="tooltip"]:not([data-target-breakpoint])::after,
.sidebar [data-toggle="tooltip"]:not([data-target-breakpoint])::after {
transition: opacity 0.3s cubic-bezier(.25, .8, .25, 1), top 0.3s cubic-bezier(.25, .8, .25, 1);
top: 0;
opacity: 0;
font-weight: bold;
}
.sidebar [data-toggle="tooltip"]:not([data-target-breakpoint]):hover::after,
.sidebar [data-toggle="tooltip"]:not([data-target-breakpoint]):focus::after {
opacity: 1;
top: 50%
}

View file

@ -6,7 +6,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Miru">
<meta name="description" content="Anime torrent streaming, ad free in a simple solution.">
<meta name="theme-color" content="#11141700" />
<meta name="theme-color" content="#191c20" />
<link rel="icon" href="logo.png">
<link rel="apple-touch-icon" href="logo.png">
<meta charset="utf-8" />
@ -27,7 +27,7 @@
</style>
</head>
<body class="dark-mode with-custom-webkit-scrollbars with-custom-css-scrollbars">
<body class="dark-mode with-custom-webkit-scrollbars with-custom-css-scrollbars" data-sidebar-shortcut-enabled="true">
<div class="modal" id="tsearch" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content w-auto">
@ -123,40 +123,63 @@
</div>
</div>
</div>
<div id="pageWrapper" class="page-wrapper with-navbar">
<div id="pageWrapper" class="page-wrapper with-sidebar" data-sidebar-type="full-height overlayed-sm-and-down" data-sidebar-hidden="hidden">
<div class="sticky-alerts"></div>
<nav class="navbar bg-dark">
<div class="container">
<a href="#player" class="navbar-brand mt-5 ml-10">
<img src="logo_cut.png" alt="logo">
<div class="sidebar-overlay" onclick="halfmoon.toggleSidebar()"></div>
<div class="sidebar">
<div class="sidebar-menu h-full d-flex flex-column m-0">
<a class="sidebar-link sidebar-link-with-icon pointer brand" data-toggle="tooltip" data-placement="right"
data-title="Open Menu" onclick="halfmoon.toggleSidebar()">
<span class="text-nowrap d-flex align-items-center justify-content-between">
<img src="logo_cut.png" alt="logo" class="text">
<span class="material-icons menu">menu</span>
</span>
</a>
<a href="#downloads" class="navbar-brand pointer ml-auto" data-toggle="tooltip" data-placement="bottom"
data-title="Your Downloads" id="navDownloads">
<span class="material-icons font-size-20">get_app</span>
</a>
<a href="#home" class="navbar-brand pointer" data-toggle="tooltip" data-placement="bottom"
data-title="Airing Schedule" id="navSchedule" data-function="schedule">
<span class="material-icons font-size-20">schedule</span>
</a>
<a href="#home" class="navbar-brand pointer" data-toggle="tooltip" data-placement="bottom"
<a href="#home" class="sidebar-link sidebar-link-with-icon" data-toggle="tooltip" data-placement="right"
data-title="Home Page" id="navHome">
<span class="material-icons font-size-20">home</span>
<span class="text-nowrap d-flex align-items-center">
<span class="material-icons">home</span>
<span class="text">Home Page</span>
</span>
</a>
<a class="navbar-brand pointer d-none" data-toggle="tooltip" data-placement="bottom"
data-title="Currently Playing Anime" id="navNowPlaying">
<span class="material-icons font-size-20">queue_music</span>
<a href="#home" class="sidebar-link sidebar-link-with-icon" data-toggle="tooltip" data-placement="right"
data-title="Airing Schedule" id="navSchedule" data-function="schedule" id="#home">
<span class="text-nowrap d-flex align-items-center">
<span class="material-icons">schedule</span>
<span class="text">Airing Schedule</span>
</span>
</a>
<a href="#settingsTab" class="navbar-brand pointer font-size-20" data-toggle="tooltip" data-placement="bottom"
<a href="#downloads" class="sidebar-link sidebar-link-with-icon" data-toggle="tooltip" data-placement="right"
data-title="Your Downloads" id="navDownloads">
<span class="text-nowrap d-flex align-items-centern">
<span class="material-icons">get_app</span>
<span class="text">Your Downloads</span>
</span>
</a>
<a class="sidebar-link sidebar-link-with-icon d-none pointer" data-toggle="tooltip" data-placement="right"
data-title="Now Playing" id="navNowPlaying">
<span class="text-nowrap d-flex align-items-center">
<span class="material-icons">queue_music</span>
<span class="text">Now Playing</span>
</span>
</a>
<a href="#settingsTab" class="sidebar-link sidebar-link-with-icon mt-auto" data-toggle="tooltip" data-placement="right"
data-title="Settings">
<span class="material-icons font-size-20">tune</span>
<span class="text-nowrap d-flex align-items-center">
<span class="material-icons">tune</span>
<span class="text">Settings</span>
</span>
</a>
<a id="oauth" class="navbar-brand pointer font-size-20 mr-5" data-toggle="tooltip" data-placement="bottom"
<a id="oauth" class="sidebar-link sidebar-link-with-icon pointer" data-toggle="tooltip" data-placement="right"
data-title="Login With AniList"
href="https://anilist.co/api/v2/oauth/authorize?client_id=4254&response_type=token">
<span class="material-icons font-size-20">login</span>
<span class="text-nowrap d-flex align-items-center">
<span class="material-icons">login</span>
<span class="text">Login</span>
</span>
</a>
</div>
</nav>
</div>
<div class="overflow-y-hidden content-wrapper">
<section id="player" class="torrent-player">
<video id="video" src=""></video>
@ -231,8 +254,8 @@
</div>
</div>
<div class="subtitles dropdown dropup with-arrow">
<span class="material-icons ctrl" title="Subtitles [C]" id="bcap" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false" disabled data-name="captionsButton">
<span class="material-icons ctrl" title="Subtitles [C]" id="bcap" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false" disabled data-name="captionsButton">
subtitles
</span>
<div class="dropdown-menu dropdown-menu-right ctrl custom-radio p-10 pb-5 text-capitalize"
@ -724,8 +747,12 @@
<input id="subtitle1" type="text" list="subtitle1list" class="form-control" autocomplete="off"
value="SubsPlease">
<datalist id="subtitle1list">
<option value="SubsPlease">Roboto Medium,26,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,1.3,0,2,20,20,23,1</option>
<option value="Erai-raws">Open Sans Semibold,45,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,10,10,25,1</option>
<option value="SubsPlease">Roboto
Medium,26,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,1.3,0,2,20,20,23,1
</option>
<option value="Erai-raws">Open Sans
Semibold,45,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,10,10,25,1
</option>
</datalist>
</div>
<div class="custom-switch mb-10" data-toggle="tooltip" data-placement="top"

View file

@ -7,7 +7,7 @@ import { nyaaRss } from './rss.js'
import halfmoon from 'halfmoon'
import anitomyscript from 'anitomyscript'
const torrentRx = /(magnet:){1}|(^[A-F\d]{8,40}$){1}|(.*\.torrent){1}/i
const imageRx = /\.(jpeg|jpg|gif|png|webp)/
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) {

View file

@ -315,7 +315,7 @@ __webpack_require__.r(__webpack_exports__);
const torrentRx = /(magnet:){1}|(^[A-F\d]{8,40}$){1}|(.*\.torrent){1}/i
const imageRx = /\.(jpeg|jpg|gif|png|webp)/
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) {
@ -899,6 +899,7 @@ function loadHomePage () {
}
}
function reloadHome () {
clearSearch()
home.classList.remove('browsing')
lastRSSDate = undefined
for (const item of homePreviewElements) {
@ -909,16 +910,12 @@ function loadHomePage () {
document.querySelector('.browse').textContent = ''
}
navHome.onclick = reloadHome
searchClear.onclick = reloadHome
function clearSearch () {
for (const element of homeSearchElements) {
element.value = ''
}
}
searchClear.onclick = () => {
clearSearch()
reloadHome()
home.classList.remove('browsing')
}
let searchTimeout
searchWrapper.oninput = e => {
if (!searchTimeout) {
@ -985,7 +982,12 @@ function initMenu () {
(0,_anilist_js__WEBPACK_IMPORTED_MODULE_0__.alRequest)({ method: 'Viewer' }).then(result => {
oauth.removeAttribute('href')
oauth.setAttribute('data-title', `${result.data.Viewer.name}\nClick To Logout`)
oauth.innerHTML = `<img src="${result.data.Viewer.avatar.medium}" class="m-0">`
oauth.innerHTML = `
<span class="text-nowrap d-flex align-items-center">
<img src="${result.data.Viewer.avatar.medium}">
<span class="text">${result.data.Viewer.name}</span>
<span class="material-icons menu text">logout</span>
</span>`
oauth.onclick = () => {
localStorage.removeItem('ALtoken')
location.reload()
@ -1065,102 +1067,101 @@ const client = new webtorrent_player__WEBPACK_IMPORTED_MODULE_0__.default({
streamedDownload: _settings_js__WEBPACK_IMPORTED_MODULE_4__.settings.torrent8,
generateThumbnails: _settings_js__WEBPACK_IMPORTED_MODULE_4__.settings.player5,
defaultSSAStyles: Object.values(subtitle1list.options).filter(item => item.value === _settings_js__WEBPACK_IMPORTED_MODULE_4__.settings.subtitle1)[0].textContent,
resolveFileMedia: _anime_js__WEBPACK_IMPORTED_MODULE_2__.resolveFileMedia,
onDownloadDone: File => {
halfmoon__WEBPACK_IMPORTED_MODULE_6___default().initStickyAlert({
content: `<span class="text-break">${File.name}</span> has finished downloading. Now seeding.`,
title: 'Download Complete',
alertType: 'alert-success',
fillType: ''
})
},
onWatched: (File, FileMedia) => {
if (FileMedia?.media?.episodes || FileMedia?.media?.nextAiringEpisode?.episode) {
if (_settings_js__WEBPACK_IMPORTED_MODULE_4__.settings.other2 && (FileMedia.media.episodes || FileMedia.media.nextAiringEpisode?.episode > FileMedia.episodeNumber)) {
(0,_anilist_js__WEBPACK_IMPORTED_MODULE_3__.alEntry)()
} else {
halfmoon__WEBPACK_IMPORTED_MODULE_6___default().initStickyAlert({
content: `Do You Want To Mark <br><b>${FileMedia.mediaTitle}</b><br>Episode ${FileMedia.episodeNumber} As Completed?<br>
resolveFileMedia: _anime_js__WEBPACK_IMPORTED_MODULE_2__.resolveFileMedia
})
client.on('download-done', ({ file }) => {
halfmoon__WEBPACK_IMPORTED_MODULE_6___default().initStickyAlert({
content: `<span class="text-break">${file.name}</span> has finished downloading. Now seeding.`,
title: 'Download Complete',
alertType: 'alert-success',
fillType: ''
})
})
client.on('watched', ({ filemedia }) => {
if (filemedia?.media?.episodes || filemedia?.media?.nextAiringEpisode?.episode) {
if (_settings_js__WEBPACK_IMPORTED_MODULE_4__.settings.other2 && (filemedia.media.episodes || filemedia.media.nextAiringEpisode?.episode > filemedia.episodeNumber)) {
(0,_anilist_js__WEBPACK_IMPORTED_MODULE_3__.alEntry)()
} else {
halfmoon__WEBPACK_IMPORTED_MODULE_6___default().initStickyAlert({
content: `Do You Want To Mark <br><b>${filemedia.mediaTitle}</b><br>Episode ${filemedia.episodeNumber} As Completed?<br>
<button class="btn btn-sm btn-square btn-success mt-5" onclick="alEntry()" data-dismiss="alert" type="button" aria-label="Close"></button>
<button class="btn btn-sm btn-square mt-5" data-dismiss="alert" type="button" aria-label="Close"><span aria-hidden="true">X</span></button>`,
title: 'Episode Complete',
timeShown: 180000
})
}
}
},
onPlaylist: () => {
window.location.hash = '#playlist'
},
onNext: (File, FileMedia) => {
if (FileMedia.media) {
(0,_anime_js__WEBPACK_IMPORTED_MODULE_2__.nyaaSearch)(FileMedia.media, FileMedia.episodeNumber + 1)
} else {
halfmoon__WEBPACK_IMPORTED_MODULE_6___default().initStickyAlert({
content: 'Couldn\'t find anime name! Try specifying a torrent manually.',
title: 'Search Failed',
alertType: 'alert-danger',
fillType: ''
title: 'Episode Complete',
timeShown: 180000
})
}
},
onPrev: (File, FileMedia) => {
if (FileMedia.media) {
(0,_anime_js__WEBPACK_IMPORTED_MODULE_2__.nyaaSearch)(FileMedia.media, FileMedia.episodeNumber - 1)
} else {
halfmoon__WEBPACK_IMPORTED_MODULE_6___default().initStickyAlert({
content: 'Couldn\'t find anime name! Try specifying a torrent manually.',
title: 'Search Failed',
alertType: 'alert-danger',
fillType: ''
})
}
},
onOfflineTorrent: torrent => {
(0,_anime_js__WEBPACK_IMPORTED_MODULE_2__.resolveFileMedia)({ fileName: torrent.name, method: 'SearchName' }).then(mediaInformation => {
mediaInformation.onclick = () => client.addTorrent(torrent, { media: mediaInformation, episode: mediaInformation.episode })
const template = (0,_interface_js__WEBPACK_IMPORTED_MODULE_5__.cardCreator)(mediaInformation)
document.querySelector('.downloads').appendChild(template)
})
},
onVideoFiles: async (videoFiles, torrent) => {
document.querySelector('.playlist').textContent = ''
const cards = []
for (const file of videoFiles) {
const mediaInformation = await (0,_anime_js__WEBPACK_IMPORTED_MODULE_2__.resolveFileMedia)({ fileName: file.name, method: 'SearchName' })
mediaInformation.onclick = () => {
client.buildVideo(torrent, {
media: mediaInformation,
episode: mediaInformation.parseObject.episode,
file: file
})
}
cards.push((0,_interface_js__WEBPACK_IMPORTED_MODULE_5__.cardCreator)(mediaInformation))
}
document.querySelector('.playlist').append(...cards)
},
onWarn: (warn, torrent) => {
switch (warn) {
case 'no file':
halfmoon__WEBPACK_IMPORTED_MODULE_6___default().initStickyAlert({
content: `Couldn't find video file for <span class="text-break">${torrent.infoHash}</span>!`,
title: 'Search Failed',
alertType: 'alert-danger',
fillType: ''
})
break
case 'no peers':
if (torrent.progress !== 1) {
halfmoon__WEBPACK_IMPORTED_MODULE_6___default().initStickyAlert({
content: `Couldn't find peers for <span class="text-break">${torrent.infoHash}</span>! Try a torrent with more seeders.`,
title: 'Search Failed',
alertType: 'alert-danger',
fillType: ''
})
}
}
}
})
client.on('playlist', () => {
window.location.hash = '#playlist'
})
client.on('next', ({ filemedia }) => {
if (filemedia.media) {
(0,_anime_js__WEBPACK_IMPORTED_MODULE_2__.nyaaSearch)(filemedia.media, filemedia.episodeNumber + 1)
} else {
halfmoon__WEBPACK_IMPORTED_MODULE_6___default().initStickyAlert({
content: 'Couldn\'t find anime name! Try specifying a torrent manually.',
title: 'Search Failed',
alertType: 'alert-danger',
fillType: ''
})
}
})
client.on('prev', ({ filemedia }) => {
if (filemedia.media) {
(0,_anime_js__WEBPACK_IMPORTED_MODULE_2__.nyaaSearch)(filemedia.media, filemedia.episodeNumber - 1)
} else {
halfmoon__WEBPACK_IMPORTED_MODULE_6___default().initStickyAlert({
content: 'Couldn\'t find anime name! Try specifying a torrent manually.',
title: 'Search Failed',
alertType: 'alert-danger',
fillType: ''
})
}
})
client.on('offline-torrent', torrent => {
;(0,_anime_js__WEBPACK_IMPORTED_MODULE_2__.resolveFileMedia)({ fileName: torrent.name, method: 'SearchName' }).then(mediaInformation => {
mediaInformation.onclick = () => client.addTorrent(torrent, { media: mediaInformation, episode: mediaInformation.episode })
const template = (0,_interface_js__WEBPACK_IMPORTED_MODULE_5__.cardCreator)(mediaInformation)
document.querySelector('.downloads').appendChild(template)
})
})
client.on('video-files', async ({ files, torrent }) => {
document.querySelector('.playlist').textContent = ''
const cards = []
for (const file of files) {
const mediaInformation = await (0,_anime_js__WEBPACK_IMPORTED_MODULE_2__.resolveFileMedia)({ fileName: file.name, method: 'SearchName' })
mediaInformation.onclick = () => {
client.buildVideo(torrent, {
media: mediaInformation,
episode: mediaInformation.parseObject.episode,
file: file
})
}
cards.push((0,_interface_js__WEBPACK_IMPORTED_MODULE_5__.cardCreator)(mediaInformation))
}
document.querySelector('.playlist').append(...cards)
})
client.on('no-files', torrent => {
halfmoon__WEBPACK_IMPORTED_MODULE_6___default().initStickyAlert({
content: `Couldn't find video file for <span class="text-break">${torrent.infoHash}</span>!`,
title: 'Search Failed',
alertType: 'alert-danger',
fillType: ''
})
})
client.on('no peers', torrent => {
if (torrent.progress !== 1) {
halfmoon__WEBPACK_IMPORTED_MODULE_6___default().initStickyAlert({
content: `Couldn't find peers for <span class="text-break">${torrent.infoHash}</span>! Try a torrent with more seeders.`,
title: 'Search Failed',
alertType: 'alert-danger',
fillType: ''
})
}
})
client.nowPlaying = { name: 'Miru' }
window.client = client
window.onbeforeunload = () => { return '' }
@ -46181,11 +46182,9 @@ module.exports = function forEach (obj, fn, ctx) {
/*!*****************************************************!*\
!*** ./node_modules/fs-access-chunk-store/index.js ***!
\*****************************************************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
/* provided dependency */ var Buffer = __webpack_require__(/*! ./node_modules/buffer/index.js */ "./node_modules/buffer/index.js")["Buffer"];
const queueMicrotask = __webpack_require__(/*! queue-microtask */ "./node_modules/queue-microtask/index.js")
/***/ ((module) => {
/* eslint-env browser */
class WebFsChunkStore {
constructor (chunkLength, opts = {}) {
this.chunkLength = Number(chunkLength)
@ -46195,19 +46194,78 @@ class WebFsChunkStore {
}
this.closed = false
this.length = Number(opts.length) || Infinity
if (this.length !== Infinity) {
this.lastChunkLength = this.length % this.chunkLength || this.chunkLength
this.lastChunkIndex = Math.ceil(this.length / this.chunkLength) - 1
}
this.name = opts.name || 'default'
this.rootDirPromise = opts.rootDir || navigator.storage.getDirectory()
this.storageDirPromise = this._getStorageDirectoryHandle()
this.chunksDirPromise = ((opts.files && opts.rootDir) && this._getChunksDirHandle()) || this.storageDirPromise
this.chunks = []
this.chunks = [] // individual chunks, required for reads :/
this.chunkMap = [] // full files
this.directoryMap = {}
if (opts.files && opts.rootDir) {
this.files = opts.files.slice(0).map((file, i, files) => {
if (file.path == null) throw new Error('File is missing `path` property')
if (file.length == null) throw new Error('File is missing `length` property')
if (file.offset == null) {
if (i === 0) {
file.offset = 0
} else {
const prevFile = files[i - 1]
file.offset = prevFile.offset + prevFile.length
}
}
// file handles
if (file.handle == null) {
file.handle = this._createFileHandle({ path: file.path })
}
// file chunkMap
const fileStart = file.offset
const fileEnd = file.offset + file.length
const firstChunk = Math.floor(fileStart / this.chunkLength)
const lastChunk = Math.floor((fileEnd - 1) / this.chunkLength)
for (let p = firstChunk; p <= lastChunk; ++p) {
const chunkStart = p * this.chunkLength
const chunkEnd = chunkStart + this.chunkLength
const from = (fileStart < chunkStart) ? 0 : fileStart - chunkStart
const to = (fileEnd > chunkEnd) ? this.chunkLength : fileEnd - chunkStart
const offset = (fileStart > chunkStart) ? 0 : chunkStart - fileStart
if (!this.chunkMap[p]) this.chunkMap[p] = []
this.chunkMap[p].push({ from, to, offset, file })
}
return file
})
// close streams is page is frozen/unloaded, they will re-open if the user returns via BFC
window.addEventListener('pagehide', this.cleanup)
this.length = this.files.reduce((sum, file) => sum + file.length, 0)
if (opts.length != null && opts.length !== this.length) {
throw new Error('total `files` length is not equal to explicit `length` option')
}
} else {
this.length = Number(opts.length) || Infinity
}
if (this.length !== Infinity) {
this.lastChunkLength = this.length % this.chunkLength || this.chunkLength
this.lastChunkIndex = Math.ceil(this.length / this.chunkLength) - 1
}
}
async _getChunksDirHandle () {
const storageDir = await this.storageDirPromise
return await storageDir.getDirectoryHandle('chunks', { create: true })
}
async _getStorageDirectoryHandle () {
@ -46220,16 +46278,31 @@ class WebFsChunkStore {
if (!chunk) {
const fileName = index.toString()
const storageDir = await this.storageDirPromise
const storageDir = await this.chunksDirPromise
chunk = this.chunks[index] = {
fileHandlePromise: storageDir.getFileHandle(fileName, { create: true })
}
chunk = this.chunks[index] = { fileHandlePromise: storageDir.getFileHandle(fileName, { create: true }) }
}
return chunk
}
async _createFileHandle (opts) {
const fileName = opts.path.slice(opts.path.lastIndexOf('/') + 1)
return (await this._getDirectoryHandle(opts)).getFileHandle(fileName, { create: true })
}
// recursive, equiv of cd and mkdirp
async _getDirectoryHandle (opts) {
const lastIndex = opts.path.lastIndexOf('/')
if (lastIndex === -1 || lastIndex === 0) return this.storageDirPromise
const path = opts.path = opts.path.slice(0, lastIndex)
if (!this.directoryMap[path]) {
const parent = this._getDirectoryHandle(opts)
this.directoryMap[path] = (await parent).getDirectoryHandle(path.slice(path.lastIndexOf('/') + 1), { create: true })
}
return this.directoryMap[path]
}
put (index, buf, cb = () => {}) {
if (this.closed) {
queueMicrotask(() => cb(new Error('Storage is closed')))
@ -46264,8 +46337,33 @@ class WebFsChunkStore {
return
}
cb(null)
if (!this.files) cb(null)
})()
if (this.files) {
const targets = this.chunkMap[index]
if (!targets) {
queueMicrotask(() => cb(new Error('No files matching the request range')))
}
const promises = targets.map(target => {
return (async () => {
try {
const { file } = target
if (!file.stream) {
file.stream = (await file.handle).createWritable({
keepExistingData: true
})
}
const stream = await file.stream
await stream.write({ type: 'write', position: target.offset, data: buf.slice(target.from, target.to) })
return null
} catch (err) {
return cb(err)
}
})()
})
Promise.all(promises).then(() => cb(null))
}
}
get (index, opts, cb = () => {}) {
@ -46282,33 +46380,82 @@ class WebFsChunkStore {
if (!opts) opts = {}
const offset = opts.offset || 0
const len = opts.length || chunkLength - offset
const rangeFrom = opts.offset || 0
const rangeTo = opts.length ? rangeFrom + opts.length : chunkLength
const len = opts.length || chunkLength - rangeFrom
;(async () => {
let buf
try {
const chunk = await this._getChunk(index)
const fileHandle = await chunk.fileHandlePromise
let file = await fileHandle.getFile()
if (offset !== 0 || len !== chunkLength) {
file = file.slice(offset, len + offset)
if (!this.files || this.chunks[index]) {
;(async () => {
let buf
try {
const chunk = await this._getChunk(index)
const fileHandle = await chunk.fileHandlePromise
let file = await fileHandle.getFile()
if (rangeFrom !== 0 || len !== chunkLength) {
file = file.slice(rangeFrom, len + rangeFrom)
}
buf = await file.arrayBuffer()
} catch (err) {
cb(err)
return
}
buf = await file.arrayBuffer()
} catch (err) {
cb(err)
return
}
if (buf.byteLength === 0) {
const err = new Error(`Index ${index} does not exist`)
err.notFound = true
cb(err)
return
}
if (buf.byteLength === 0) {
const err = new Error(`Index ${index} does not exist`)
err.notFound = true
cb(err)
return
}
cb(null, Buffer.from(buf))
})()
cb(null, new Uint8Array(buf))
})()
} else {
let targets = this.chunkMap[index]
if (!targets) {
queueMicrotask(() => cb(new Error('No files matching the request range')))
}
if (opts) {
targets = targets.filter(target => {
return target.to > rangeFrom && target.from < rangeTo
})
if (targets.length === 0) {
queueMicrotask(() => cb(new Error('No files matching the request range')))
}
}
if (rangeFrom === rangeTo) return queueMicrotask(() => cb(null, new Uint8Array(0)))
const promises = targets.map(target => {
return (async () => {
let from = target.from
let to = target.to
let offset = target.offset
if (opts) {
if (to > rangeTo) to = rangeTo
if (from < rangeFrom) {
offset += (rangeFrom - from)
from = rangeFrom
}
}
try {
const handle = await target.file.handle
const file = (await handle.getFile()).slice(offset, offset + to - from)
return await file.arrayBuffer()
} catch (err) {
return err
}
})()
})
Promise.all(promises).then(values => {
if (values.length === 1) {
cb(null, new Uint8Array(values[0]))
} else {
new Blob(values).arrayBuffer().then(buf => {
cb(null, new Uint8Array(buf))
})
}
})
}
}
close (cb = () => {}) {
@ -46318,13 +46465,30 @@ class WebFsChunkStore {
}
this.closed = true
this.chunks = []
this.chunkMap = []
this.directoryMap = {}
this.cleanup()
queueMicrotask(() => {
cb(null)
})
}
async cleanup () {
if (this.files) {
for (const file of this.files) {
if (file.stream) {
await (await file.stream).close()
file.stream = null
}
}
const storageDir = await this.storageDirPromise
await storageDir.removeEntry('chunks', { recursive: true })
}
this.chunks = []
}
destroy (cb = () => {}) {
if (this.closed) {
queueMicrotask(() => cb(new Error('Storage is closed')))
@ -78290,23 +78454,14 @@ Style: Default,${options.defaultSSAStyles || 'Roboto Medium,26,&H00FFFFFF,&H0000
}
this.completed = false
this.onWatched = options.onWatched
if (this.onWatched) this.video.addEventListener('timeupdate', () => this.checkCompletion())
this.video.addEventListener('timeupdate', () => this.checkCompletion())
this.onPlaylist = options.onPlaylist
this.onNext = options.onNext
this.onPrev = options.onPrev
this.nextTimeout = undefined
if (this.onNext && options.autoNext) this.video.addEventListener('ended', () => this.playNext())
this.onError = options.onError
this.onWarn = options.onWarn
if (options.autoNext) this.video.addEventListener('ended', () => this.playNext())
this.resolveFileMedia = options.resolveFileMedia
this.currentFile = undefined
this.videoFile = undefined
this.onVideoFiles = options.onVideoFiles
if (this.controls.thumbnail) {
this.generateThumbnails = options.generateThumbnails
@ -78329,10 +78484,9 @@ Style: Default,${options.defaultSSAStyles || 'Roboto Medium,26,&H00FFFFFF,&H0000
})
}
this.onDownloadDone = options.onDownloadDone
this.onDone = undefined
this.onOfflineTorrent = options.onOfflineTorrent
this.destroyStore = options.destroyStore || true
this.immerseTimeout = undefined
this.immerseTime = options.immerseTime || 5
@ -78693,7 +78847,7 @@ Style: Default,${options.defaultSSAStyles || 'Roboto Medium,26,&H00FFFFFF,&H0000
}
openPlaylist () {
if (this.onPlaylist) this.onPlaylist()
this.emit('playlist', { files: this.videoFiles })
}
playNext () {
@ -78705,7 +78859,7 @@ Style: Default,${options.defaultSSAStyles || 'Roboto Medium,26,&H00FFFFFF,&H0000
const torrent = this.currentFile._torrent
this.buildVideo(torrent, { media: nowPlaying, file: this.videoFiles[this.videoFiles.indexOf(this.currentFile) + 1] })
} else {
if (this.onNext) this.onNext(this.currentFile, this.nowPlaying)
this.emit('next', { file: this.currentFile, filemedia: this.nowPlaying })
}
}, 200)
}
@ -78719,7 +78873,7 @@ Style: Default,${options.defaultSSAStyles || 'Roboto Medium,26,&H00FFFFFF,&H0000
const torrent = this.currentFile._torrent
this.buildVideo(torrent, { media: nowPlaying, file: this.videoFiles[this.videoFiles.indexOf(this.currentFile) - 1] })
} else {
if (this.onPrev) this.onPrev(this.currentFile, this.nowPlaying)
this.emit('prev', { file: this.currentFile, filemedia: this.nowPlaying })
}
}, 200)
}
@ -78848,7 +79002,7 @@ Style: Default,${options.defaultSSAStyles || 'Roboto Medium,26,&H00FFFFFF,&H0000
checkCompletion () {
if (!this.completed && this.video.duration - 180 < this.video.currentTime) {
this.completed = true
this.onWatched(this.currentFile, this.nowPlaying)
this.emit('watched', { file: this.currentFile, filemedia: this.nowPlaying })
}
}
@ -79197,7 +79351,7 @@ Style: Default,${options.defaultSSAStyles || 'Roboto Medium,26,&H00FFFFFF,&H0000
}
postDownload () {
this.onDownloadDone(this.currentFile)
this.emit('download-done', { file: this.currentFile })
this.parseSubtitles(this.currentFile).then(() => {
if (this.generateThumbnails) {
this.finishThumbnails(this.video.src)
@ -79208,21 +79362,21 @@ Style: Default,${options.defaultSSAStyles || 'Roboto Medium,26,&H00FFFFFF,&H0000
playTorrent (torrentID, opts = {}) { // TODO: clean this up
const handleTorrent = (torrent, opts) => {
torrent.on('noPeers', () => {
if (this.onWarn) this.onWarn('no peers', torrent)
this.emit('no-peers', torrent)
})
if (this.streamedDownload) {
torrent.files.forEach(file => file.deselect())
torrent.deselect(0, torrent.pieces.length - 1, false)
}
this.videoFiles = torrent.files.filter(file => this.videoExtensions.some(ext => file.name.endsWith(ext)))
if (this.onVideoFiles) this.onVideoFiles(this.videoFiles, torrent)
this.emit('video-files', { files: this.videoFiles, torrent: torrent })
if (this.videoFiles.length > 1) {
torrent.files.forEach(file => file.deselect())
}
if (this.videoFiles) {
this.buildVideo(torrent, opts)
} else {
if (this.onWarn) this.onWarn('no file', torrent)
this.emit('no-file', torrent)
this.cleanupTorrents()
}
}
@ -79251,7 +79405,7 @@ Style: Default,${options.defaultSSAStyles || 'Roboto Medium,26,&H00FFFFFF,&H0000
// cleanup torrent and store
cleanupTorrents () {
// creates an array of all non-offline store torrents and removes them
this.torrents.filter(torrent => !this.offlineTorrents[torrent.infoHash]).forEach(torrent => torrent.destroy({ destroyStore: true }))
this.torrents.filter(torrent => !this.offlineTorrents[torrent.infoHash]).forEach(torrent => torrent.destroy({ destroyStore: this.destroyStore }))
}
// add torrent for offline download
@ -79270,7 +79424,7 @@ Style: Default,${options.defaultSSAStyles || 'Roboto Medium,26,&H00FFFFFF,&H0000
this.offlineTorrents[torrent.infoHash] = Array.from(torrent.torrentFile)
localStorage.setItem('offlineTorrents', JSON.stringify(this.offlineTorrents))
}
if (this.onOfflineTorrent) this.onOfflineTorrent(torrent)
this.emit('offline-torrent', torrent)
})
}
}

File diff suppressed because one or more lines are too long

View file

@ -194,6 +194,7 @@ export function loadHomePage () {
}
}
function reloadHome () {
clearSearch()
home.classList.remove('browsing')
lastRSSDate = undefined
for (const item of homePreviewElements) {
@ -204,16 +205,12 @@ export function loadHomePage () {
document.querySelector('.browse').textContent = ''
}
navHome.onclick = reloadHome
searchClear.onclick = reloadHome
function clearSearch () {
for (const element of homeSearchElements) {
element.value = ''
}
}
searchClear.onclick = () => {
clearSearch()
reloadHome()
home.classList.remove('browsing')
}
let searchTimeout
searchWrapper.oninput = e => {
if (!searchTimeout) {
@ -280,7 +277,12 @@ export function initMenu () {
alRequest({ method: 'Viewer' }).then(result => {
oauth.removeAttribute('href')
oauth.setAttribute('data-title', `${result.data.Viewer.name}\nClick To Logout`)
oauth.innerHTML = `<img src="${result.data.Viewer.avatar.medium}" class="m-0">`
oauth.innerHTML = `
<span class="text-nowrap d-flex align-items-center">
<img src="${result.data.Viewer.avatar.medium}">
<span class="text">${result.data.Viewer.name}</span>
<span class="material-icons menu text">logout</span>
</span>`
oauth.onclick = () => {
localStorage.removeItem('ALtoken')
location.reload()

View file

@ -42,102 +42,101 @@ export const client = new WebTorrentPlayer({
streamedDownload: settings.torrent8,
generateThumbnails: settings.player5,
defaultSSAStyles: Object.values(subtitle1list.options).filter(item => item.value === settings.subtitle1)[0].textContent,
resolveFileMedia: resolveFileMedia,
onDownloadDone: File => {
halfmoon.initStickyAlert({
content: `<span class="text-break">${File.name}</span> has finished downloading. Now seeding.`,
title: 'Download Complete',
alertType: 'alert-success',
fillType: ''
})
},
onWatched: (File, FileMedia) => {
if (FileMedia?.media?.episodes || FileMedia?.media?.nextAiringEpisode?.episode) {
if (settings.other2 && (FileMedia.media.episodes || FileMedia.media.nextAiringEpisode?.episode > FileMedia.episodeNumber)) {
alEntry()
} else {
halfmoon.initStickyAlert({
content: `Do You Want To Mark <br><b>${FileMedia.mediaTitle}</b><br>Episode ${FileMedia.episodeNumber} As Completed?<br>
resolveFileMedia: resolveFileMedia
})
client.on('download-done', ({ file }) => {
halfmoon.initStickyAlert({
content: `<span class="text-break">${file.name}</span> has finished downloading. Now seeding.`,
title: 'Download Complete',
alertType: 'alert-success',
fillType: ''
})
})
client.on('watched', ({ filemedia }) => {
if (filemedia?.media?.episodes || filemedia?.media?.nextAiringEpisode?.episode) {
if (settings.other2 && (filemedia.media.episodes || filemedia.media.nextAiringEpisode?.episode > filemedia.episodeNumber)) {
alEntry()
} else {
halfmoon.initStickyAlert({
content: `Do You Want To Mark <br><b>${filemedia.mediaTitle}</b><br>Episode ${filemedia.episodeNumber} As Completed?<br>
<button class="btn btn-sm btn-square btn-success mt-5" onclick="alEntry()" data-dismiss="alert" type="button" aria-label="Close"></button>
<button class="btn btn-sm btn-square mt-5" data-dismiss="alert" type="button" aria-label="Close"><span aria-hidden="true">X</span></button>`,
title: 'Episode Complete',
timeShown: 180000
})
}
}
},
onPlaylist: () => {
window.location.hash = '#playlist'
},
onNext: (File, FileMedia) => {
if (FileMedia.media) {
nyaaSearch(FileMedia.media, FileMedia.episodeNumber + 1)
} else {
halfmoon.initStickyAlert({
content: 'Couldn\'t find anime name! Try specifying a torrent manually.',
title: 'Search Failed',
alertType: 'alert-danger',
fillType: ''
title: 'Episode Complete',
timeShown: 180000
})
}
},
onPrev: (File, FileMedia) => {
if (FileMedia.media) {
nyaaSearch(FileMedia.media, FileMedia.episodeNumber - 1)
} else {
halfmoon.initStickyAlert({
content: 'Couldn\'t find anime name! Try specifying a torrent manually.',
title: 'Search Failed',
alertType: 'alert-danger',
fillType: ''
})
}
},
onOfflineTorrent: torrent => {
resolveFileMedia({ fileName: torrent.name, method: 'SearchName' }).then(mediaInformation => {
mediaInformation.onclick = () => client.addTorrent(torrent, { media: mediaInformation, episode: mediaInformation.episode })
const template = cardCreator(mediaInformation)
document.querySelector('.downloads').appendChild(template)
})
},
onVideoFiles: async (videoFiles, torrent) => {
document.querySelector('.playlist').textContent = ''
const cards = []
for (const file of videoFiles) {
const mediaInformation = await resolveFileMedia({ fileName: file.name, method: 'SearchName' })
mediaInformation.onclick = () => {
client.buildVideo(torrent, {
media: mediaInformation,
episode: mediaInformation.parseObject.episode,
file: file
})
}
cards.push(cardCreator(mediaInformation))
}
document.querySelector('.playlist').append(...cards)
},
onWarn: (warn, torrent) => {
switch (warn) {
case 'no file':
halfmoon.initStickyAlert({
content: `Couldn't find video file for <span class="text-break">${torrent.infoHash}</span>!`,
title: 'Search Failed',
alertType: 'alert-danger',
fillType: ''
})
break
case 'no peers':
if (torrent.progress !== 1) {
halfmoon.initStickyAlert({
content: `Couldn't find peers for <span class="text-break">${torrent.infoHash}</span>! Try a torrent with more seeders.`,
title: 'Search Failed',
alertType: 'alert-danger',
fillType: ''
})
}
}
}
})
client.on('playlist', () => {
window.location.hash = '#playlist'
})
client.on('next', ({ filemedia }) => {
if (filemedia.media) {
nyaaSearch(filemedia.media, filemedia.episodeNumber + 1)
} else {
halfmoon.initStickyAlert({
content: 'Couldn\'t find anime name! Try specifying a torrent manually.',
title: 'Search Failed',
alertType: 'alert-danger',
fillType: ''
})
}
})
client.on('prev', ({ filemedia }) => {
if (filemedia.media) {
nyaaSearch(filemedia.media, filemedia.episodeNumber - 1)
} else {
halfmoon.initStickyAlert({
content: 'Couldn\'t find anime name! Try specifying a torrent manually.',
title: 'Search Failed',
alertType: 'alert-danger',
fillType: ''
})
}
})
client.on('offline-torrent', torrent => {
resolveFileMedia({ fileName: torrent.name, method: 'SearchName' }).then(mediaInformation => {
mediaInformation.onclick = () => client.addTorrent(torrent, { media: mediaInformation, episode: mediaInformation.episode })
const template = cardCreator(mediaInformation)
document.querySelector('.downloads').appendChild(template)
})
})
client.on('video-files', async ({ files, torrent }) => {
document.querySelector('.playlist').textContent = ''
const cards = []
for (const file of files) {
const mediaInformation = await resolveFileMedia({ fileName: file.name, method: 'SearchName' })
mediaInformation.onclick = () => {
client.buildVideo(torrent, {
media: mediaInformation,
episode: mediaInformation.parseObject.episode,
file: file
})
}
cards.push(cardCreator(mediaInformation))
}
document.querySelector('.playlist').append(...cards)
})
client.on('no-files', torrent => {
halfmoon.initStickyAlert({
content: `Couldn't find video file for <span class="text-break">${torrent.infoHash}</span>!`,
title: 'Search Failed',
alertType: 'alert-danger',
fillType: ''
})
})
client.on('no peers', torrent => {
if (torrent.progress !== 1) {
halfmoon.initStickyAlert({
content: `Couldn't find peers for <span class="text-break">${torrent.infoHash}</span>! Try a torrent with more seeders.`,
title: 'Search Failed',
alertType: 'alert-danger',
fillType: ''
})
}
})
client.nowPlaying = { name: 'Miru' }
window.client = client
window.onbeforeunload = () => { return '' }

View file

@ -8,8 +8,8 @@
],
"lang": "en-US",
"orientation": "landscape",
"background_color": "#111417",
"theme_color": "#111417",
"background_color": "#191c20",
"theme_color": "#191c20",
"scope": "/app/",
"description": "Anime torrent streaming, ad free in a simple solution.",
"icons": [
@ -32,10 +32,10 @@
"scope_url_host": "localhost",
"scope_url_path": "/app/"
},
"capture_links": "existing_client_event",
"capture_links": "new-client",
"url_handlers": [
{
"origin": "localhost"
"origin": "https://localhost:5500"
}
],
"protocol_handlers": [

View file

@ -27,6 +27,12 @@
"dependencies": {
"webtorrent-player": "github:ThaUnknown/webtorrent-player"
},
"standard": {
"ignore": [
"bundle.js",
"bundle.map.js"
]
},
"devDependencies": {
"node-polyfill-webpack-plugin": "^1.1.4",
"standard": "^16.0.3",

13
sw.js
View file

@ -5,19 +5,12 @@ const staticCacheName = 'v1.0.0'
const filesToCache = [
'/app/index.html',
'/app/js/settingsHandler.js',
'/app/js/animeHandler.js',
'/app/js/playerHandler.js',
'/app/js/torrentHandler.js',
'/app/js/rangeParser.js',
'/app/js/util.js',
'/app/js/bundle.js',
'/app/css/misc.css',
'/app/css/player.css',
'/app/css/torrent-player.css',
'/app/logo.png',
'https://cdn.jsdelivr.net/npm/matroska-subtitles@3.0.1/dist/matroska-subtitles.min.js',
'https://cdn.jsdelivr.net/npm/halfmoon@1.1.0/css/halfmoon-variables.min.css',
'https://cdn.jsdelivr.net/gh/halfmoonui/halfmoon@1.1.0/js/halfmoon.min.js',
'https://cdn.jsdelivr.net/npm/webtorrent@latest/webtorrent.min.js',
'https://fonts.googleapis.com/icon?family=Material+Icons'
]
@ -63,7 +56,7 @@ self.addEventListener('activate', event => {
// }
// return response
// })
// }).catch(error => {
// }).catch(_error => {
// return caches.match('index.html')
// })
// )