mirror of
https://github.com/NoCrypt/migu.git
synced 2026-03-11 17:45:32 +00:00
Merge branch 'master' of https://github.com/RockinChaos/miru
This commit is contained in:
commit
be803481a9
51 changed files with 1125 additions and 1418 deletions
|
|
@ -113,7 +113,7 @@ apt install linux-Miru-*.deb
|
|||
|
||||
## **Building and Development**
|
||||
|
||||
*dont*
|
||||
*good luck*
|
||||
|
||||
Dependencies:
|
||||
- Node 16 or above
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@
|
|||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -18,24 +18,27 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/assets": "github:thaunknown/capacitor-assets",
|
||||
"@capacitor/cli": "^6.1.1",
|
||||
"@capacitor/cli": "^6.1.2",
|
||||
"cordova-res": "^0.15.4",
|
||||
"nodejs-mobile-gyp": "^0.3.1",
|
||||
"nodejs-mobile-gyp": "^0.4.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-merge": "^5.10.0"
|
||||
"webpack-merge": "^6.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^6.1.1",
|
||||
"@capacitor/app": "^6.0.0",
|
||||
"@capacitor/browser": "^6.0.1",
|
||||
"@capacitor/core": "^6.1.1",
|
||||
"@capacitor/android": "^6.1.2",
|
||||
"@capacitor/app": "^6.0.1",
|
||||
"@capacitor/app-launcher": "^6.0.2",
|
||||
"@capacitor/browser": "^6.0.2",
|
||||
"@capacitor/core": "^6.1.2",
|
||||
"@capacitor/device": "^6.0.1",
|
||||
"@capacitor/ios": "^6.1.1",
|
||||
"@capacitor/local-notifications": "^6.0.0",
|
||||
"@capacitor/status-bar": "^6.0.0",
|
||||
"@capacitor/ios": "^6.1.2",
|
||||
"@capacitor/local-notifications": "^6.1.0",
|
||||
"@capacitor/status-bar": "^6.0.1",
|
||||
"capacitor-folder-picker": "^0.0.2",
|
||||
"capacitor-intent-uri": "^0.0.1",
|
||||
"capacitor-nodejs": "https://github.com/funniray/Capacitor-NodeJS/releases/download/nodejs-18/capacitor-nodejs-1.0.0-beta.6.tgz",
|
||||
"capacitor-plugin-safe-area": "^2.0.6",
|
||||
"capacitor-plugin-safe-area": "^3.0.3",
|
||||
"common": "workspace:*",
|
||||
"cordova-plugin-navigationbar": "^1.0.31",
|
||||
"cordova-plugin-pip": "^0.0.2",
|
||||
|
|
|
|||
|
|
@ -3,11 +3,18 @@ import { StatusBar, Style } from '@capacitor/status-bar'
|
|||
import { SafeArea } from 'capacitor-plugin-safe-area'
|
||||
import { App } from '@capacitor/app'
|
||||
import { Browser } from '@capacitor/browser'
|
||||
import { IntentUri } from 'capacitor-intent-uri'
|
||||
import { LocalNotifications } from '@capacitor/local-notifications'
|
||||
import { Device } from '@capacitor/device'
|
||||
import { FolderPicker } from 'capacitor-folder-picker'
|
||||
import { toast } from 'svelte-sonner'
|
||||
import IPC from './ipc.js'
|
||||
|
||||
IPC.on('open', url => Browser.open({ url }))
|
||||
IPC.on('intent', async url => {
|
||||
await IntentUri.openUri({ url })
|
||||
IPC.emit('intent-end')
|
||||
})
|
||||
|
||||
App.addListener('appUrlOpen', ({ url }) => handleProtocol(url))
|
||||
|
||||
|
|
@ -51,6 +58,30 @@ IPC.on('get-device-info', async () => {
|
|||
IPC.emit('device-info', JSON.stringify(deviceInfo))
|
||||
})
|
||||
|
||||
const STORAGE_TYPE_MAP = {
|
||||
primary: '/sdcard/',
|
||||
secondary: '/sdcard/'
|
||||
}
|
||||
|
||||
IPC.on('dialog', async () => {
|
||||
const result = await FolderPicker.chooseFolder()
|
||||
const normalizedPath = decodeURIComponent(result.path)
|
||||
|
||||
const [, uri, ...path] = normalizedPath.split(':')
|
||||
const [,, app, subpath, type, ...rest] = uri.split('/')
|
||||
|
||||
if (app !== 'com.android.externalstorage.documents') return toast.error('Unverified app', { description: 'Expected com.android.externalstorage.documents, got: ' + app })
|
||||
if (rest.length) return toast.error('Unsupported uri', { description: 'Unxpected access type, got: tree/' + rest.join('/') })
|
||||
if (subpath !== 'tree') return toast.error('Unsupported subpath type', { description: 'Expected tree subpath, got: ' + subpath })
|
||||
|
||||
let base = STORAGE_TYPE_MAP[type]
|
||||
if (!base) {
|
||||
if (!/[a-z0-9]{4}-[a-z0-9]{4}/i.test(type)) return toast.error('Unsupported storage type')
|
||||
base = `/storage/${type}/`
|
||||
}
|
||||
IPC.emit('path', base + path.join(''))
|
||||
})
|
||||
|
||||
// schema: miru://key/value
|
||||
const protocolMap = {
|
||||
auth: token => sendToken(token),
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ if (typeof localStorage === 'undefined') {
|
|||
}
|
||||
}
|
||||
|
||||
let client
|
||||
|
||||
channel.on('port-init', data => {
|
||||
localStorage.setItem('settings', data)
|
||||
const port = {
|
||||
|
|
@ -24,11 +26,18 @@ channel.on('port-init', data => {
|
|||
channel.send('ipc', { data })
|
||||
}
|
||||
}
|
||||
let storedSettings = {}
|
||||
|
||||
try {
|
||||
storedSettings = JSON.parse(localStorage.getItem('settings')) || {}
|
||||
} catch (error) {}
|
||||
|
||||
channel.on('ipc', a => port.onmessage(a))
|
||||
channel.emit('port', {
|
||||
ports: [port]
|
||||
})
|
||||
})
|
||||
if (!client) {
|
||||
client = new TorrentClient(channel, storageQuota, 'node', storedSettings.torrentPathNew || env.TMPDIR)
|
||||
|
||||
globalThis.client = new TorrentClient(channel, storageQuota, 'node', env.TMPDIR)
|
||||
channel.emit('port', {
|
||||
ports: [port]
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,11 +5,7 @@ export const SUPPORTS = {
|
|||
update: false,
|
||||
angle: false,
|
||||
doh: false,
|
||||
dht: true,
|
||||
discord: false,
|
||||
torrentPort: true,
|
||||
torrentPath: false,
|
||||
torrentPersist: false,
|
||||
keybinds: false,
|
||||
isAndroid: true,
|
||||
externalPlayer: false,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { writable } from 'simple-store-svelte'
|
||||
import { anilistClient } from '@/modules/anilist.js'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
import { rss } from './views/TorrentSearch/TorrentModal.svelte'
|
||||
|
||||
export const page = writable('home')
|
||||
export const overlay = writable('none')
|
||||
|
|
@ -15,6 +16,37 @@
|
|||
IPC.on('schedule', () => {
|
||||
page.set('schedule')
|
||||
})
|
||||
|
||||
let ignoreNext = false
|
||||
function addPage (value, type) {
|
||||
if (ignoreNext) {
|
||||
ignoreNext = false
|
||||
return
|
||||
}
|
||||
history.pushState({ type, value }, '', location.origin + location.pathname + '?id=' + Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER).toString())
|
||||
}
|
||||
page.subscribe((value) => {
|
||||
addPage(value, 'page')
|
||||
})
|
||||
view.subscribe((value) => {
|
||||
addPage(value, 'view')
|
||||
})
|
||||
|
||||
addPage('home', 'page')
|
||||
|
||||
window.addEventListener('popstate', e => {
|
||||
const { state } = e
|
||||
if (!state) return
|
||||
ignoreNext = true
|
||||
view.set(null)
|
||||
rss.set(null)
|
||||
if (document.fullscreenElement) document.exitFullscreen()
|
||||
if (state.type === 'page') {
|
||||
page.set(state.value)
|
||||
} else {
|
||||
view.set(state.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
import { click } from '@/modules/click.js'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
import NavbarLink from './NavbarLink.svelte'
|
||||
import { MagnifyingGlass } from 'svelte-radix'
|
||||
import { Users, Clock, ListMusic, Settings, Heart } from 'lucide-svelte'
|
||||
const view = getContext('view')
|
||||
export let page
|
||||
function close () {
|
||||
|
|
@ -15,13 +17,47 @@
|
|||
<nav class='navbar navbar-fixed-bottom d-block d-md-none border-0 bg-dark'>
|
||||
<div class='navbar-menu h-full d-flex flex-row justify-content-center align-items-center m-0 pb-5' class:animate={page !== 'player'}>
|
||||
<img src='./logo_filled.png' class='w-50 h-50 m-10 pointer p-5' alt='ico' use:click={close} />
|
||||
<NavbarLink click={() => { page = 'search'; if ($view) $view = null }} _page='search' css='ml-auto' icon='search' {page} />
|
||||
<NavbarLink click={() => { page = 'schedule' }} _page='schedule' icon='schedule' {page} />
|
||||
<NavbarLink click={() => { page = 'search'; if ($view) $view = null }} _page='search' css='ml-auto' icon='search' {page} let:active>
|
||||
<MagnifyingGlass size='2rem' class='flex-shrink-0 p-5 w-30 h-30 m-5 rounded' stroke-width={active ? '2' : '0'} stroke='currentColor' />
|
||||
</NavbarLink>
|
||||
<NavbarLink click={() => { page = 'schedule' }} _page='schedule' icon='schedule' {page} let:active>
|
||||
<Clock size='2rem' class='flex-shrink-0 p-5 w-30 h-30 m-5 rounded' strokeWidth={active ? '3.5' : '2'} />
|
||||
</NavbarLink>
|
||||
{#if $media?.media}
|
||||
<NavbarLink click={() => { $view = $media.media }} icon='queue_music' {page} />
|
||||
<NavbarLink click={() => { $view = $media.media }} icon='queue_music' {page} let:active>
|
||||
<ListMusic size='2rem' class='flex-shrink-0 p-5 w-30 h-30 m-5 rounded' strokeWidth={active ? '3.5' : '2'} />
|
||||
</NavbarLink>
|
||||
{/if}
|
||||
<NavbarLink click={() => { page = 'watchtogether' }} _page='watchtogether' icon='groups' {page} />
|
||||
<NavbarLink click={() => { IPC.emit('open', 'https://github.com/sponsors/ThaUnknown/') }} icon='favorite' css='ml-auto donate' {page} />
|
||||
<NavbarLink click={() => { page = 'settings' }} _page='settings' icon='settings' {page} />
|
||||
<NavbarLink click={() => { page = 'watchtogether' }} _page='watchtogether' icon='groups' {page} let:active>
|
||||
<Users size='2rem' class='flex-shrink-0 p-5 w-30 h-30 m-5 rounded' strokeWidth={active ? '3.5' : '2'} />
|
||||
</NavbarLink>
|
||||
<NavbarLink click={() => { IPC.emit('open', 'https://github.com/sponsors/ThaUnknown/') }} icon='favorite' css='ml-auto donate' {page} let:active>
|
||||
<Heart size='2rem' class='flex-shrink-0 p-5 w-30 h-30 m-5 rounded donate' strokeWidth={active ? '3.5' : '2'} fill='currentColor' />
|
||||
</NavbarLink>
|
||||
<NavbarLink click={() => { page = 'settings' }} _page='settings' icon='settings' {page} let:active>
|
||||
<Settings size='2rem' class='flex-shrink-0 p-5 w-30 h-30 m-5 rounded' strokeWidth={active ? '3.5' : '2'} />
|
||||
</NavbarLink>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
.navbar .animate :global(.donate) {
|
||||
animation: glow 1s ease-in-out infinite alternate;
|
||||
}
|
||||
.navbar :global(.donate):active {
|
||||
color: #fa68b6 !important;
|
||||
}
|
||||
.navbar :global(.donate) {
|
||||
font-variation-settings: 'FILL' 1;
|
||||
color: #fa68b6;
|
||||
text-shadow: 0 0 1rem #fa68b6;
|
||||
}
|
||||
@keyframes glow {
|
||||
from {
|
||||
filter: drop-shadow(0 0 1rem #fa68b6);
|
||||
}
|
||||
to {
|
||||
filter: drop-shadow(0 0 0.5rem #fa68b6);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
let _click = () => {}
|
||||
export { _click as click }
|
||||
export let image = ''
|
||||
export let page
|
||||
export let _page = ''
|
||||
export let css = ''
|
||||
|
|
@ -13,89 +12,31 @@
|
|||
<div
|
||||
class='navbar-link navbar-link-with-icon pointer overflow-hidden {css}'
|
||||
use:click={() => { _click(); if (!icon.includes("favorite")) { window.dispatchEvent(new Event('overlay-check')) } } }>
|
||||
{#if image}
|
||||
<span class='material-symbols-outlined rounded' class:filled={page === _page}>
|
||||
<img src={image} class='h-30 rounded' alt='logo' />
|
||||
</span>
|
||||
{:else}
|
||||
<span class='material-symbols-outlined rounded' class:filled={page === _page}>{icon}</span>
|
||||
{/if}
|
||||
|
||||
<span class='rounded d-flex'>
|
||||
<slot active={page === _page}>{icon}</slot>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes glow {
|
||||
from {
|
||||
text-shadow: 0 0 2rem #fa68b6;
|
||||
}
|
||||
to {
|
||||
text-shadow: 0 0 1rem #fa68b6;
|
||||
}
|
||||
}
|
||||
.animate .donate .material-symbols-outlined {
|
||||
animation: glow 1s ease-in-out infinite alternate;
|
||||
}
|
||||
.donate:hover .material-symbols-outlined {
|
||||
background: #fff;
|
||||
color: #fa68b6 !important;
|
||||
}
|
||||
.donate .material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 1;
|
||||
color: #fa68b6;
|
||||
text-shadow: 0 0 1rem #fa68b6;
|
||||
}
|
||||
/* .sidebar-menu {
|
||||
padding-top: 10rem;
|
||||
} */
|
||||
.text {
|
||||
opacity: 1;
|
||||
transition: opacity 0.8s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-link > span {
|
||||
color: #fff;
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
.navbar-link > span {
|
||||
color: #fff;
|
||||
transition: background .8s cubic-bezier(0.25, 0.8, 0.25, 1), color .8s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
|
||||
.navbar-link:hover > span {
|
||||
.navbar-link:active > span {
|
||||
background: #fff;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.navbar-link {
|
||||
font-size: 1.4rem;
|
||||
padding: 0.75rem;
|
||||
height: 5.5rem;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: 2.2rem;
|
||||
min-width: 4rem;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-link img {
|
||||
font-size: 2.2rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin: 0.5rem;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-right: var(--sidebar-brand-image-margin-right);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script context='module'>
|
||||
const badgeKeys = ['title', 'search', 'genre', 'tag', 'season', 'year', 'format', 'status', 'sort', 'hideSubs', 'hideMyAnime', 'hideStatus']
|
||||
const badgeDisplayNames = { title: 'recent_actors', search: 'title', genre: 'theater_comedy', tag: 'tag', season: 'calendar_month', year: 'spa', format: 'monitor', status: 'live_tv', sort: 'sort', hideMyAnime: 'tune', hideSubs: 'mic' }
|
||||
const badgeDisplayNames = { title: BookUser, search: Type, genre: Drama, tag: Hash, season: CalendarRange, year: Leaf, format: Tv, status: MonitorPlay, sort: ArrowDownWideNarrow, hideMyAnime: SlidersHorizontal, hideSubs: Mic }
|
||||
const sortOptions = { START_DATE_DESC: 'Release Date', SCORE_DESC: 'Score', POPULARITY_DESC: 'Popularity', TRENDING_DESC: 'Trending', UPDATED_TIME_DESC: 'Last Updated', STARTED_ON_DESC: 'Started On', FINISHED_ON_DESC: 'Finished On', PROGRESS_DESC: 'Your Progress', USER_SCORE_DESC: 'Your Score' }
|
||||
|
||||
export function searchCleanup (search, badge) {
|
||||
|
|
@ -17,6 +17,8 @@
|
|||
import { page } from '@/App.svelte'
|
||||
import { toast } from 'svelte-sonner'
|
||||
import Helper from '@/modules/helper.js'
|
||||
import { MagnifyingGlass, Image } from 'svelte-radix'
|
||||
import { BookUser, Type, Drama, Leaf, CalendarRange, MonitorPlay, Tv, ArrowDownWideNarrow, Filter, FilterX, Tags, Hash, SlidersHorizontal, Mic, Grid3X3, Grid2X2 } from 'lucide-svelte'
|
||||
|
||||
export let search
|
||||
let searchTextInput = {
|
||||
|
|
@ -256,10 +258,6 @@
|
|||
return sortOptions[value] || value
|
||||
}
|
||||
|
||||
function getBadgeDisplayName(key) {
|
||||
return badgeDisplayNames[key] || ''
|
||||
}
|
||||
|
||||
function removeBadge(badge) {
|
||||
if (badge.key === 'title') {
|
||||
delete search.load
|
||||
|
|
@ -341,16 +339,16 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<form class='container-fluid py-20 px-md-50 px-20 bg-dark pb-0 position-sticky top-0 search-container z-40' on:input bind:this={form}>
|
||||
<form class='container-fluid py-20 px-md-50 bg-dark pb-0 position-sticky top-0 search-container z-40' on:input bind:this={form}>
|
||||
<div class='row'>
|
||||
<div class='col-lg col-4 p-10 d-flex flex-column justify-content-end'>
|
||||
<div class='pb-10 font-size-24 font-weight-semi-bold d-flex'>
|
||||
<div class='material-symbols-outlined mr-10 font-size-30'>title</div>
|
||||
<Type class='mr-10' size='3rem' />
|
||||
<div>Title</div>
|
||||
</div>
|
||||
<div class='input-group'>
|
||||
<div class='input-group-prepend'>
|
||||
<span class='input-group-text d-flex material-symbols-outlined bg-dark-light pr-0 font-size-18'>search</span>
|
||||
<MagnifyingGlass size='2.75rem' class='input-group-text bg-dark-light pr-0' />
|
||||
</div>
|
||||
<input
|
||||
bind:this={searchTextInput.title}
|
||||
|
|
@ -365,7 +363,7 @@
|
|||
</div>
|
||||
<div class='col-lg col-4 p-10 d-flex flex-column justify-content-end'>
|
||||
<div class='pb-10 font-size-24 font-weight-semi-bold d-flex'>
|
||||
<div class='material-symbols-outlined mr-10 font-size-30'>theater_comedy</div>
|
||||
<Drama class='mr-10' size='3rem' />
|
||||
<div>Genres</div>
|
||||
</div>
|
||||
<div class='input-group'>
|
||||
|
|
@ -392,7 +390,7 @@
|
|||
</div>
|
||||
<div class='col-lg col-4 p-10 d-flex flex-column justify-content-end'>
|
||||
<div class='pb-10 font-size-24 font-weight-semi-bold d-flex'>
|
||||
<div class='material-symbols-outlined mr-10 font-size-30'>tag</div>
|
||||
<Hash class='mr-10' size='3rem' />
|
||||
<div>Tags</div>
|
||||
</div>
|
||||
<div class='input-group'>
|
||||
|
|
@ -417,7 +415,7 @@
|
|||
</div>
|
||||
<div class='col-lg col-4 p-10 d-flex flex-column justify-content-end'>
|
||||
<div class='pb-10 font-size-24 font-weight-semi-bold d-flex'>
|
||||
<div class='material-symbols-outlined mr-10 font-size-30'>calendar_month</div>
|
||||
<CalendarRange class='mr-10' size='3rem' />
|
||||
<div>Season</div>
|
||||
</div>
|
||||
<div class='input-group'>
|
||||
|
|
@ -439,7 +437,7 @@
|
|||
</div>
|
||||
<div class='col p-10 d-flex flex-column justify-content-end'>
|
||||
<div class='pb-10 font-size-24 font-weight-semi-bold d-flex'>
|
||||
<div class='material-symbols-outlined mr-10 font-size-30'>monitor</div>
|
||||
<Tv class='mr-10' size='3rem' />
|
||||
<div>Format</div>
|
||||
</div>
|
||||
<div class='input-group'>
|
||||
|
|
@ -455,7 +453,7 @@
|
|||
</div>
|
||||
<div class='col p-10 d-flex flex-column justify-content-end'>
|
||||
<div class='pb-10 font-size-24 font-weight-semi-bold d-flex'>
|
||||
<div class='material-symbols-outlined mr-10 font-size-30'>live_tv</div>
|
||||
<MonitorPlay class='mr-10' size='3rem' />
|
||||
<div>Status</div>
|
||||
</div>
|
||||
<div class='input-group'>
|
||||
|
|
@ -470,7 +468,7 @@
|
|||
</div>
|
||||
<div class='col p-10 d-flex flex-column justify-content-end'>
|
||||
<div class='pb-10 font-size-24 font-weight-semi-bold d-flex'>
|
||||
<div class='material-symbols-outlined mr-10 font-size-30'>sort</div>
|
||||
<ArrowDownWideNarrow class='mr-10' size='3rem' />
|
||||
<div>Sort</div>
|
||||
</div>
|
||||
<div class='input-group'>
|
||||
|
|
@ -497,13 +495,15 @@
|
|||
<div class='col-auto p-10 d-flex'>
|
||||
<div class='align-self-end'>
|
||||
<button
|
||||
class='btn btn-square bg-dark-light material-symbols-outlined font-size-18 px-5 align-self-end border-0'
|
||||
class='btn btn-square bg-dark-light px-5 align-self-end border-0'
|
||||
type='button'
|
||||
title='Hide My Anime'
|
||||
use:click={toggleHideMyAnime}
|
||||
disabled={search.disableHide || search.disableSearch}
|
||||
class:text-primary={search.hideMyAnime}>
|
||||
<label for='hide-my-anime' class='pointer mb-0'> tune </label>
|
||||
<label for='hide-my-anime' class='pointer mb-0 d-flex align-items-center justify-content-center'>
|
||||
<SlidersHorizontal size='1.625rem' />
|
||||
</label>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -511,30 +511,36 @@
|
|||
<div class='col-auto p-10 d-flex'>
|
||||
<div class='align-self-end'>
|
||||
<button
|
||||
class='btn btn-square bg-dark-light material-symbols-outlined font-size-18 px-5 align-self-end border-0'
|
||||
class='btn btn-square bg-dark-light px-5 align-self-end border-0'
|
||||
type='button'
|
||||
title='Dubbed Audio'
|
||||
use:click={toggleSubs}
|
||||
disabled={search.disableSearch}
|
||||
class:text-primary={search.hideSubs}>
|
||||
<label for='hide-subs' class='pointer mb-0'> mic </label>
|
||||
<label for='hide-subs' class='pointer mb-0 d-flex align-items-center justify-content-center'>
|
||||
<Mic size='1.625rem' />
|
||||
</label>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type='file' class='d-none' id='search-image' accept='image/*' on:input|preventDefault|stopPropagation={handleFile} />
|
||||
<div class='col-auto p-10 d-flex'>
|
||||
<div class='align-self-end'>
|
||||
<button class='btn btn-square bg-dark-light material-symbols-outlined font-size-18 px-5 align-self-end border-0' type='button' title='Image Search'>
|
||||
<label for='search-image' class='pointer mb-0'>
|
||||
image
|
||||
<button class='btn btn-square bg-dark-light px-5 align-self-end border-0' type='button' title='Image Search'>
|
||||
<label for='search-image' class='pointer mb-0 d-flex align-items-center justify-content-center'>
|
||||
<Image size='1.625rem' />
|
||||
</label>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class='col-auto p-10 d-flex'>
|
||||
<div class='align-self-end'>
|
||||
<button class='btn btn-square bg-dark-light material-symbols-outlined font-size-18 px-5 align-self-end border-0' type='button' title='Clear Search' use:click={searchClear} disabled={sanitisedSearch.length <= 0} class:text-danger={!!sanitisedSearch?.length || search.disableSearch || search.clearNext}>
|
||||
{!!sanitisedSearch?.length || search.disableSearch || search.clearNext ? 'filter_alt_off' : 'filter_alt'}
|
||||
<button class='btn btn-square bg-dark-light d-flex align-items-center justify-content-center px-5 align-self-end border-0' type='button' use:click={searchClear} disabled={sanitisedSearch.length <= 0} class:text-danger={!!sanitisedSearch?.length || search.disableSearch || search.clearNext}>
|
||||
{#if !!sanitisedSearch?.length || search.disableSearch || search.clearNext}
|
||||
<FilterX size='1.625rem' />
|
||||
{:else}
|
||||
<Filter size='1.625rem' />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -546,14 +552,14 @@
|
|||
{@const filteredBadges = sanitisedSearch.filter(badge => badge.key !== 'hideStatus' && (search.userList || badge.key !== 'title'))}
|
||||
<div class='d-flex flex-row align-items-center'>
|
||||
{#if filteredBadges.length > 0}
|
||||
<span class='material-symbols-outlined font-size-24 mr-20 filled text-dark-light'>sell</span>
|
||||
<Tags class='text-dark-light mr-20' size='3rem' />
|
||||
{/if}
|
||||
{#each badgeKeys as key}
|
||||
{@const matchingBadges = filteredBadges.filter(badge => badge.key === key)}
|
||||
{#each matchingBadges as badge}
|
||||
{#if badge.key === key && (badge.key !== 'hideStatus' && (search.userList || badge.key !== 'title')) }
|
||||
<div class='badge bg-light border-0 py-5 px-10 text-capitalize mr-20 text-white text-nowrap d-flex align-items-center'>
|
||||
<div class='material-symbols-outlined font-size-18 mr-5'>{getBadgeDisplayName(badge.key)}</div>
|
||||
<svelte:component this={badgeDisplayNames[badge.key]} class='mr-5' size='1.8rem' />
|
||||
<div class='font-size-12'>{badge.key === 'sort' ? getSortDisplayName(badge.value) : (badge.key === 'hideMyAnime' ? 'Hide My Anime' : badge.key === 'hideSubs' ? 'Dubbed' : ('' + badge.value).replace(/_/g, ' ').toLowerCase())}</div>
|
||||
<button on:click={() => removeBadge(badge)} class='pointer bg-transparent border-0 text-white font-size-12 position-relative ml-10 pt-0' title='Remove Filter' type='button'>x</button>
|
||||
</div>
|
||||
|
|
@ -564,15 +570,12 @@
|
|||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
<span class='material-symbols-outlined font-size-24 mr-10 filled ml-auto text-dark-light pointer' class:text-muted={$settings.cards === 'small'} use:click={() => changeCardMode('small')}>grid_on</span>
|
||||
<span class='material-symbols-outlined font-size-24 filled text-dark-light pointer' class:text-muted={$settings.cards === 'full'} use:click={() => changeCardMode('full')}>grid_view</span>
|
||||
<span class='mr-10 filled ml-auto text-dark-light pointer' class:text-muted={$settings.cards === 'small'} use:click={() => changeCardMode('small')}><Grid3X3 size='2.25rem' /></span>
|
||||
<span class='text-dark-light pointer' class:text-muted={$settings.cards === 'full'} use:click={() => changeCardMode('full')}><Grid2X2 size='2.25rem' /></span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.text-dark-light {
|
||||
color: var(--gray-color-light);
|
||||
}
|
||||
.input-group,
|
||||
.container-fluid button, .pointer {
|
||||
transition: scale 0.2s ease;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
import Helper from '@/modules/helper.js'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
import SidebarLink from './SidebarLink.svelte'
|
||||
import { Clock, Download, Heart, Home, ListMusic, LogIn, Settings, Users } from 'lucide-svelte'
|
||||
import { MagnifyingGlass } from 'svelte-radix'
|
||||
|
||||
let updateState = ''
|
||||
|
||||
|
|
@ -25,105 +27,71 @@
|
|||
<div class='sidebar z-30 d-md-block' class:animated={$settings.expandingSidebar}>
|
||||
<div class='sidebar-overlay pointer-events-none h-full position-absolute' />
|
||||
<div class='sidebar-menu h-full d-flex flex-column justify-content-center align-items-center m-0 pb-5' class:animate={page !== 'player'}>
|
||||
<SidebarLink click={() => { $profileView = true }} icon='login' text={Helper.getUser() ? 'Profiles' : 'Login'} css='mt-auto' {page} image={Helper.getUserAvatar()} />
|
||||
<SidebarLink click={() => { page = 'home'; if ($view) $view = null }} _page='home' icon='home' text='Home' {page} />
|
||||
<SidebarLink click={() => { page = 'search'; if ($view) $view = null }} _page='search' icon='search' text='Search' {page} />
|
||||
<SidebarLink click={() => { page = 'schedule' }} _page='schedule' icon='schedule' text='Schedule' {page} />
|
||||
<SidebarLink click={() => { $profileView = true }} icon='login' text={Helper.getUser() ? 'Profiles' : 'Login'} css='mt-auto' {page} image={Helper.getUserAvatar()}>
|
||||
<LogIn size='2rem' class='flex-shrink-0 p-5 w-30 h-30 m-5 rounded' />
|
||||
</SidebarLink>
|
||||
<SidebarLink click={() => { page = 'home'; if ($view) $view = null }} _page='home' text='Home' {page} let:active>
|
||||
<Home size='2rem' class='flex-shrink-0 p-5 w-30 h-30 m-5 rounded' strokeWidth={active ? '3.5' : '2'} />
|
||||
</SidebarLink>
|
||||
<SidebarLink click={() => { page = 'search'; if ($view) $view = null }} _page='search' text='Search' {page} let:active>
|
||||
<MagnifyingGlass size='2rem' class='flex-shrink-0 p-5 w-30 h-30 m-5 rounded' stroke-width={active ? '2' : '0'} stroke='currentColor' />
|
||||
</SidebarLink>
|
||||
<SidebarLink click={() => { page = 'schedule' }} _page='schedule' icon='schedule' text='Schedule' {page} let:active>
|
||||
<Clock size='2rem' class='flex-shrink-0 p-5 w-30 h-30 m-5 rounded' strokeWidth={active ? '3.5' : '2'} />
|
||||
</SidebarLink>
|
||||
{#if $media?.media}
|
||||
<SidebarLink click={() => { $view = $media.media }} icon='queue_music' text='Now Playing' {page} />
|
||||
<SidebarLink click={() => { $view = $media.media }} icon='queue_music' text='Now Playing' {page} let:active>
|
||||
<ListMusic size='2rem' class='flex-shrink-0 p-5 w-30 h-30 m-5 rounded' strokeWidth={active ? '3.5' : '2'} />
|
||||
</SidebarLink>
|
||||
{/if}
|
||||
<SidebarLink click={() => { page = 'watchtogether' }} _page='watchtogether' icon='groups' text='Watch Together' {page} />
|
||||
<SidebarLink click={() => { IPC.emit('open', 'https://github.com/sponsors/ThaUnknown/') }} icon='favorite' text='Support This App' css='mt-auto donate' {page} />
|
||||
<SidebarLink click={() => { page = 'watchtogether' }} _page='watchtogether' icon='groups' text='Watch Together' {page} let:active>
|
||||
<Users size='2rem' class='flex-shrink-0 p-5 w-30 h-30 m-5 rounded' strokeWidth={active ? '3.5' : '2'} />
|
||||
</SidebarLink>
|
||||
<SidebarLink click={() => { IPC.emit('open', 'https://github.com/sponsors/ThaUnknown/') }} icon='favorite' text='Support This App' css='mt-auto' {page} let:active>
|
||||
<Heart size='2rem' class='flex-shrink-0 p-5 w-30 h-30 m-5 rounded donate' strokeWidth={active ? '3.5' : '2'} fill='currentColor' />
|
||||
</SidebarLink>
|
||||
{#if updateState === 'downloading'}
|
||||
<SidebarLink click={() => { toast('Update is downloading...') }} icon='download' text='Update Downloading...' {page} />
|
||||
<SidebarLink click={() => { toast('Update is downloading...') }} icon='download' text='Update Downloading...' {page} let:active>
|
||||
<Download size='2rem' class='flex-shrink-0 p-5 w-30 h-30 m-5 rounded' strokeWidth={active ? '3.5' : '2'} />
|
||||
</SidebarLink>
|
||||
{:else if updateState === 'ready'}
|
||||
<SidebarLink click={() => { IPC.emit('quit-and-install') }} css='update' icon='download' text='Update Ready!' {page} />
|
||||
<SidebarLink click={() => { IPC.emit('quit-and-install') }} icon='download' text='Update Ready!' {page} let:active>
|
||||
<Download size='2rem' class='flex-shrink-0 p-5 w-30 h-30 m-5 rounded update' strokeWidth={active ? '3.5' : '2'} />
|
||||
</SidebarLink>
|
||||
{/if}
|
||||
<SidebarLink click={() => { page = 'settings' }} _page='settings' icon='settings' text='Settings' {page} />
|
||||
<SidebarLink click={() => { page = 'settings' }} _page='settings' icon='settings' text='Settings' {page} let:active>
|
||||
<Settings size='2rem' class='flex-shrink-0 p-5 w-30 h-30 m-5 rounded' strokeWidth={active ? '3.5' : '2'} />
|
||||
</SidebarLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes glow {
|
||||
from {
|
||||
text-shadow: 0 0 2rem #fa68b6;
|
||||
}
|
||||
to {
|
||||
text-shadow: 0 0 1rem #fa68b6;
|
||||
}
|
||||
}
|
||||
.animate .donate .material-symbols-outlined {
|
||||
.sidebar .animate :global(.donate) {
|
||||
animation: glow 1s ease-in-out infinite alternate;
|
||||
}
|
||||
.donate:hover .material-symbols-outlined {
|
||||
background: #fff;
|
||||
.sidebar :global(.donate):hover {
|
||||
color: #fa68b6 !important;
|
||||
}
|
||||
.donate .material-symbols-outlined {
|
||||
.sidebar :global(.donate) {
|
||||
font-variation-settings: 'FILL' 1;
|
||||
color: #fa68b6;
|
||||
text-shadow: 0 0 1rem #fa68b6;
|
||||
}
|
||||
.update .material-symbols-outlined {
|
||||
:global(.update) {
|
||||
color: #47cb6a;
|
||||
font-variation-settings: 'FILL' 1;
|
||||
}
|
||||
@keyframes glow {
|
||||
from {
|
||||
filter: drop-shadow(0 0 1rem #fa68b6);
|
||||
}
|
||||
to {
|
||||
filter: drop-shadow(0 0 0.5rem #fa68b6);
|
||||
}
|
||||
}
|
||||
.sidebar-menu {
|
||||
padding-top: 10rem;
|
||||
}
|
||||
.text {
|
||||
opacity: 1;
|
||||
transition: opacity 0.8s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sidebar-link > span {
|
||||
color: #fff;
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
color: #fff;
|
||||
transition: background .8s cubic-bezier(0.25, 0.8, 0.25, 1), color .8s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
|
||||
.sidebar-link:hover > span > *:nth-child(1) {
|
||||
background: #fff;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.sidebar-link {
|
||||
width: 100%;
|
||||
font-size: 1.4rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
height: 5.5rem;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: 2.2rem;
|
||||
min-width: 4rem;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sidebar-link img {
|
||||
font-size: 2.2rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin: 0.5rem;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-right: var(--sidebar-brand-image-margin-right);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
transition: width .8s cubic-bezier(0.25, 0.8, 0.25, 1), left .8s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
|
||||
|
|
|
|||
|
|
@ -11,50 +11,24 @@
|
|||
export let icon = ''
|
||||
</script>
|
||||
|
||||
<div
|
||||
class='sidebar-link sidebar-link-with-icon pointer overflow-hidden {css}'
|
||||
<div class='sidebar-link sidebar-link-with-icon pointer overflow-hidden {css}'
|
||||
use:click={() => { _click(); if (!icon.includes("login") && !icon.includes("favorite")) { window.dispatchEvent(new Event('overlay-check')) } } }>
|
||||
<span class='text-nowrap d-flex align-items-center w-full h-full'>
|
||||
{#if image}
|
||||
<span class='material-symbols-outlined rounded' class:filled={page === _page}>
|
||||
<span class='rounded d-flex'>
|
||||
<img src={image} class='h-30 rounded' alt='logo' />
|
||||
</span>
|
||||
<span class='text ml-20'>{text}</span>
|
||||
{:else}
|
||||
<span class='material-symbols-outlined rounded' class:filled={page === _page}>{icon}</span>
|
||||
<span class='rounded d-flex'>
|
||||
<slot active={page === _page}>{icon}</slot>
|
||||
</span>
|
||||
<span class='text ml-20'>{text}</span>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes glow {
|
||||
from {
|
||||
text-shadow: 0 0 2rem #fa68b6;
|
||||
}
|
||||
to {
|
||||
text-shadow: 0 0 1rem #fa68b6;
|
||||
}
|
||||
}
|
||||
.animate .donate .material-symbols-outlined {
|
||||
animation: glow 1s ease-in-out infinite alternate;
|
||||
}
|
||||
.donate:hover .material-symbols-outlined {
|
||||
background: #fff;
|
||||
color: #fa68b6 !important;
|
||||
}
|
||||
.donate .material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 1;
|
||||
color: #fa68b6;
|
||||
text-shadow: 0 0 1rem #fa68b6;
|
||||
}
|
||||
.update .material-symbols-outlined {
|
||||
color: #47cb6a;
|
||||
font-variation-settings: 'FILL' 1;
|
||||
}
|
||||
.sidebar-menu {
|
||||
padding-top: 10rem;
|
||||
}
|
||||
.text {
|
||||
opacity: 1;
|
||||
transition: opacity 0.8s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
|
|
@ -68,12 +42,12 @@
|
|||
border-radius: 0.3rem;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
.sidebar-link > span > span:nth-child(1) {
|
||||
color: #fff;
|
||||
transition: background .8s cubic-bezier(0.25, 0.8, 0.25, 1), color .8s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
|
||||
.sidebar-link:hover > span > *:nth-child(1) {
|
||||
.sidebar-link:hover > span > span:nth-child(1) {
|
||||
background: #fff;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
|
@ -85,16 +59,6 @@
|
|||
height: 5.5rem;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: 2.2rem;
|
||||
min-width: 4rem;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sidebar-link img {
|
||||
font-size: 2.2rem;
|
||||
width: 3rem;
|
||||
|
|
@ -108,22 +72,4 @@
|
|||
img {
|
||||
margin-right: var(--sidebar-brand-image-margin-right);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
transition: width .8s cubic-bezier(0.25, 0.8, 0.25, 1), left .8s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
|
||||
background: none !important;
|
||||
overflow-y: unset;
|
||||
overflow-x: visible;
|
||||
left: unset;
|
||||
}
|
||||
.sidebar.animated:hover {
|
||||
width: 22rem
|
||||
}
|
||||
.sidebar-overlay {
|
||||
width: var(--sidebar-width);
|
||||
transition: width .8s cubic-bezier(0.25, 0.8, 0.25, 1), left .8s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
|
||||
background: var(--sidebar-gradient);
|
||||
backdrop-filter: blur(2px);
|
||||
z-index: -1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
import Scoring from '@/views/ViewAnime/Scoring.svelte'
|
||||
import AudioLabel from '@/views/ViewAnime/AudioLabel.svelte'
|
||||
import Helper from "@/modules/helper.js"
|
||||
import { Heart } from 'lucide-svelte'
|
||||
|
||||
export let mediaList
|
||||
|
||||
let current = mediaList[0]
|
||||
|
|
@ -92,8 +94,8 @@
|
|||
use:click={() => playMedia(current)}>
|
||||
Watch Now
|
||||
</button>
|
||||
<button class='btn bg-dark-light btn-square ml-10 material-symbols-outlined font-size-16 shadow-none border-0' class:filled={current.isFavourite} use:click={toggleFavourite} disabled={!Helper.isAniAuth()}>
|
||||
favorite
|
||||
<button class='btn bg-dark-light btn-square ml-10 d-flex align-items-center justify-content-center shadow-none border-0' use:click={toggleFavourite} disabled={!Helper.isAniAuth()}>
|
||||
<Heart fill={current.isFavourite ? 'currentColor' : 'transparent'} size='1.5rem' />
|
||||
</button>
|
||||
<Scoring media={current} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import { getContext } from 'svelte'
|
||||
import { liveAnimeEpisodeProgress } from '@/modules/animeprogress.js'
|
||||
import { anilistClient } from '@/modules/anilist.js'
|
||||
import { Play } from 'lucide-svelte'
|
||||
export let data
|
||||
|
||||
let preview = false
|
||||
|
|
@ -37,19 +38,14 @@
|
|||
const completed = !watched && media?.mediaListEntry?.progress >= data?.episode
|
||||
</script>
|
||||
|
||||
<div class='d-flex p-20 pb-10 position-relative episode-card' use:hoverChange={() => prompt.set(false)} use:hoverClick={[setClickState, setHoverState]}>
|
||||
<div class='d-flex p-20 pb-10 position-relative episode-card' use:hoverChange={() => prompt.set(false)} use:hoverClick={[setClickState, setHoverState]} on:contextmenu|preventDefault={viewMedia} role='none'>
|
||||
{#if preview}
|
||||
<EpisodePreviewCard {data} bind:prompt={$prompt} />
|
||||
{/if}
|
||||
<div class='item d-flex flex-column h-full pointer content-visibility-auto' class:opacity-half={completed}>
|
||||
<div class='image h-200 w-full position-relative rounded overflow-hidden d-flex justify-content-between align-items-end text-white' class:bg-black={episodeThumbnail === ' '}>
|
||||
<img loading='lazy' src={episodeThumbnail} alt='cover' class='cover-img w-full h-full position-absolute' style:--color={media?.coverImage?.color || '#1890ff'} />
|
||||
{#if data.failed}
|
||||
<div class='material-symbols-outlined pl-10 pt-10 position-absolute top-0 left-0 text-danger filled font-weight-medium' title='Failed to resolve media z-10'>
|
||||
sync_problem
|
||||
</div>
|
||||
{/if}
|
||||
<div class='pl-10 pb-10 material-symbols-outlined filled z-10'>play_arrow</div>
|
||||
<Play class='mb-5 ml-5 pl-10 pb-10 z-10' fill='currentColor' size='3rem' />
|
||||
<div class='pr-15 pb-10 font-size-16 font-weight-medium z-10'>
|
||||
{#if media?.duration}
|
||||
{media.duration}m
|
||||
|
|
@ -122,9 +118,6 @@
|
|||
.opacity-half {
|
||||
opacity: 30%;
|
||||
}
|
||||
.material-symbols-outlined {
|
||||
font-size: 3rem;
|
||||
}
|
||||
.title {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
import { anilistClient } from "@/modules/anilist"
|
||||
import AudioLabel from '@/views/ViewAnime/AudioLabel.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import { CalendarDays, Play, Tv } from 'lucide-svelte'
|
||||
|
||||
export let data
|
||||
export let prompt
|
||||
/** @type {import('@/modules/al.d.ts').Media | null} */
|
||||
|
|
@ -38,12 +40,7 @@
|
|||
on:loadeddata={() => { hide = false }}
|
||||
autoplay />
|
||||
{/if}
|
||||
{#if data.failed}
|
||||
<div class='material-symbols-outlined pl-10 pt-10 position-absolute top-0 left-0 text-danger filled font-weight-medium z-10' title='Failed to resolve media'>
|
||||
sync_problem
|
||||
</div>
|
||||
{/if}
|
||||
<div class='pl-15 pb-10 material-symbols-outlined filled z-10'>play_arrow</div>
|
||||
<Play class='mb-5 ml-5 pl-10 pb-10 z-10' fill='currentColor' size='3rem' />
|
||||
<div class='pr-20 pb-10 font-size-16 font-weight-medium z-10'>
|
||||
{#if media?.duration}
|
||||
{media.duration}m
|
||||
|
|
@ -111,13 +108,13 @@
|
|||
</div>
|
||||
{#if media}
|
||||
<div class='d-flex flex-row pt-15 font-weight-medium justify-content-between w-full text-muted'>
|
||||
<div class='d-flex align-items-center' style='margin-left: -3px'>
|
||||
<span class='material-symbols-outlined font-size-24 pr-5'>calendar_month</span>
|
||||
{media.seasonYear || 'N/A'}
|
||||
<div class='d-flex align-items-center' style='margin-left: -2px'>
|
||||
<CalendarDays class='pr-5' size='2.6rem' />
|
||||
<span class='line-height-1'>{media.seasonYear || 'N/A'}</span>
|
||||
</div>
|
||||
<div class='d-flex align-items-center'>
|
||||
{formatMap[media.format]}
|
||||
<span class='material-symbols-outlined font-size-24 pl-5'>monitor</span>
|
||||
<span class='line-height-1'>{formatMap[media.format]}</span>
|
||||
<Tv class='pl-5' size='2.6rem' />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -128,9 +125,7 @@
|
|||
use:click={() => {
|
||||
data.onclick() || viewMedia()
|
||||
}}>
|
||||
<span class='material-symbols-outlined font-size-24 filled pr-10'>
|
||||
play_arrow
|
||||
</span>
|
||||
<Play class='mr-10' fill='currentColor' size='1.6rem' />
|
||||
Continue Anyway?
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -140,9 +135,6 @@
|
|||
.overlay {
|
||||
background-color: rgba(28, 28, 28, 0.9);
|
||||
}
|
||||
.material-symbols-outlined {
|
||||
font-size: 3rem;
|
||||
}
|
||||
.description {
|
||||
display: -webkit-box !important;
|
||||
-webkit-line-clamp: 3;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
import Scoring from '@/views/ViewAnime/Scoring.svelte'
|
||||
import AudioLabel from '@/views/ViewAnime/AudioLabel.svelte'
|
||||
import Helper from "@/modules/helper.js"
|
||||
import { Heart, Play, VolumeX, Volume2, ThumbsUp, ThumbsDown } from 'lucide-svelte'
|
||||
|
||||
/** @type {import('@/modules/al.d.ts').Media} */
|
||||
export let media
|
||||
export let type = null
|
||||
|
|
@ -37,9 +39,6 @@
|
|||
playMedia(media)
|
||||
}
|
||||
|
||||
function volume (video) {
|
||||
video.volume = 0.1
|
||||
}
|
||||
let muted = true
|
||||
function toggleMute () {
|
||||
muted = !muted
|
||||
|
|
@ -47,13 +46,19 @@
|
|||
</script>
|
||||
|
||||
<div class='position-absolute w-350 h-400 absolute-container top-0 bottom-0 m-auto bg-dark-light z-30 rounded overflow-hidden pointer'>
|
||||
<div class='banner position-relative bg-black'>
|
||||
<div class='banner position-relative bg-black overflow-hidden'>
|
||||
<img src={media.bannerImage || (media.trailer?.id ? `https://i.ytimg.com/vi/${media.trailer?.id}/hqdefault.jpg` : media.coverImage?.extraLarge || ' ')} alt='banner' class='img-cover w-full h-full' />
|
||||
{#if media.trailer?.id}
|
||||
<div class='material-symbols-outlined filled position-absolute z-10 top-0 right-0 p-15 font-size-22' class:d-none={hide} use:click={toggleMute}>{muted ? 'volume_off' : 'volume_up'}</div>
|
||||
<!-- for now we use some invidious instance, would be nice to somehow get these links outselves, this redirects straight to some google endpoint -->
|
||||
<!-- eslint-disable-next-line svelte/valid-compile -->
|
||||
<video src={`https://inv.tux.pizza/latest_version?id=${media.trailer.id}&itag=18`}
|
||||
<div class='position-absolute z-10 top-0 right-0 p-15' use:click={toggleMute}>
|
||||
{#if muted}
|
||||
<VolumeX size='2.2rem' fill='currentColor' />
|
||||
{:else}
|
||||
<Volume2 size='2.2rem' fill='currentColor' />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- indivious is nice because its faster, but not reliable -->
|
||||
<!-- <video src={`https://inv.tux.pizza/latest_version?id=${media.trailer.id}&itag=18`}
|
||||
class='w-full h-full position-absolute left-0'
|
||||
class:d-none={hide}
|
||||
playsinline
|
||||
|
|
@ -62,15 +67,15 @@
|
|||
use:volume
|
||||
bind:muted
|
||||
on:loadeddata={() => { hide = false }}
|
||||
autoplay />
|
||||
<!-- <iframe
|
||||
autoplay /> -->
|
||||
<iframe
|
||||
class='w-full border-0 position-absolute left-0'
|
||||
class:d-none={hide}
|
||||
title={media.title.userPreferred}
|
||||
allow='autoplay'
|
||||
on:load={() => { hide = false }}
|
||||
src={`https://www.youtube-nocookie.com/embed/${media.trailer?.id}?autoplay=1&controls=0&mute=1&disablekb=1&loop=1&vq=medium&playlist=${media.trailer?.id}`}
|
||||
/> -->
|
||||
src={`https://www.youtube-nocookie.com/embed/${media.trailer?.id}?autoplay=1&controls=0&mute=${muted ? 1 : 0}&disablekb=1&loop=1&vq=medium&playlist=${media.trailer?.id}&cc_lang_pref=ja`}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class='w-full px-20'>
|
||||
|
|
@ -81,13 +86,11 @@
|
|||
<button class='btn btn-secondary flex-grow-1 text-dark font-weight-bold shadow-none border-0 d-flex align-items-center justify-content-center'
|
||||
use:click={play}
|
||||
disabled={media.status === 'NOT_YET_RELEASED'}>
|
||||
<span class='material-symbols-outlined font-size-20 filled pr-10'>
|
||||
play_arrow
|
||||
</span>
|
||||
<Play class='pr-10 z-10' fill='currentColor' size='2.2rem' />
|
||||
{playButtonText}
|
||||
</button>
|
||||
<button class='btn btn-square ml-10 material-symbols-outlined font-size-16 shadow-none border-0' class:filled={media.isFavourite} use:click={toggleFavourite} disabled={!Helper.isAniAuth()}>
|
||||
favorite
|
||||
<button class='btn btn-square ml-10 d-flex align-items-center justify-content-center shadow-none border-0' use:click={toggleFavourite} disabled={!Helper.isAniAuth()}>
|
||||
<Heart fill={media.isFavourite ? 'currentColor' : 'transparent'} size='1.5rem' />
|
||||
</button>
|
||||
<Scoring {media} previewAnime={true}/>
|
||||
</div>
|
||||
|
|
@ -95,13 +98,9 @@
|
|||
{#if type || type === 0}
|
||||
<span class='context-type text-nowrap d-flex align-items-center'>
|
||||
{#if Number.isInteger(type) && type >= 0}
|
||||
<span class='material-symbols-outlined filled font-size-18 pr-5 {type === 0 ? "text-muted" : "text-success"}'>
|
||||
thumb_up
|
||||
</span>
|
||||
<ThumbsUp fill='currentColor'class='pr-5 pb-5 {type === 0 ? "text-muted" : "text-success"}' size='2rem' />
|
||||
{:else if Number.isInteger(type) && type < 0}
|
||||
<span class='material-symbols-outlined text-danger filled font-size-18 pr-5'>
|
||||
thumb_down
|
||||
</span>
|
||||
<ThumbsDown fill='currentColor' class='text-danger pr-5 pb-5' size='2rem' />
|
||||
{/if}
|
||||
{(Number.isInteger(type) ? Math.abs(type).toLocaleString() + (type >= 0 ? ' likes' : ' dislikes') : type)}
|
||||
</span>
|
||||
|
|
@ -171,9 +170,9 @@
|
|||
.banner {
|
||||
height: 45%
|
||||
}
|
||||
video {
|
||||
/* video {
|
||||
object-fit: cover;
|
||||
}
|
||||
} */
|
||||
.banner::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
|
@ -200,17 +199,17 @@
|
|||
left: -100%;
|
||||
right: -100%;
|
||||
}
|
||||
/* @keyframes delayedShow {
|
||||
@keyframes delayedShow {
|
||||
to {
|
||||
visibility: visible;
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
/* iframe {
|
||||
iframe {
|
||||
height: 200%;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
visibility: hidden;
|
||||
animation: 0s linear 0.5s forwards delayedShow ;
|
||||
} */
|
||||
animation: 0s linear 0.5s forwards delayedShow;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
import { page } from '@/App.svelte'
|
||||
import { anilistClient } from "@/modules/anilist"
|
||||
import Helper from "@/modules/helper.js"
|
||||
import { CalendarDays, Tv, ThumbsUp, ThumbsDown } from 'lucide-svelte'
|
||||
|
||||
/** @type {import('@/modules/al.d.ts').Media} */
|
||||
export let media
|
||||
export let type = null
|
||||
|
|
@ -48,13 +50,9 @@
|
|||
{#if type || type === 0}
|
||||
<div class='context-type d-flex align-items-center'>
|
||||
{#if Number.isInteger(type) && type >= 0}
|
||||
<span class='material-symbols-outlined filled font-size-18 pr-5 {type === 0 ? "text-muted" : "text-success"}'>
|
||||
thumb_up
|
||||
</span>
|
||||
<ThumbsUp fill='currentColor' class='pr-5 pb-5 {type === 0 ? "text-muted" : "text-success"}' size='2rem' />
|
||||
{:else if Number.isInteger(type) && type < 0}
|
||||
<span class='material-symbols-outlined text-danger filled font-size-18 pr-5'>
|
||||
thumb_down
|
||||
</span>
|
||||
<ThumbsDown fill='currentColor' class='text-danger pr-5 pb-5' size='2rem' />
|
||||
{/if}
|
||||
{(Number.isInteger(type) ? Math.abs(type).toLocaleString() + (type >= 0 ? ' likes' : ' dislikes') : type)}
|
||||
</div>
|
||||
|
|
@ -66,13 +64,13 @@
|
|||
{anilistClient.title(media)}
|
||||
</div>
|
||||
<div class='d-flex flex-row mt-auto pt-10 font-weight-medium justify-content-between w-full text-muted'>
|
||||
<div class='d-flex align-items-center pr-5' style='margin-left: -2px'>
|
||||
<span class='material-symbols-outlined font-size-24 pr-5'>calendar_month</span>
|
||||
{media.seasonYear || 'N/A'}
|
||||
<div class='d-flex align-items-center pr-5' style='margin-left: -1px'>
|
||||
<CalendarDays class='pr-5' size='2.6rem' />
|
||||
<span class='line-height-1'>{media.seasonYear || 'N/A'}</span>
|
||||
</div>
|
||||
<div class='d-flex align-items-center text-nowrap text-right'>
|
||||
{formatMap[media.format]}
|
||||
<span class='material-symbols-outlined font-size-24 pl-5'>monitor</span>
|
||||
<span class='line-height-1'>{formatMap[media.format]}</span>
|
||||
<Tv class='pl-5' size='2.6rem' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -33,10 +33,37 @@
|
|||
--section-end-gradient: linear-gradient(270deg, #17191cff 0%, #17191c00 100%);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
src: /* webpackIgnore: true */ url(Roboto.ttf) format("truetype");
|
||||
unicode-range: U+0000, U+0002, U+0009, U+000D, U+0020-007E, U+00A0-0377,
|
||||
U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03E1,
|
||||
U+03F0-052F, U+1AB0-1ABE, U+1D00-1DF5, U+1DFC-1F15, U+1F18-1F1D,
|
||||
U+1F20-1F45, U+1F48-1F4D, U+1F50-1F57, U+1F59, U+1F5B, U+1F5D,
|
||||
U+1F5F-1F7D, U+1F80-1FB4, U+1FB6-1FC4, U+1FC6-1FD3, U+1FD6-1FDB,
|
||||
U+1FDD-1FEF, U+1FF2-1FF4, U+1FF6-1FFE, U+2000-2027, U+202F-205F,
|
||||
U+2070-2071, U+2074-208E, U+2090-209C, U+20A0-20BE, U+20DB-20DC,
|
||||
U+20E3, U+20E8, U+20F0, U+2100-2101, U+2103, U+2105-2106, U+2109,
|
||||
U+2113, U+2116-2117, U+211E-2123, U+2125-2126, U+212A-212B, U+212E,
|
||||
U+2132, U+213B, U+214D, U+214F-2189, U+2191, U+2193, U+2202, U+2206,
|
||||
U+220F, U+2211-2212, U+221A, U+221E, U+222B, U+2248, U+2260,
|
||||
U+2264-2265, U+2423, U+25CA, U+2669-266F, U+27E6-27EF, U+2B4E-2B4F,
|
||||
U+2B5A-2B5F, U+2C60-2C7F, U+2DE0-2E42, U+A640-A69D, U+A69F,
|
||||
U+A700-A7AD, U+A7B0-A7B1, U+A7F7-A7FF, U+A92E, U+AB30-AB5F,
|
||||
U+AB64-AB65, U+EE01-EE02, U+F6C3, U+FB00-FB06, U+FE20-FE2D, U+FEFF,
|
||||
U+FFFC-FFFD, U+1F16A-1F16B;
|
||||
}
|
||||
|
||||
.dark-mode {
|
||||
background-color: var(--dark-color) !important;
|
||||
}
|
||||
|
||||
.h-30 {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
:root {
|
||||
--sidebar-minimised: 7rem;
|
||||
|
|
@ -45,6 +72,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.line-height-1 {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.line-height-normal {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
* {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
|
@ -86,27 +121,8 @@ a[href]:active, button:not([disabled]):active, fieldset:not([disabled]):active,
|
|||
z-index: 101;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-family: "Material Symbols Outlined Variable";
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-feature-settings: "liga";
|
||||
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 64;
|
||||
}
|
||||
|
||||
.filled {
|
||||
font-variation-settings: 'FILL' 1;
|
||||
.text-dark-light {
|
||||
color: var(--gray-color-light);
|
||||
}
|
||||
|
||||
.pointer {
|
||||
|
|
@ -209,7 +225,7 @@ img[src=''], img[src=' '] {
|
|||
.skeloader .skeleloader-swipe {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: linear-gradient(to right, transparent 0%, #17191c 50%, transparent 100%);
|
||||
background: linear-gradient(to right, transparent 0%, hsl(var(--dark-color-hsl)) 50%, transparent 100%);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 200% 100%;
|
||||
animation: skeleloader 1s infinite cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import App from './App.svelte'
|
||||
import 'quartermoon/css/quartermoon-variables.css'
|
||||
import '@fontsource-variable/material-symbols-outlined/full.css'
|
||||
import '@fontsource-variable/nunito'
|
||||
import '@fontsource/roboto'
|
||||
import './css.css'
|
||||
|
||||
export default new App({ target: document.body })
|
||||
|
|
|
|||
|
|
@ -248,7 +248,6 @@ class AnilistClient {
|
|||
body: JSON.stringify({
|
||||
query: query.replace(/\s/g, '').replaceAll(' ', ' '),
|
||||
variables: {
|
||||
sort: 'TRENDING_DESC',
|
||||
page: 1,
|
||||
perPage: 50,
|
||||
status_in: '[CURRENT,PLANNING,COMPLETED,DROPPED,PAUSED,REPEATING]',
|
||||
|
|
|
|||
|
|
@ -78,7 +78,8 @@ export default new class AnimeResolver {
|
|||
|
||||
debug(`Finding ${titleObjects.length} titles: ${titleObjects.map(obj => obj.title).join(', ')}`)
|
||||
|
||||
for (const chunk of chunks(titleObjects, 62)) { // single title has a complexity of 8.1, al limits complexity to 500
|
||||
for (const chunk of chunks(titleObjects, 60)) {
|
||||
// single title has a complexity of 8.1, al limits complexity to 500, so this can be at most 62, undercut it to 60, al pagination is 50, but at most we'll do 30 titles since isAduld duplicates each title
|
||||
for (const [key, media] of await anilistClient.alSearchCompound(chunk)) {
|
||||
debug(`Found ${key} as ${media.id}: ${media.title.userPreferred}`)
|
||||
this.animeNameCache[key] = media
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { settings } from '@/modules/settings.js'
|
||||
import { exclusions } from '../rss.js'
|
||||
import { sleep } from '../util.js'
|
||||
import { anilistClient } from '../anilist.js'
|
||||
import { anitomyscript } from '../anime.js'
|
||||
|
|
@ -10,6 +9,22 @@ import Debug from 'debug'
|
|||
|
||||
const debug = Debug('ui:extensions')
|
||||
|
||||
const exclusions = ['DTS', 'TrueHD', '[EMBER]']
|
||||
const isDev = location.hostname === 'localhost'
|
||||
|
||||
const video = document.createElement('video')
|
||||
|
||||
if (!isDev && !video.canPlayType('video/mp4; codecs="hev1.1.6.L93.B0"')) {
|
||||
exclusions.push('HEVC', 'x265', 'H.265')
|
||||
}
|
||||
if (!isDev && !video.canPlayType('audio/mp4; codecs="ac-3"')) {
|
||||
exclusions.push('AC3', 'AC-3')
|
||||
}
|
||||
if (!('audioTracks' in HTMLVideoElement.prototype)) {
|
||||
exclusions.push('DUAL')
|
||||
}
|
||||
video.remove()
|
||||
|
||||
/** @typedef {import('@thaunknown/ani-resourced/sources/types.d.ts').Options} Options */
|
||||
/** @typedef {import('@thaunknown/ani-resourced/sources/types.d.ts').Result} Result */
|
||||
|
||||
|
|
@ -40,7 +55,7 @@ export default async function getResultsFromExtensions ({ media, episode, batch,
|
|||
anidbEid,
|
||||
titles: createTitles(media),
|
||||
resolution,
|
||||
exclusions
|
||||
exclusions: settings.value.enableExternal ? [] : exclusions
|
||||
}
|
||||
|
||||
const { results, errors } = await worker.query(options, { movie, batch }, settings.value.sources)
|
||||
|
|
|
|||
|
|
@ -10,22 +10,6 @@ import Debug from 'debug'
|
|||
|
||||
const debug = Debug('ui:rss')
|
||||
|
||||
export const exclusions = ['DTS', 'TrueHD', '[EMBER]']
|
||||
const isDev = location.hostname === 'localhost'
|
||||
|
||||
const video = document.createElement('video')
|
||||
|
||||
if (!isDev && !video.canPlayType('video/mp4; codecs="hev1.1.6.L93.B0"')) {
|
||||
exclusions.push('HEVC', 'x265', 'H.265')
|
||||
}
|
||||
if (!isDev && !video.canPlayType('audio/mp4; codecs="ac-3"')) {
|
||||
exclusions.push('AC3', 'AC-3')
|
||||
}
|
||||
if (!('audioTracks' in HTMLVideoElement.prototype)) {
|
||||
exclusions.push('DUAL')
|
||||
}
|
||||
video.remove()
|
||||
|
||||
export function parseRSSNodes (nodes) {
|
||||
return nodes.map(item => {
|
||||
const pubDate = item.querySelector('pubDate')?.textContent
|
||||
|
|
|
|||
|
|
@ -5,11 +5,7 @@ export const SUPPORTS = {
|
|||
update: true,
|
||||
angle: true,
|
||||
doh: true,
|
||||
dht: true,
|
||||
discord: true,
|
||||
torrentPort: true,
|
||||
torrentPath: true,
|
||||
torrentPersist: true,
|
||||
keybinds: true,
|
||||
extensions: true,
|
||||
isAndroid: false,
|
||||
|
|
|
|||
|
|
@ -81,3 +81,12 @@ export async function add (torrentID, hide) {
|
|||
client.send('torrent', torrentID)
|
||||
}
|
||||
}
|
||||
// external player for android
|
||||
client.on('open', ({ detail }) => {
|
||||
debug(`Open: ${detail}`)
|
||||
IPC.emit('intent', detail)
|
||||
})
|
||||
|
||||
IPC.on('intent-end', () => {
|
||||
client.dispatch('externalWatched')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -183,7 +183,11 @@ export const defaults = {
|
|||
angle: 'default',
|
||||
toshoURL: SUPPORTS.extensions ? decodeURIComponent(atob('aHR0cHM6Ly9mZWVkLmFuaW1ldG9zaG8ub3JnLw==')) : '',
|
||||
extensions: SUPPORTS.extensions ? ['@thaunknown/ani-resourced'] : [],
|
||||
sources: {}
|
||||
sources: {},
|
||||
enableExternal: false,
|
||||
playerPath: '',
|
||||
playerSeek: 2,
|
||||
playerSkip: false
|
||||
}
|
||||
|
||||
export const subtitleExtensions = ['srt', 'vtt', 'ass', 'ssa', 'sub', 'txt']
|
||||
|
|
|
|||
|
|
@ -37,12 +37,6 @@ const ANNOUNCE = [
|
|||
atob('aHR0cDovL3RyYWNrZXIuYW5pcmVuYS5jb206ODAvYW5ub3VuY2U=')
|
||||
]
|
||||
|
||||
let storedSettings = {}
|
||||
|
||||
try {
|
||||
storedSettings = JSON.parse(localStorage.getItem('settings')) || {}
|
||||
} catch (error) {}
|
||||
|
||||
export default class TorrentClient extends WebTorrent {
|
||||
static excludedErrorMessages = ['WebSocket', 'User-Initiated Abort, reason=', 'Connection failed.']
|
||||
|
||||
|
|
@ -54,6 +48,12 @@ export default class TorrentClient extends WebTorrent {
|
|||
ipc
|
||||
|
||||
constructor (ipc, storageQuota, serverMode, torrentPath, controller) {
|
||||
let storedSettings = {}
|
||||
|
||||
try {
|
||||
storedSettings = JSON.parse(localStorage.getItem('settings')) || {}
|
||||
} catch (error) {}
|
||||
|
||||
const settings = { ...defaults, ...storedSettings }
|
||||
debug('Initializing TorrentClient with settings: ' + JSON.stringify(settings))
|
||||
super({
|
||||
|
|
@ -69,6 +69,7 @@ export default class TorrentClient extends WebTorrent {
|
|||
this.torrentPath = torrentPath
|
||||
this._ready = new Promise(resolve => {
|
||||
ipc.on('port', ({ ports }) => {
|
||||
if (this.message) return
|
||||
this.message = ports[0].postMessage.bind(ports[0])
|
||||
ports[0].onmessage = ({ data }) => {
|
||||
debug(`Received IPC message ${data.type}: ${data.data}`)
|
||||
|
|
@ -324,20 +325,26 @@ export default class TorrentClient extends WebTorrent {
|
|||
this.dispatchError('File Too Big! This File Exceeds The Selected Drive\'s Available Space. Change Download Location In Torrent Settings To A Drive With More Space And Restart The App!')
|
||||
}
|
||||
this.current = found
|
||||
if (data.data.external && this.player) {
|
||||
this.playerProcess = spawn(this.player, ['' + new URL('http://localhost:' + this.server.address().port + found.streamURL)])
|
||||
this.playerProcess.stdout.on('data', () => {})
|
||||
const startTime = Date.now()
|
||||
this.playerProcess.once('close', () => {
|
||||
this.playerProcess = null
|
||||
const seconds = (Date.now() - startTime) / 1000
|
||||
this.dispatch('externalWatched', seconds)
|
||||
})
|
||||
} else {
|
||||
this.parser = new Parser(this, found)
|
||||
this.findSubtitleFiles(found)
|
||||
this.findFontFiles(found)
|
||||
if (data.data.external) {
|
||||
if (this.player) {
|
||||
this.playerProcess = spawn(this.player, ['' + new URL('http://localhost:' + this.server.address().port + found.streamURL)])
|
||||
this.playerProcess.stdout.on('data', () => {})
|
||||
const startTime = Date.now()
|
||||
this.playerProcess.once('close', () => {
|
||||
this.playerProcess = null
|
||||
const seconds = (Date.now() - startTime) / 1000
|
||||
this.dispatch('externalWatched', seconds)
|
||||
})
|
||||
return
|
||||
}
|
||||
if (SUPPORTS.isAndroid) {
|
||||
this.dispatch('open', `intent://localhost:${this.server.address().port}${found.streamURL}#Intent;type=video/any;scheme=http;end;`)
|
||||
return
|
||||
}
|
||||
}
|
||||
this.parser = new Parser(this, found)
|
||||
this.findSubtitleFiles(found)
|
||||
this.findFontFiles(found)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@
|
|||
"name": "common",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fontsource-variable/material-symbols-outlined": "^5.0.24",
|
||||
"@fontsource-variable/nunito": "^5.0.18",
|
||||
"@fontsource/roboto": "^5.0.12",
|
||||
"@thaunknown/ani-resourced": "^1.0.3",
|
||||
"anitomyscript": "github:ThaUnknown/anitomyscript#42290c4b3f256893be08a4e89051f448ff5e9d00",
|
||||
"bottleneck": "^2.19.5",
|
||||
|
|
@ -12,16 +10,18 @@
|
|||
"comlink": "^4.4.1",
|
||||
"jassub": "^1.7.17",
|
||||
"js-levenshtein": "^1.1.6",
|
||||
"lucide-svelte": "^0.429.0",
|
||||
"p2pt": "github:ThaUnknown/p2pt#modernise",
|
||||
"perfect-seekbar": "^1.1.0",
|
||||
"quartermoon": "^1.2.3",
|
||||
"simple-font-select": "^1.0.1",
|
||||
"simple-store-svelte": "^1.0.6",
|
||||
"svelte": "^4.2.12",
|
||||
"svelte-keybinds": "^1.0.6",
|
||||
"svelte-keybinds": "^1.0.9",
|
||||
"svelte-loader": "^3.1.9",
|
||||
"svelte-miniplayer": "^1.0.5",
|
||||
"svelte-persisted-store": "^0.11.0",
|
||||
"svelte-radix": "^1.1.0",
|
||||
"svelte-sonner": "^0.3.19",
|
||||
"video-deband": "^1.0.5",
|
||||
"webpack-merge": "^5.10.0"
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
import { SUPPORTS } from '@/modules/support.js'
|
||||
import 'rvfc-polyfill'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
import { ArrowDown, ArrowUp, Captions, Cast, CircleHelp, Contrast, FastForward, Keyboard, List, ListMusic, ListVideo, Maximize, Minimize, Pause, PictureInPicture, PictureInPicture2, Play, Proportions, RefreshCcw, Rewind, RotateCcw, RotateCw, ScreenShare, SkipBack, SkipForward, Users, Volume1, Volume2, VolumeX } from 'lucide-svelte'
|
||||
|
||||
const emit = createEventDispatcher()
|
||||
|
||||
|
|
@ -348,10 +349,10 @@
|
|||
video.currentTime = targetTime
|
||||
}
|
||||
function forward () {
|
||||
seek(2)
|
||||
seek(settings.value.playerSeek)
|
||||
}
|
||||
function rewind () {
|
||||
seek(-2)
|
||||
seek(-settings.value.playerSeek)
|
||||
}
|
||||
function selectAudio (id) {
|
||||
if (id !== undefined) {
|
||||
|
|
@ -466,11 +467,13 @@
|
|||
KeyX: {
|
||||
fn: () => screenshot(),
|
||||
id: 'screenshot_monitor',
|
||||
icon: ScreenShare,
|
||||
type: 'icon',
|
||||
desc: 'Save Screenshot to Clipboard'
|
||||
},
|
||||
KeyI: {
|
||||
fn: () => toggleStats(),
|
||||
icon: List,
|
||||
id: 'list',
|
||||
type: 'icon',
|
||||
desc: 'Toggle Stats'
|
||||
|
|
@ -478,24 +481,28 @@
|
|||
Backquote: {
|
||||
fn: () => (showKeybinds = !showKeybinds),
|
||||
id: 'help_outline',
|
||||
icon: CircleHelp,
|
||||
type: 'icon',
|
||||
desc: 'Toggle Keybinds'
|
||||
},
|
||||
Space: {
|
||||
fn: () => playPause(),
|
||||
id: 'play_arrow',
|
||||
play: Play,
|
||||
type: 'icon',
|
||||
desc: 'Play/Pause'
|
||||
},
|
||||
KeyN: {
|
||||
fn: () => playNext(),
|
||||
id: 'skip_next',
|
||||
icon: SkipForward,
|
||||
type: 'icon',
|
||||
desc: 'Next Episode'
|
||||
},
|
||||
KeyB: {
|
||||
fn: () => playLast(),
|
||||
id: 'skip_previous',
|
||||
icon: SkipBack,
|
||||
type: 'icon',
|
||||
desc: 'Previous Episode'
|
||||
},
|
||||
|
|
@ -504,24 +511,28 @@
|
|||
$settings.playerDeband = !$settings.playerDeband
|
||||
},
|
||||
id: 'deblur',
|
||||
icon: Contrast,
|
||||
type: 'icon',
|
||||
desc: 'Toggle Video Debanding'
|
||||
},
|
||||
KeyM: {
|
||||
fn: () => (muted = !muted),
|
||||
id: 'volume_off',
|
||||
icon: VolumeX,
|
||||
type: 'icon',
|
||||
desc: 'Toggle Mute'
|
||||
},
|
||||
KeyP: {
|
||||
fn: () => togglePopout(),
|
||||
id: 'picture_in_picture',
|
||||
icon: PictureInPicture2,
|
||||
type: 'icon',
|
||||
desc: 'Toggle Picture in Picture'
|
||||
},
|
||||
KeyF: {
|
||||
fn: () => toggleFullscreen(),
|
||||
id: 'fullscreen',
|
||||
icon: Maximize,
|
||||
type: 'icon',
|
||||
desc: 'Toggle Fullscreen'
|
||||
},
|
||||
|
|
@ -533,18 +544,21 @@
|
|||
KeyW: {
|
||||
fn: () => { fitWidth = !fitWidth },
|
||||
id: 'fit_width',
|
||||
icon: Proportions,
|
||||
type: 'icon',
|
||||
desc: 'Toggle Video Cover'
|
||||
},
|
||||
KeyD: {
|
||||
fn: () => toggleCast(),
|
||||
id: 'cast',
|
||||
icon: Cast,
|
||||
type: 'icon',
|
||||
desc: 'Toggle Cast [broken]'
|
||||
},
|
||||
KeyC: {
|
||||
fn: () => cycleSubtitles(),
|
||||
id: 'subtitles',
|
||||
icon: Captions,
|
||||
type: 'icon',
|
||||
desc: 'Cycle Subtitles'
|
||||
},
|
||||
|
|
@ -554,8 +568,10 @@
|
|||
e.preventDefault()
|
||||
rewind()
|
||||
},
|
||||
id: '-2',
|
||||
desc: 'Rewind 2s'
|
||||
id: 'fast_rewind',
|
||||
icon: Rewind,
|
||||
type: 'icon',
|
||||
desc: 'Rewind'
|
||||
},
|
||||
ArrowRight: {
|
||||
fn: e => {
|
||||
|
|
@ -563,8 +579,10 @@
|
|||
e.preventDefault()
|
||||
forward()
|
||||
},
|
||||
id: '+2',
|
||||
desc: 'Seek 2s'
|
||||
id: 'fast_forward',
|
||||
icon: FastForward,
|
||||
type: 'icon',
|
||||
desc: 'Seek'
|
||||
},
|
||||
ArrowUp: {
|
||||
fn: e => {
|
||||
|
|
@ -573,6 +591,7 @@
|
|||
volume = Math.min(1, volume + 0.05)
|
||||
},
|
||||
id: 'volume_up',
|
||||
icon: Volume2,
|
||||
type: 'icon',
|
||||
desc: 'Volume Up'
|
||||
},
|
||||
|
|
@ -583,23 +602,27 @@
|
|||
volume = Math.max(0, volume - 0.05)
|
||||
},
|
||||
id: 'volume_down',
|
||||
icon: Volume1,
|
||||
type: 'icon',
|
||||
desc: 'Volume Down'
|
||||
},
|
||||
BracketLeft: {
|
||||
fn: () => { playbackRate = video.defaultPlaybackRate -= 0.1 },
|
||||
id: 'history',
|
||||
icon: RotateCcw,
|
||||
type: 'icon',
|
||||
desc: 'Decrease Playback Rate'
|
||||
},
|
||||
BracketRight: {
|
||||
fn: () => { playbackRate = video.defaultPlaybackRate += 0.1 },
|
||||
id: 'update',
|
||||
icon: RotateCw,
|
||||
type: 'icon',
|
||||
desc: 'Increase Playback Rate'
|
||||
},
|
||||
Backslash: {
|
||||
fn: () => { playbackRate = video.defaultPlaybackRate = 1 },
|
||||
icon: RefreshCcw,
|
||||
id: 'schedule',
|
||||
type: 'icon',
|
||||
desc: 'Reset Playback Rate'
|
||||
|
|
@ -784,6 +807,7 @@
|
|||
}
|
||||
|
||||
let currentSkippable = null
|
||||
$: currentSkippable && settings.value.playerAutoSkip && skip()
|
||||
function checkSkippableChapters () {
|
||||
const current = findChapter(currentTime)
|
||||
if (current) {
|
||||
|
|
@ -1027,7 +1051,15 @@
|
|||
{#if showKeybinds && !miniplayer}
|
||||
<div class='position-absolute bg-tp w-full h-full z-50 font-size-12 p-20 d-flex align-items-center justify-content-center pointer' on:pointerup|self={() => (showKeybinds = false)} tabindex='-1' role='button'>
|
||||
<Keybinds let:prop={item} autosave={true} clickable={true}>
|
||||
<div class:material-symbols-outlined={item?.type} class='bind' title={item?.desc} style='pointer-events: all !important;'>{item?.id || ''}</div>
|
||||
{#if item?.type}
|
||||
<div class='bind icon' title={item?.desc} style='pointer-events: all !important;'>
|
||||
{#if item?.icon}
|
||||
<svelte:component this={item.icon} size='2rem' />
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class='bind font-weight-normal' title={item?.desc} style='pointer-events: all !important;'>{item?.id || ''}</div>
|
||||
{/if}
|
||||
</Keybinds>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -1095,11 +1127,11 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class='d-flex col-4 justify-content-center'>
|
||||
<span class='material-symbols-outlined'> people </span>
|
||||
<span class='icon'><Users size='3rem' class='pt-5' strokeWidth={3} /> </span>
|
||||
<span class='stats'>{torrent.peers || 0}</span>
|
||||
<span class='material-symbols-outlined'> arrow_downward </span>
|
||||
<span class='icon'><ArrowDown size='3rem' /></span>
|
||||
<span class='stats'>{fastPrettyBytes(torrent.down)}/s</span>
|
||||
<span class='material-symbols-outlined'> arrow_upward </span>
|
||||
<span class='icon'><ArrowUp size='3rem' /></span>
|
||||
<span class='stats'>{fastPrettyBytes(torrent.up)}/s</span>
|
||||
</div>
|
||||
<div class='col-4' />
|
||||
|
|
@ -1111,10 +1143,24 @@
|
|||
<div class='w-full h-full position-absolute toggle-immerse d-none' on:dblclick={toggleFullscreen} on:click|self={toggleImmerse} />
|
||||
<div class='w-full h-full position-absolute mobile-focus-target d-none' use:click={() => { page = 'player'; window.dispatchEvent(new Event('overlay-check')) }} />
|
||||
<!-- eslint-disable-next-line svelte/valid-compile -->
|
||||
<span class='material-symbols-outlined ctrl h-full align-items-center justify-content-end w-150 mw-full mr-auto' on:click={rewind}> fast_rewind </span>
|
||||
<span class='material-symbols-outlined ctrl' data-name='playPause' use:click={playPause}> {ended ? 'replay' : paused ? 'play_arrow' : 'pause'} </span>
|
||||
<span class='icon ctrl h-full align-items-center justify-content-end w-150 mw-full mr-auto' on:click={rewind}>
|
||||
<Rewind size='3rem' />
|
||||
</span>
|
||||
<span class='icon ctrl' data-name='playPause' use:click={playPause}>
|
||||
{#if ended}
|
||||
<RotateCw size='3rem' />
|
||||
{:else}
|
||||
{#if paused}
|
||||
<Play size='3rem' fill='white' />
|
||||
{:else}
|
||||
<Pause size='3rem' fill='white' />
|
||||
{/if}
|
||||
{/if}
|
||||
</span>
|
||||
<!-- eslint-disable-next-line svelte/valid-compile -->
|
||||
<span class='material-symbols-outlined ctrl h-full align-items-center w-150 mw-full ml-auto' on:click={forward}> fast_forward </span>
|
||||
<span class='icon ctrl h-full align-items-center w-150 mw-full ml-auto' on:click={forward}>
|
||||
<FastForward size='3rem' />
|
||||
</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 z-30' use:click={skip}>
|
||||
|
|
@ -1137,26 +1183,47 @@
|
|||
/>
|
||||
</div>
|
||||
<div class='d-flex'>
|
||||
<span class='material-symbols-outlined ctrl' title='Play/Pause [Space]' data-name='playPause' use:click={playPause}> {ended ? 'replay' : paused ? 'play_arrow' : 'pause'} </span>
|
||||
<span class='icon ctrl m-5' title='Play/Pause [Space]' data-name='playPause' use:click={playPause}>
|
||||
{#if ended}
|
||||
<RotateCw size='2rem' />
|
||||
{:else}
|
||||
{#if paused}
|
||||
<Play size='2rem' fill='white' />
|
||||
{:else}
|
||||
<Pause size='2rem' fill='white' />
|
||||
{/if}
|
||||
{/if}</span>
|
||||
{#if hasLast}
|
||||
<span class='material-symbols-outlined ctrl' title='Last [B]' use:click={playLast}> skip_previous </span>
|
||||
<span class='icon ctrl m-5' title='Last [B]' use:click={playLast}>
|
||||
<SkipBack size='2rem' fill='white' />
|
||||
</span>
|
||||
{/if}
|
||||
{#if hasNext}
|
||||
<span class='material-symbols-outlined ctrl' title='Next [N]' use:click={playNext}> skip_next </span>
|
||||
<span class='icon ctrl m-5' title='Next [N]' use:click={playNext}>
|
||||
<SkipForward size='2rem' fill='white' />
|
||||
</span>
|
||||
{/if}
|
||||
<div class='d-flex w-auto volume'>
|
||||
<span class='material-symbols-outlined ctrl' title='Mute [M]' data-name='toggleMute' use:click={toggleMute}> {muted ? 'volume_off' : 'volume_up'} </span>
|
||||
<span class='icon ctrl m-5' title='Mute [M]' data-name='toggleMute' use:click={toggleMute}>
|
||||
{#if muted}
|
||||
<VolumeX size='2rem' fill='white' />
|
||||
{:else}
|
||||
<Volume2 size='2rem' fill='white' />
|
||||
{/if}
|
||||
</span>
|
||||
<input class='ctrl h-full custom-range' type='range' min='0' max='1' step='any' data-name='setVolume' bind:value={volume} />
|
||||
</div>
|
||||
<div class='ts' class:mr-auto={playbackRate === 1}>{toTS(targetTime, safeduration > 3600 ? 2 : 3)} / {toTS(safeduration - targetTime, safeduration > 3600 ? 2 : 3)}</div>
|
||||
{#if playbackRate !== 1}
|
||||
<div class='ts mr-auto'>x{playbackRate.toFixed(1)}</div>
|
||||
{/if}
|
||||
<span class='material-symbols-outlined ctrl keybinds' title='Keybinds [`]' use:click={() => (showKeybinds = true)}> keyboard </span>
|
||||
<span class='icon ctrl mr-5 d-flex align-items-center keybinds' title='Keybinds [`]' use:click={() => (showKeybinds = true)}>
|
||||
<Keyboard size='2.5rem' strokeWidth={2.5} />
|
||||
</span>
|
||||
{#if 'audioTracks' in HTMLVideoElement.prototype && video?.audioTracks?.length > 1}
|
||||
<div class='dropdown dropup with-arrow' use:click={toggleDropdown}>
|
||||
<span class='material-symbols-outlined ctrl' title='Audio Tracks'>
|
||||
queue_music
|
||||
<span class='icon ctrl mr-5 d-flex align-items-center' title='Audio Tracks'>
|
||||
<ListMusic size='2.5rem' strokeWidth={2.5} />
|
||||
</span>
|
||||
<div class='dropdown-menu dropdown-menu-left ctrl custom-radio p-10 pb-5 text-capitalize'>
|
||||
{#each video.audioTracks as track}
|
||||
|
|
@ -1170,8 +1237,8 @@
|
|||
{/if}
|
||||
{#if 'videoTracks' in HTMLVideoElement.prototype && video?.videoTracks?.length > 1}
|
||||
<div class='dropdown dropup with-arrow'>
|
||||
<span class='material-symbols-outlined ctrl' title='Video Tracks'>
|
||||
playlist_play
|
||||
<span class='icon ctrl mr-5 d-flex align-items-center' title='Video Tracks'>
|
||||
<ListVideo size='2.5rem' strokeWidth={2.5} />
|
||||
</span>
|
||||
<div class='dropdown-menu dropdown-menu-left ctrl custom-radio p-10 pb-5 text-capitalize'>
|
||||
{#each video.videoTracks as track}
|
||||
|
|
@ -1185,8 +1252,8 @@
|
|||
{/if}
|
||||
{#if subHeaders?.length}
|
||||
<div class='subtitles dropdown dropup with-arrow' use:click={toggleDropdown}>
|
||||
<span class='material-symbols-outlined ctrl' title='Subtitles [C]'>
|
||||
subtitles
|
||||
<span class='icon ctrl mr-5 d-flex align-items-center' title='Subtitles [C]'>
|
||||
<Captions size='2.5rem'strokeWidth={2.5} />
|
||||
</span>
|
||||
<div class='dropdown-menu dropdown-menu-right ctrl custom-radio p-10 pb-5 text-capitalize'>
|
||||
<input name='subtitle-radio-set' type='radio' id='subtitle-off-radio' value='off' checked={subHeaders && subs?.current === -1} />
|
||||
|
|
@ -1204,17 +1271,29 @@
|
|||
</div>
|
||||
{/if}
|
||||
{#if 'PresentationRequest' in window && canCast && current}
|
||||
<span class='material-symbols-outlined ctrl' title='Cast Video [D]' data-name='toggleCast' use:click={toggleCast}>
|
||||
{presentationConnection ? 'cast_connected' : 'cast'}
|
||||
<span class='icon ctrl mr-5 d-flex align-items-center' title='Cast Video [D]' data-name='toggleCast' use:click={toggleCast}>
|
||||
{#if presentationConnection}
|
||||
<Cast size='2.5rem' fill='white' strokeWidth={0} />
|
||||
{:else}
|
||||
<Cast size='2.5rem'strokeWidth={2.5} />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
{#if 'pictureInPictureEnabled' in document}
|
||||
<span class='material-symbols-outlined ctrl' title='Popout Window [P]' data-name='togglePopout' use:click={togglePopout}>
|
||||
{pip ? 'featured_video' : 'picture_in_picture'}
|
||||
<span class='icon ctrl mr-5 d-flex align-items-center' title='Popout Window [P]' data-name='togglePopout' use:click={togglePopout}>
|
||||
{#if pip}
|
||||
<PictureInPicture size='2.5rem' strokeWidth={2.5} />
|
||||
{:else}
|
||||
<PictureInPicture2 size='2.5rem'strokeWidth={2.5} />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
<span class='material-symbols-outlined ctrl' title='Fullscreen [F]' data-name='toggleFullscreen' use:click={toggleFullscreen}>
|
||||
{isFullscreen ? 'fullscreen_exit' : 'fullscreen'}
|
||||
<span class='icon ctrl mr-5 d-flex align-items-center' title='Fullscreen [F]' data-name='toggleFullscreen' use:click={toggleFullscreen}>
|
||||
{#if isFullscreen}
|
||||
<Minimize size='2.5rem' strokeWidth={2.5} />
|
||||
{:else}
|
||||
<Maximize size='2.5rem' strokeWidth={2.5} />
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1377,11 +1456,10 @@
|
|||
height: 1px !important;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
.icon {
|
||||
font-size: 2.8rem;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
font-variation-settings: 'FILL' 1, 'wght' 300, 'GRAD' 100, 'opsz' 64;
|
||||
}
|
||||
|
||||
.immersed {
|
||||
|
|
@ -1413,9 +1491,6 @@
|
|||
transition: 0.5s opacity ease 0.2s;
|
||||
filter: drop-shadow(0 0 8px #000);
|
||||
}
|
||||
.disabled {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
.buffering .middle .bufferingDisplay {
|
||||
opacity: 1 !important;
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@
|
|||
|
||||
<div class='bg-dark h-full w-full overflow-y-scroll d-flex flex-wrap flex-row root overflow-x-hidden justify-content-center align-content-start' use:smoothScroll bind:this={container} on:scroll={infiniteScroll}>
|
||||
<Search bind:search={$search} on:input={update} />
|
||||
<div class='w-full d-grid d-md-flex flex-wrap flex-row px-md-50 px-20 justify-content-center align-content-start'>
|
||||
<div class='w-full d-grid d-md-flex flex-wrap flex-row px-md-50 justify-content-center align-content-start'>
|
||||
{#key $key}
|
||||
{#each $items as card}
|
||||
<Card {card} variables={{...$search}} />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<script>
|
||||
import { click } from '@/modules/click.js'
|
||||
import { sections } from '@/modules/sections.js'
|
||||
import { SUPPORTS } from '@/modules/support.js'
|
||||
import { ArrowDown, ArrowDownUp, ArrowUp, Trash2 } from 'lucide-svelte'
|
||||
|
||||
const allowedHomeSections = sections.map(({ title }) => title)
|
||||
export let homeSections
|
||||
|
|
@ -15,11 +17,16 @@
|
|||
|
||||
$: {
|
||||
if (draggingItemIndex != null && hoveredItemIndex != null && draggingItemIndex !== hoveredItemIndex) {
|
||||
[homeSections[draggingItemIndex], homeSections[hoveredItemIndex]] = [homeSections[hoveredItemIndex], homeSections[draggingItemIndex]]
|
||||
swapItem(draggingItemIndex, hoveredItemIndex)
|
||||
|
||||
draggingItemIndex = hoveredItemIndex
|
||||
}
|
||||
}
|
||||
|
||||
function swapItem (a, b) {
|
||||
b = Math.min(homeSections.length - 1, Math.max(0, b))
|
||||
;[homeSections[a], homeSections[b]] = [homeSections[b], homeSections[a]]
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if mouseYCoordinate}
|
||||
|
|
@ -27,7 +34,7 @@
|
|||
class='input-group mb-10 ghost w-full'
|
||||
style='top: {mouseYCoordinate + distanceTopGrabbedVsPointer}px;'>
|
||||
<div class='input-group-prepend'>
|
||||
<span class='input-group-text d-flex justify-content-center px-5 material-symbols-outlined font-size-20'>swap_vert</span>
|
||||
<span class='input-group-text d-flex align-items-center px-5'><ArrowDownUp size='1.8rem' /></span>
|
||||
</div>
|
||||
<select class='form-control' value={draggingItem}>
|
||||
{#each allowedHomeSections as section}
|
||||
|
|
@ -35,7 +42,7 @@
|
|||
{/each}
|
||||
</select>
|
||||
<div class='input-group-append'>
|
||||
<button type='button' class='btn btn-danger input-group-append px-5 material-symbols-outlined font-size-20'>delete</button>
|
||||
<button type='button' class='btn btn-danger btn-square input-group-append px-5 d-flex align-items-center'><Trash2 size='1.8rem' /></button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -59,16 +66,25 @@
|
|||
draggingItem = null
|
||||
hoveredItemIndex = null
|
||||
}}>
|
||||
<div class='input-group-prepend grab'>
|
||||
<span class='input-group-text d-flex justify-content-center px-5 material-symbols-outlined font-size-20'>swap_vert</span>
|
||||
</div>
|
||||
{#if !SUPPORTS.isAndroid}
|
||||
<div class='input-group-prepend grab'>
|
||||
<span class='input-group-text d-flex align-items-center px-5'><ArrowDownUp size='1.8rem' /></span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class='input-group-prepend'>
|
||||
<button use:click={() => swapItem(index, index - 1)} class='input-group-text d-flex align-items-center px-5 pointer'><ArrowUp size='1.8rem' /></button>
|
||||
</div>
|
||||
<div class='input-group-prepend'>
|
||||
<button use:click={() => swapItem(index, index + 1)} class='input-group-text d-flex align-items-center px-5 pointer'><ArrowDown size='1.8rem' /></button>
|
||||
</div>
|
||||
{/if}
|
||||
<select class='form-control bg-dark w-300 mw-full' bind:value={homeSections[index]}>
|
||||
{#each allowedHomeSections as section}
|
||||
<option>{section}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<div class='input-group-append'>
|
||||
<button type='button' use:click={() => { homeSections.splice(index, 1); homeSections = homeSections }} class='btn btn-danger input-group-append px-5 material-symbols-outlined font-size-20'>delete</button>
|
||||
<button type='button' use:click={() => { homeSections.splice(index, 1); homeSections = homeSections }} class='btn btn-danger btn-square input-group-append px-5 d-flex align-items-center'><Trash2 size='1.8rem' /></button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import IPC from '@/modules/ipc.js'
|
||||
import SettingCard from './SettingCard.svelte'
|
||||
import { SUPPORTS } from '@/modules/support.js'
|
||||
import { Trash2 } from 'lucide-svelte'
|
||||
import AudioLabel from '@/views/ViewAnime/AudioLabel.svelte'
|
||||
import Helper from "@/modules/helper.js"
|
||||
function updateAngle () {
|
||||
|
|
@ -99,7 +100,7 @@
|
|||
<option value='Judas [Small Size]'>{settings.toshoURL + 'rss2?qx=1&q="[Judas] "'}</option>
|
||||
</datalist>
|
||||
<div class='input-group-append'>
|
||||
<button type='button' use:click={() => { settings.rssFeedsNew.splice(i, 1); settings.rssFeedsNew = settings.rssFeedsNew }} class='btn btn-danger input-group-append px-5 material-symbols-outlined font-size-20'>delete</button>
|
||||
<button type='button' use:click={() => { settings.rssFeedsNew.splice(i, 1); settings.rssFeedsNew = settings.rssFeedsNew }} class='btn btn-danger btn-square input-group-append px-5 d-flex align-items-center'><Trash2 size='1.8rem' /></button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import { SUPPORTS } from '@/modules/support.js'
|
||||
import { click } from '@/modules/click.js'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
import { Trash2 } from 'lucide-svelte'
|
||||
export let settings
|
||||
|
||||
async function changeFont ({ detail }) {
|
||||
|
|
@ -39,7 +40,7 @@
|
|||
<div class='input-group w-400 mw-full'>
|
||||
<FontSelect class='form-control bg-dark w-300 mw-full' on:change={changeFont} value={settings.font?.name} />
|
||||
<div class='input-group-append'>
|
||||
<button type='button' class='btn btn-danger btn-square px-5 material-symbols-outlined font-size-20' use:click={() => removeFont()}>delete</button>
|
||||
<button type='button' use:click={() => removeFont()} class='btn btn-danger btn-square input-group-append px-5 d-flex align-items-center'><Trash2 size='1.8rem' /></button>
|
||||
</div>
|
||||
</div>
|
||||
</SettingCard>
|
||||
|
|
@ -92,6 +93,7 @@
|
|||
<option value='slo'>Slovak</option>
|
||||
<option value='swe'>Swedish</option>
|
||||
<option value='ara'>Arabic</option>
|
||||
<option value='idn'>Indonesian</option>
|
||||
</select>
|
||||
</SettingCard>
|
||||
<SettingCard title='Preferred Audio Language' description="What audio language to automatically select when a video is loaded if it exists. This won't find torrents with this language automatically. If not found defaults to Japanese.">
|
||||
|
|
@ -118,6 +120,7 @@
|
|||
<option value='slo'>Slovak</option>
|
||||
<option value='swe'>Swedish</option>
|
||||
<option value='ara'>Arabic</option>
|
||||
<option value='idn'>Indonesian</option>
|
||||
</select>
|
||||
</SettingCard>
|
||||
|
||||
|
|
@ -146,15 +149,29 @@
|
|||
<label for='player-deband'>{settings.playerDeband ? 'On' : 'Off'}</label>
|
||||
</div>
|
||||
</SettingCard>
|
||||
|
||||
{#if SUPPORTS.externalPlayer}
|
||||
<h4 class='mb-10 font-weight-bold'>External Player Settings</h4>
|
||||
<SettingCard title='Enable External Player' description='Tells Miru to open a custom user-defined video player to play video, instead of using the built-in one.'>
|
||||
<div class='custom-switch'>
|
||||
<input type='checkbox' id='player-external-enabled' bind:checked={settings.enableExternal} />
|
||||
<label for='player-external-enabled'>{settings.enableExternal ? 'On' : 'Off'}</label>
|
||||
<SettingCard title='Seek Duration' description='Seconds to skip forward or backward when using the seek buttons or keyboard shortcuts. Higher values might negatively impact buffering speeds.'>
|
||||
<div class='input-group w-100 mw-full'>
|
||||
<input type='number' inputmode='numeric' pattern={'[0-9]*'} bind:value={settings.playerSeek} min='1' max='50' class='form-control text-right bg-dark' />
|
||||
<div class='input-group-append'>
|
||||
<span class='input-group-text bg-dark'>sec</span>
|
||||
</div>
|
||||
</SettingCard>
|
||||
</div>
|
||||
</SettingCard>
|
||||
<SettingCard title='Auto-Skip Intro/Outro' description='Attempt to automatically skip intro and outro. This WILL sometimes skip incorrect chapters, as some of the chapter data is community sourced.'>
|
||||
<div class='custom-switch'>
|
||||
<input type='checkbox' id='player-skip' bind:checked={settings.playerSkip} />
|
||||
<label for='player-skip'>{settings.playerSkip ? 'On' : 'Off'}</label>
|
||||
</div>
|
||||
</SettingCard>
|
||||
|
||||
<h4 class='mb-10 font-weight-bold'>External Player Settings</h4>
|
||||
<SettingCard title='Enable External Player' description='Tells Miru to open a custom user-picked external video player to play video, instead of using the built-in one.'>
|
||||
<div class='custom-switch'>
|
||||
<input type='checkbox' id='player-external-enabled' bind:checked={settings.enableExternal} />
|
||||
<label for='player-external-enabled'>{settings.enableExternal ? 'On' : 'Off'}</label>
|
||||
</div>
|
||||
</SettingCard>
|
||||
{#if SUPPORTS.externalPlayer}
|
||||
<SettingCard title='External Video Player' description='Executable for an external video player. Make sure the player supports HTTP sources.'>
|
||||
<div
|
||||
class='input-group w-300 mw-full'>
|
||||
|
|
|
|||
|
|
@ -37,27 +37,28 @@
|
|||
import { profileView } from '@/components/Profiles.svelte'
|
||||
import smoothScroll from '@/modules/scroll.js'
|
||||
import Helper from '@/modules/helper.js'
|
||||
import { AppWindow, Heart, LogIn, Logs, Play, Rss, Settings } from 'lucide-svelte'
|
||||
|
||||
const groups = {
|
||||
player: {
|
||||
name: 'Player',
|
||||
icon: 'play_arrow'
|
||||
icon: Play
|
||||
},
|
||||
torrent: {
|
||||
name: 'Torrent',
|
||||
icon: 'rss_feed'
|
||||
icon: Rss
|
||||
},
|
||||
interface: {
|
||||
name: 'Interface',
|
||||
icon: 'settings'
|
||||
icon: AppWindow
|
||||
},
|
||||
app: {
|
||||
name: 'App',
|
||||
icon: 'info'
|
||||
icon: Settings
|
||||
},
|
||||
changelog: {
|
||||
name: 'Changelog',
|
||||
icon: 'description'
|
||||
icon: Logs
|
||||
}
|
||||
}
|
||||
function pathListener (data) {
|
||||
|
|
@ -86,28 +87,28 @@
|
|||
<div class='px-20 py-15 font-size-24 font-weight-semi-bold'>Settings</div>
|
||||
{#each Object.values(groups) as group}
|
||||
<TabLabel>
|
||||
<div class='px-20 py-10 d-flex'>
|
||||
<span class='material-symbols-outlined font-size-24 pr-10 d-inline-flex justify-content-center align-items-center'>{group.icon}</span>
|
||||
<div class='font-size-16'>{group.name}</div>
|
||||
<div class='px-20 py-10 d-flex align-items-center'>
|
||||
<svelte:component this={group.icon} class='pr-10 d-inline-flex' size='3.1rem' fill={group.icon === Play ? 'currentColor' : 'transparent'} />
|
||||
<div class='font-size-16 line-height-normal'>{group.name}</div>
|
||||
</div>
|
||||
</TabLabel>
|
||||
{/each}
|
||||
<div class='pointer my-5 rounded' tabindex='0' role='button' use:click={() => IPC.emit('open', 'https://github.com/sponsors/ThaUnknown/')}>
|
||||
<div class='px-20 py-10 d-flex'>
|
||||
<span class='material-symbols-outlined font-size-24 pr-10 d-inline-flex justify-content-center align-items-center'>favorite</span>
|
||||
<div class='font-size-16'>Donate</div>
|
||||
<div class='px-20 py-10 d-flex align-items-center'>
|
||||
<Heart class='pr-10 d-inline-flex' size='3.1rem' />
|
||||
<div class='font-size-16 line-height-normal'>Donate</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='pointer my-5 rounded' use:click={loginButton}>
|
||||
<div class='px-20 py-10 d-flex'>
|
||||
<div class='px-20 py-10 d-flex align-items-center'>
|
||||
{#if Helper.getUser()}
|
||||
<span class='material-symbols-outlined rounded mr-10'>
|
||||
<span class='rounded mr-10'>
|
||||
<img src={Helper.getUserAvatar()} class='h-30 rounded' alt='logo' />
|
||||
</span>
|
||||
<div class='font-size-16 login-image-text'>Profiles</div>
|
||||
{:else}
|
||||
<span class='material-symbols-outlined font-size-24 pr-10 d-inline-flex justify-content-center align-items-center'>login</span>
|
||||
<div class='font-size-16'>Login</div>
|
||||
<LogIn class='pr-10 d-inline-flex' size='3.1rem' />
|
||||
<div class='font-size-16 line-height-normal'>Login</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -222,10 +223,6 @@
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.h-30 {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
.settings :global(input:not(:focus):invalid) {
|
||||
box-shadow: 0 0 0 0.2rem var(--danger-color) !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
import IPC from '@/modules/ipc.js'
|
||||
import SettingCard from './SettingCard.svelte'
|
||||
import { SUPPORTS } from '@/modules/support.js'
|
||||
import { Trash2 } from 'lucide-svelte'
|
||||
export let settings
|
||||
|
||||
function handleFolder () {
|
||||
|
|
@ -74,7 +75,7 @@
|
|||
<div class='input-group-prepend overflow-hidden w-full'>
|
||||
<span class='input-group-text bg-dark w-full'>{extension}</span>
|
||||
</div>
|
||||
<button type='button' class='btn btn-danger btn-square px-5 material-symbols-outlined font-size-20' use:click={() => removeExtension(i)}>delete</button>
|
||||
<button type='button' use:click={() => removeExtension(i)} class='btn btn-danger btn-square input-group-append px-5 d-flex align-items-center'><Trash2 size='1.8rem' /></button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -140,24 +141,24 @@
|
|||
{/if}
|
||||
|
||||
<h4 class='mb-10 font-weight-bold'>Client Settings</h4>
|
||||
{#if SUPPORTS.torrentPath}
|
||||
<SettingCard title='Torrent Download Location' description='Path to the folder used to store torrents. By default this is the TMP folder, which might lose data when your OS tries to reclaim storage.'>
|
||||
<div class='input-group w-300 mw-full'>
|
||||
<div class='input-group-prepend'>
|
||||
<button type='button' use:click={handleFolder} class='btn btn-primary input-group-append'>Select Folder</button>
|
||||
</div>
|
||||
<input type='url' class='form-control bg-dark' readonly value={settings.torrentPathNew} placeholder='/tmp' />
|
||||
<SettingCard title='Torrent Download Location' description='Path to the folder used to store torrents. By default this is the TMP folder, which might lose data when your OS tries to reclaim storage. {SUPPORTS.isAndroid ? "RESTART IS REQUIRED. /sdcard/ is internal storage, not external SD Cards. /storage/AB12-34CD/ is external storage, not internal. Thank you Android!" : ""}'>
|
||||
<div class='input-group w-300 mw-full'>
|
||||
<div class='input-group-prepend'>
|
||||
<button type='button' use:click={handleFolder} class='btn btn-primary input-group-append'>Select Folder</button>
|
||||
</div>
|
||||
</SettingCard>
|
||||
{/if}
|
||||
{#if SUPPORTS.torrentPersist}
|
||||
<SettingCard title='Persist Files' description="Keeps torrents files instead of deleting them after a new torrent is played. This doesn't seed the files, only keeps them on your drive. This will quickly fill up your storage.">
|
||||
<div class='custom-switch'>
|
||||
<input type='checkbox' id='torrent-persist' bind:checked={settings.torrentPersist} />
|
||||
<label for='torrent-persist'>{settings.torrentPersist ? 'On' : 'Off'}</label>
|
||||
</div>
|
||||
</SettingCard>
|
||||
{/if}
|
||||
{#if !SUPPORTS.isAndroid}
|
||||
<input type='url' class='form-control bg-dark' readonly bind:value={settings.torrentPathNew} placeholder='/tmp' />
|
||||
{:else}
|
||||
<input type='text' class='form-control bg-dark' bind:value={settings.torrentPathNew} placeholder='/tmp' />
|
||||
{/if}
|
||||
</div>
|
||||
</SettingCard>
|
||||
<SettingCard title='Persist Files' description="Keeps torrents files instead of deleting them after a new torrent is played. This doesn't seed the files, only keeps them on your drive. This will quickly fill up your storage.">
|
||||
<div class='custom-switch'>
|
||||
<input type='checkbox' id='torrent-persist' bind:checked={settings.torrentPersist} />
|
||||
<label for='torrent-persist'>{settings.torrentPersist ? 'On' : 'Off'}</label>
|
||||
</div>
|
||||
</SettingCard>
|
||||
<SettingCard title='Streamed Download' description="Only downloads the single file that's currently being watched, instead of downloading an entire batch of episodes. Saves bandwidth and reduces strain on the peer swarm.">
|
||||
<div class='custom-switch'>
|
||||
<input type='checkbox' id='torrent-streamed-download' bind:checked={settings.torrentStreamedDownload} />
|
||||
|
|
@ -175,22 +176,18 @@
|
|||
<SettingCard title='Max Number of Connections' description='Number of peers per torrent. Higher values will increase download speeds but might quickly fill up available ports if your ISP limits the maximum allowed number of open connections.'>
|
||||
<input type='number' inputmode='numeric' pattern='[0-9]*' bind:value={settings.maxConns} min='1' max='512' class='form-control text-right bg-dark w-100 mw-full' />
|
||||
</SettingCard>
|
||||
{#if SUPPORTS.torrentPort}
|
||||
<SettingCard title='Torrent Port' description='Port used for Torrent connections. 0 is automatic.'>
|
||||
<input type='number' inputmode='numeric' pattern='[0-9]*' bind:value={settings.torrentPort} min='0' max='65536' class='form-control text-right bg-dark w-150 mw-full' />
|
||||
</SettingCard>
|
||||
{/if}
|
||||
{#if SUPPORTS.dht}
|
||||
<SettingCard title='DHT Port' description='Port used for DHT connections. 0 is automatic.'>
|
||||
<input type='number' inputmode='numeric' pattern='[0-9]*' bind:value={settings.dhtPort} min='0' max='65536' class='form-control text-right bg-dark w-150 mw-full' />
|
||||
</SettingCard>
|
||||
<SettingCard title='Disable DHT' description='Disables Distributed Hash Tables for use in private trackers to improve privacy. Might greatly reduce the amount of discovered peers.'>
|
||||
<div class='custom-switch'>
|
||||
<input type='checkbox' id='torrent-dht' bind:checked={settings.torrentDHT} />
|
||||
<label for='torrent-dht'>{settings.torrentDHT ? 'On' : 'Off'}</label>
|
||||
</div>
|
||||
</SettingCard>
|
||||
{/if}
|
||||
<SettingCard title='Torrent Port' description='Port used for Torrent connections. 0 is automatic.'>
|
||||
<input type='number' inputmode='numeric' pattern='[0-9]*' bind:value={settings.torrentPort} min='0' max='65536' class='form-control text-right bg-dark w-150 mw-full' />
|
||||
</SettingCard>
|
||||
<SettingCard title='DHT Port' description='Port used for DHT connections. 0 is automatic.'>
|
||||
<input type='number' inputmode='numeric' pattern='[0-9]*' bind:value={settings.dhtPort} min='0' max='65536' class='form-control text-right bg-dark w-150 mw-full' />
|
||||
</SettingCard>
|
||||
<SettingCard title='Disable DHT' description='Disables Distributed Hash Tables for use in private trackers to improve privacy. Might greatly reduce the amount of discovered peers.'>
|
||||
<div class='custom-switch'>
|
||||
<input type='checkbox' id='torrent-dht' bind:checked={settings.torrentDHT} />
|
||||
<label for='torrent-dht'>{settings.torrentDHT ? 'On' : 'Off'}</label>
|
||||
</div>
|
||||
</SettingCard>
|
||||
<SettingCard title='Disable PeX' description='Disables Peer Exchange for use in private trackers to improve privacy. Might greatly reduce the amount of discovered peers.'>
|
||||
<div class='custom-switch'>
|
||||
<input type='checkbox' id='torrent-pex' bind:checked={settings.torrentPeX} />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script context='module'>
|
||||
import { click } from '@/modules/click.js'
|
||||
import { fastPrettyBytes, since } from '@/modules/util.js'
|
||||
import { Database, BadgeCheck } from 'lucide-svelte'
|
||||
|
||||
/** @typedef {import('@thaunknown/ani-resourced/sources/types.d.ts').Result} Result */
|
||||
/** @typedef {import('anitomyscript').AnitomyResult} AnitomyResult */
|
||||
|
|
@ -86,13 +87,9 @@
|
|||
<div class='d-flex w-full'>
|
||||
<div class='font-size-22 font-weight-bold text-nowrap'>{result.parseObject?.release_group && result.parseObject.release_group.length < 20 ? result.parseObject.release_group : 'No Group'}</div>
|
||||
{#if result.type === 'batch'}
|
||||
<div class='material-symbols-outlined card-title symbol-bold ml-auto' title='Batch'>
|
||||
database
|
||||
</div>
|
||||
<Database size='2.6rem' class='ml-auto' />
|
||||
{:else if result.verified}
|
||||
<div class='material-symbols-outlined card-title symbol-bold ml-auto' style='color: #53da33' title='Verified'>
|
||||
verified
|
||||
</div>
|
||||
<BadgeCheck size='2.8rem' class='ml-auto' style='color: #53da33' />
|
||||
{/if}
|
||||
</div>
|
||||
<div class='font-size-14 text-muted text-truncate overflow-hidden'>{simplifyFilename(result.parseObject)}</div>
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@
|
|||
import { add } from '@/modules/torrent.js'
|
||||
import TorrentSkeletonCard from './TorrentSkeletonCard.svelte'
|
||||
import { onDestroy } from 'svelte'
|
||||
import { MagnifyingGlass } from 'svelte-radix'
|
||||
|
||||
/** @type {{ media: Media, episode?: number }} */
|
||||
export let search
|
||||
|
|
@ -131,7 +132,7 @@
|
|||
{/await}
|
||||
<div class='input-group mt-20'>
|
||||
<div class='input-group-prepend'>
|
||||
<span class='input-group-text d-flex material-symbols-outlined bg-dark pr-0 font-size-18'>search</span>
|
||||
<MagnifyingGlass size='2.75rem' class='input-group-text bg-dark pr-0' />
|
||||
</div>
|
||||
<input
|
||||
type='search'
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { findInCurrent } from '../Player/MediaHandler.svelte'
|
||||
import { writable } from 'simple-store-svelte'
|
||||
|
||||
const rss = writable(null)
|
||||
export const rss = writable(null)
|
||||
|
||||
export function playAnime (media, episode = 1, force) {
|
||||
episode = Number(episode)
|
||||
|
|
|
|||
|
|
@ -1,161 +0,0 @@
|
|||
<script>
|
||||
import { alToken } from '../../views/Settings.svelte'
|
||||
import { addToast } from '../../components/Toasts.svelte'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
import { anilistClient } from '@/modules/anilist.js'
|
||||
import { getContext } from 'svelte'
|
||||
import { getMediaMaxEp } from '@/modules/anime.js'
|
||||
import { playAnime } from '../RSSView.svelte'
|
||||
import { click } from '@/modules/click.js'
|
||||
export let media = null
|
||||
|
||||
const toggleStatusMap = {
|
||||
CURRENT: 'DROPPED',
|
||||
COMPLETED: 'REPEATING',
|
||||
PAUSED: 'CURRENT',
|
||||
REPEATING: 'CURRENT',
|
||||
DROPPED: 'PLANNING',
|
||||
PLANNING: 'remove'
|
||||
}
|
||||
async function toggleStatus () {
|
||||
if (media.mediaListEntry?.status !== 'PLANNING') {
|
||||
// add
|
||||
await setStatus(toggleStatusMap[media.mediaListEntry?.status] || 'PLANNING')
|
||||
} else {
|
||||
await anilistClient.delete({ id: media.mediaListEntry.id })
|
||||
}
|
||||
update()
|
||||
}
|
||||
function getStatusText (media) {
|
||||
if (media.mediaListEntry) {
|
||||
const { status } = media.mediaListEntry
|
||||
if (status === 'PLANNING') return 'Remove From List'
|
||||
if (media.mediaListEntry?.status in toggleStatusMap) return 'Drop From Watching'
|
||||
}
|
||||
return 'Add To List'
|
||||
}
|
||||
function setStatus (status, other = {}) {
|
||||
const variables = {
|
||||
id: media.id,
|
||||
status,
|
||||
...other
|
||||
}
|
||||
return anilistClient.entry(variables)
|
||||
}
|
||||
async function update () {
|
||||
media = (await anilistClient.searchIDSingle({ id: media.id })).data.Media
|
||||
}
|
||||
async function score (media, score) {
|
||||
const variables = {
|
||||
id: media.id,
|
||||
score: score * 10
|
||||
}
|
||||
await anilistClient.entry(variables)
|
||||
media = (await anilistClient.searchIDSingle({ id: media.id })).data.Media
|
||||
}
|
||||
const trailer = getContext('trailer')
|
||||
function viewTrailer (media) {
|
||||
$trailer = media.trailer.id
|
||||
}
|
||||
function copyToClipboard (text) {
|
||||
navigator.clipboard.writeText(text)
|
||||
addToast({
|
||||
title: 'Copied to clipboard',
|
||||
text: 'Copied share URL to clipboard',
|
||||
type: 'primary',
|
||||
duration: '5000'
|
||||
})
|
||||
}
|
||||
function openInBrowser (url) {
|
||||
IPC.emit('open', url)
|
||||
}
|
||||
function getPlayText (media) {
|
||||
if (media.mediaListEntry) {
|
||||
const { status, progress } = media.mediaListEntry
|
||||
if (progress) {
|
||||
if (status === 'COMPLETED') return 'Rewatch'
|
||||
return 'Continue ' + Math.min(getMediaMaxEp(media, true), progress + 1)
|
||||
}
|
||||
}
|
||||
return 'Play'
|
||||
}
|
||||
async function play (media) {
|
||||
let ep = 1
|
||||
if (media.mediaListEntry) {
|
||||
const { status, progress } = media.mediaListEntry
|
||||
if (progress) {
|
||||
if (status === 'COMPLETED') {
|
||||
await setStatus('REPEATING', { episode: 0 })
|
||||
} else {
|
||||
ep = Math.min(getMediaMaxEp(media, true), progress + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
playAnime(media, ep, true)
|
||||
media = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class='col-md-4 d-flex justify-content-end flex-column'>
|
||||
<div class='d-flex flex-column flex-wrap'>
|
||||
<button
|
||||
class='btn btn-primary d-flex align-items-center font-weight-bold font-size-24 h-50 mb-5'
|
||||
type='button'
|
||||
use:click={() => play(media)}>
|
||||
<span class='material-symbols-outlined mr-10 font-size-24 w-30'> play_arrow </span>
|
||||
<span>{getPlayText(media)}</span>
|
||||
</button>
|
||||
{#if alToken}
|
||||
<button class='btn d-flex align-items-center mb-5 font-weight-bold font-size-16 btn-primary' use:click={toggleStatus}>
|
||||
<span class='material-symbols-outlined mr-10 font-size-18 w-30'> {(media.mediaListEntry?.status in toggleStatusMap) ? 'remove' : 'add'} </span>
|
||||
{getStatusText(media)}
|
||||
</button>
|
||||
<div class='input-group shadow-lg mb-5 font-size-16'>
|
||||
<div class='input-group-prepend'>
|
||||
<span class='input-group-text bg-tp pl-15 d-flex material-symbols-outlined font-size-18'>hotel_class</span>
|
||||
</div>
|
||||
<select class='form-control' required value={(media.mediaListEntry?.score || '').toString()} on:change={({ target }) => { score(media, Number(target.value)) }}>
|
||||
<option value selected disabled hidden>Score</option>
|
||||
<option>1</option>
|
||||
<option>2</option>
|
||||
<option>3</option>
|
||||
<option>4</option>
|
||||
<option>5</option>
|
||||
<option>6</option>
|
||||
<option>7</option>
|
||||
<option>8</option>
|
||||
<option>9</option>
|
||||
<option>10</option>
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
{#if media.trailer}
|
||||
<button class='btn d-flex align-items-center mb-5 font-weight-bold font-size-16' use:click={() => viewTrailer(media)}>
|
||||
<span class='material-symbols-outlined mr-15 font-size-18 w-30'> live_tv </span>
|
||||
Trailer
|
||||
</button>
|
||||
{/if}
|
||||
<div class='d-flex mb-5 w-full'>
|
||||
<button class='btn flex-fill font-weight-bold font-size-16 d-flex align-items-center' use:click={() => { openInBrowser(`https://anilist.co/anime/${media.id}`) }}>
|
||||
<span class='material-symbols-outlined mr-15 font-size-18 w-30'> open_in_new </span>
|
||||
Open
|
||||
</button>
|
||||
<button class='btn flex-fill font-weight-bold font-size-16 ml-5 d-flex align-items-center' use:click={() => { copyToClipboard(`https://miru.watch/anime/${media.id}`) }}>
|
||||
<span class='material-symbols-outlined mr-15 font-size-18 w-30'> share </span>
|
||||
Share
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
select.form-control:invalid {
|
||||
color: var(--dm-input-placeholder-text-color);
|
||||
}
|
||||
.bg-tp {
|
||||
background-color: var(--dm-button-bg-color) !important;
|
||||
}
|
||||
.w-30 {
|
||||
width: 3rem
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,16 +1,18 @@
|
|||
<script>
|
||||
import { Building2, Earth, GraduationCap, FolderKanban, Languages, CalendarRange, MonitorPlay, Type } from 'lucide-svelte'
|
||||
|
||||
export let media = null
|
||||
export let alt = null
|
||||
|
||||
const detailsMap = [
|
||||
{ property: 'season', label: 'Season', icon: 'spa', custom: 'property' },
|
||||
{ property: 'status', label: 'Status', icon: 'live_tv' },
|
||||
{ property: 'studios', label: 'Studio', icon: 'business', custom: 'property' },
|
||||
{ property: 'source', label: 'Source', icon: 'source' },
|
||||
{ property: 'countryOfOrigin', label: 'Country', icon: 'public', custom: 'property' },
|
||||
{ property: 'isAdult', label: 'Adult', icon: '18_up_rating' },
|
||||
{ property: 'english', label: 'English', icon: 'title' },
|
||||
{ property: 'romaji', label: 'Romaji', icon: 'translate' },
|
||||
{ property: 'season', label: 'Season', icon: CalendarRange, custom: 'property' },
|
||||
{ property: 'status', label: 'Status', icon: MonitorPlay },
|
||||
{ property: 'studios', label: 'Studio', icon: Building2, custom: 'property' },
|
||||
{ property: 'source', label: 'Source', icon: FolderKanban },
|
||||
{ property: 'countryOfOrigin', label: 'Country', icon: Earth, custom: 'property' },
|
||||
{ property: 'isAdult', label: 'Adult', icon: GraduationCap },
|
||||
{ property: 'english', label: 'English', icon: Type },
|
||||
{ property: 'romaji', label: 'Romaji', icon: Languages },
|
||||
{ property: 'native', label: 'Native', icon: '語', custom: 'icon' }
|
||||
]
|
||||
async function getCustomProperty (detail, media) {
|
||||
|
|
@ -47,12 +49,16 @@
|
|||
{#each detailsMap as detail}
|
||||
{@const property = getProperty(detail.property, media)}
|
||||
{#if property}
|
||||
<div class='d-flex flex-row mx-10 py-5'>
|
||||
<div class={'mr-10 ' + (detail.custom === 'icon' ? 'd-flex align-items-center text-nowrap font-size-20 font-weight-bold' : 'material-symbols-outlined font-size-24')}>
|
||||
{detail.icon}
|
||||
</div>
|
||||
<div class='d-flex flex-row mx-10 py-5 justify-content-center'>
|
||||
{#if detail.custom !== 'icon'}
|
||||
<svelte:component size='2rem' this={detail.icon} class='mr-10' />
|
||||
{:else}
|
||||
<div class='mr-10 d-flex align-items-center text-nowrap font-size-12 font-weight-bold line-height-normal'>
|
||||
{detail.icon}
|
||||
</div>
|
||||
{/if}
|
||||
<div class='d-flex flex-column justify-content-center text-nowrap'>
|
||||
<div class='font-weight-bold select-all'>
|
||||
<div class='font-weight-bold select-all line-height-normal'>
|
||||
{#if detail.custom === 'property'}
|
||||
{#await getCustomProperty(detail, media)}
|
||||
Fetching...
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
<script context='module'>
|
||||
let fillerEpisodes = {}
|
||||
|
||||
fetch('https://raw.githubusercontent.com/ThaUnknown/filler-scrape/master/filler.json').then(async res => {
|
||||
fillerEpisodes = await res.json()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { since } from '@/modules/util.js'
|
||||
import { click } from '@/modules/click.js'
|
||||
|
|
@ -25,12 +33,12 @@
|
|||
async function load () {
|
||||
// updates episodeList when clicking through relations / recommendations
|
||||
episodeList = Array.from({ length: episodeCount }, (_, i) => ({
|
||||
episode: i + 1, image: null, summary: null, rating: null, title: null, length: null, airdate: null, airingAt: null
|
||||
episode: i + 1, image: null, summary: null, rating: null, title: null, length: null, airdate: null, airingAt: null, filler: fillerEpisodes[id]?.includes(i + 1)
|
||||
}))
|
||||
|
||||
const res = await fetch('https://api.ani.zip/mappings?anilist_id=' + id)
|
||||
const { episodes, specialCount, episodeCount: newEpisodeCount } = await res.json()
|
||||
/** @type {{ airingAt: number; episode: number; }[]} */
|
||||
/** @type {{ airingAt: number; episode: number; filler?: boolean }[]} */
|
||||
let alEpisodes = episodeList
|
||||
|
||||
// fallback: pull episodes from airing schedule if anime doesn't have expected episode count
|
||||
|
|
@ -38,14 +46,14 @@
|
|||
const settled = (await anilistClient.episodes({ id })).data.Page?.airingSchedules
|
||||
if (settled?.length) alEpisodes = settled
|
||||
}
|
||||
for (const { episode, airingAt } of alEpisodes) {
|
||||
for (const { episode, airingAt, filler } of alEpisodes) {
|
||||
const alDate = new Date((airingAt || 0) * 1000)
|
||||
|
||||
// validate by air date if the anime has specials AND doesn't have matching episode count
|
||||
const needsValidation = !(!specialCount || (media.episodes && media.episodes === newEpisodeCount && episodes && episodes[Number(episode)]))
|
||||
const { image, summary, rating, title, length, airdate } = needsValidation ? episodeByAirDate(alDate, episodes, episode) : ((episodes && episodes[Number(episode)]) || {})
|
||||
|
||||
episodeList[episode - 1] = { episode, image, summary, rating, title, length: length || duration, airdate: +alDate || airdate, airingAt: +alDate || airdate }
|
||||
episodeList[episode - 1] = { episode, image, summary, rating, title, length: length || duration, airdate: +alDate || airdate, airingAt: +alDate || airdate, filler }
|
||||
}
|
||||
}
|
||||
$: if (media) load()
|
||||
|
|
@ -53,17 +61,22 @@
|
|||
const animeProgress = liveAnimeProgress(id)
|
||||
</script>
|
||||
|
||||
{#each episodeOrder ? episodeList : [...episodeList].reverse() as { episode, image, summary, rating, title, length, airdate }}
|
||||
{#each episodeOrder ? episodeList : [...episodeList].reverse() as { episode, image, summary, rating, title, length, airdate, filler }}
|
||||
{@const completed = !watched && userProgress >= episode}
|
||||
{@const target = userProgress + 1 === episode}
|
||||
{@const progress = !watched && ($animeProgress?.[episode] ?? 0)}
|
||||
<div class='w-full my-20 content-visibility-auto scale' class:opacity-half={completed} class:px-20={!target} class:h-150={image || summary}>
|
||||
<div class='rounded w-full h-full overflow-hidden d-flex flex-xsm-column flex-row pointer' class:border={target} class:bg-black={completed} class:bg-dark={!completed} use:click={() => play(episode)}>
|
||||
<div class='rounded w-full h-full overflow-hidden d-flex flex-xsm-column flex-row pointer position-relative' class:border={target || filler} class:bg-black={completed} class:border-secondary={filler} class:bg-dark={!completed} use:click={() => play(episode)}>
|
||||
{#if image}
|
||||
<div class='h-full'>
|
||||
<img alt='thumbnail' src={image} class='img-cover h-full' />
|
||||
</div>
|
||||
{/if}
|
||||
{#if filler}
|
||||
<div class='position-absolute bottom-0 right-0 bg-secondary py-5 px-10 text-dark rounded-top rounded-left font-weight-bold'>
|
||||
Filler
|
||||
</div>
|
||||
{/if}
|
||||
<div class='h-full w-full px-20 py-15 d-flex flex-column'>
|
||||
<div class='w-full d-flex flex-row mb-15'>
|
||||
<div class='text-white font-weight-bold font-size-16 overflow-hidden title'>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { anilistClient } from '@/modules/anilist.js'
|
||||
import { click } from '@/modules/click.js'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
import { ExternalLink } from 'lucide-svelte'
|
||||
|
||||
/** @type {import('@/modules/al.d.ts').Media} */
|
||||
export let media
|
||||
|
|
@ -22,7 +23,9 @@
|
|||
<img src={friend.user.avatar.medium} alt='avatar' class='w-50 h-50 img-fluid rounded cover-img' />
|
||||
<span class='my-0 pl-20 mr-auto text-truncate'>{friend.user.name}</span>
|
||||
<span class='my-0 px-10 text-capitalize'>{friend.status.toLowerCase()}</span>
|
||||
<span class='material-symbols-outlined pointer text-primary font-size-18' use:click={() => IPC.emit('open', 'https://anilist.co/user/' + friend.user.name)}> open_in_new </span>
|
||||
<span class='pointer text-primary d-flex align-items-center' use:click={() => IPC.emit('open', 'https://anilist.co/user/' + friend.user.name)}>
|
||||
<ExternalLink size='1.8rem' />
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,199 +0,0 @@
|
|||
<script>
|
||||
import { playAnime } from '../RSSView.svelte'
|
||||
import { anilistClient } from '@/modules/anilist.js'
|
||||
import { getMediaMaxEp } from '@/modules/anime.js'
|
||||
import { getContext } from 'svelte'
|
||||
import Details from './Details.svelte'
|
||||
import Following from './Following.svelte'
|
||||
import Controls from './Controls.svelte'
|
||||
import ToggleList from './ToggleList.svelte'
|
||||
import { click } from '@/modules/click.js'
|
||||
|
||||
const view = getContext('view')
|
||||
const trailer = getContext('trailer')
|
||||
function close () {
|
||||
$view = null
|
||||
}
|
||||
$: media = $view
|
||||
let modal
|
||||
$: media && modal?.focus()
|
||||
$: !$trailer && modal?.focus()
|
||||
$: maxPlayEp = getMediaMaxEp($view || {}, true)
|
||||
function checkClose ({ keyCode }) {
|
||||
if (keyCode === 27) close()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class='modal modal-full z-40' class:show={media} on:keydown={checkClose} tabindex='-1' role='button' bind:this={modal}>
|
||||
{#if media}
|
||||
<div class='h-full modal-content bg-very-dark p-0 overflow-y-auto'>
|
||||
<button class='close pointer z-30 bg-dark top-20 right-0 position-absolute' type='button' use:click={close}> × </button>
|
||||
<div class='h-md-half w-full position-relative z-20'>
|
||||
<div class='h-full w-full position-absolute bg-dark-light banner' style:--bannerurl={`url('${media.bannerImage || ''}')`} />
|
||||
<div class='d-flex h-full top w-full'>
|
||||
<div class='container-xl w-full'>
|
||||
<div class='row d-flex justify-content-end flex-row h-full px-20 pt-20 px-xl-0'>
|
||||
<div class='col-md-3 col-4 d-flex h-full justify-content-end flex-column pb-15 align-items-center'>
|
||||
<img class='contain-img rounded mw-full mh-full shadow' alt='cover' src={media.coverImage?.extraLarge || media.coverImage?.medium} />
|
||||
</div>
|
||||
<div class='col-md-9 col-8 row align-content-end'>
|
||||
<div class='col-md-8 col-12 d-flex justify-content-end flex-column pl-20'>
|
||||
<div class='px-md-20 d-flex flex-column font-size-12'>
|
||||
<span class='title font-weight-bold pb-sm-15 text-white select-all'>
|
||||
{media.title.userPreferred}
|
||||
</span>
|
||||
<div class='d-flex flex-row font-size-18 pb-sm-15'>
|
||||
{#if media.averageScore}
|
||||
<span class='material-symbols-outlined mr-10 font-size-24'> trending_up </span>
|
||||
<span class='mr-20'>
|
||||
Rating: {media.averageScore + '%'}
|
||||
</span>
|
||||
{/if}
|
||||
{#if media.format}
|
||||
<span class='material-symbols-outlined mx-10 font-size-24'> monitor </span>
|
||||
<span class='mr-20 text-capitalize'>
|
||||
Format: {media.format === 'TV' ? media.format : media.format?.replace(/_/g, ' ').toLowerCase()}
|
||||
</span>
|
||||
{/if}
|
||||
{#if media.episodes !== 1 && getMediaMaxEp(media)}
|
||||
<span class='material-symbols-outlined mx-10 font-size-24'> theaters </span>
|
||||
<span class='mr-20'>
|
||||
Episodes: {getMediaMaxEp(media)}
|
||||
</span>
|
||||
{:else if media.duration}
|
||||
<span class='material-symbols-outlined mx-10 font-size-24'> timer </span>
|
||||
<span class='mr-20'>
|
||||
Length: {media.duration + ' min'}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class='pb-15 pt-5 px-5 overflow-x-auto text-nowrap font-weight-bold'>
|
||||
{#each media.genres as genre}
|
||||
<div class='badge badge-pill shadow'>
|
||||
{genre}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Controls bind:media={$view} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='container-xl bg-very-dark z-10'>
|
||||
<div class='row p-20 px-xl-0 flex-column-reverse flex-md-row'>
|
||||
<div class='col-md-9 pr-50'>
|
||||
<h1 class='title font-weight-bold text-white'>Synopsis</h1>
|
||||
<div class='font-size-16 pre-wrap select-all card m-0'>
|
||||
{media.description?.replace(/<[^>]*>/g, '') || ''}
|
||||
</div>
|
||||
<ToggleList list={media.relations?.edges?.filter(({ node }) => node.type === 'ANIME')} let:item title='Relations'>
|
||||
<div class='w-150 mx-15 my-10 rel pointer'
|
||||
use:click={async () => { $view = null; $view = (await anilistClient.searchIDSingle({ id: item.node.id })).data.Media }}>
|
||||
<img loading='lazy' src={item.node.coverImage.medium || ''} alt='cover' class='cover-img w-full h-200 rel-img rounded' />
|
||||
<div class='pt-5'>{item.relationType.replace(/_/g, ' ').toLowerCase()}</div>
|
||||
<h5 class='font-weight-bold text-white mb-5'>{item.node.title.userPreferred}</h5>
|
||||
</div>
|
||||
</ToggleList>
|
||||
{#if maxPlayEp}
|
||||
<h1 class='title font-weight-bold text-white pt-20'>Episodes</h1>
|
||||
<div class='card m-0 d-inline-block'>
|
||||
<table class='table table-hover w-500 table-auto '>
|
||||
<tbody>
|
||||
{#each Array(maxPlayEp) as _, i}
|
||||
{@const ep = maxPlayEp - i}
|
||||
<tr class="font-size-20 py-10 pointer {ep <= media.mediaListEntry?.progress ? 'text-muted' : 'text-white'}"
|
||||
use:click={() => {
|
||||
playAnime(media, ep)
|
||||
close()
|
||||
}}>
|
||||
<td class='w-full font-weight-semi-bold'>Episode {ep}</td>
|
||||
<td class='material-symbols-outlined text-right h-full d-table-cell'>play_arrow</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
<ToggleList list={media.recommendations.edges.filter(edge => edge.node.mediaRecommendation)} let:item title='Recommendations'>
|
||||
<div class='w-150 mx-15 my-10 rel pointer'
|
||||
use:click={async () => { $view = null; $view = (await anilistClient.searchIDSingle({ id: item.node.mediaRecommendation.id })).data.Media }}>
|
||||
<img loading='lazy' src={item.node.mediaRecommendation.coverImage.medium || ''} alt='cover' class='cover-img w-full h-200 rel-img rounded' />
|
||||
<h5 class='font-weight-bold text-white mb-5'>{item.node.mediaRecommendation.title.userPreferred}</h5>
|
||||
</div>
|
||||
</ToggleList>
|
||||
</div>
|
||||
<div class='col-md-3 px-sm-0 px-20'>
|
||||
{#if media.mediaListEntry?.progress}
|
||||
<h1 class='title font-weight-bold text-white'>Progress</h1>
|
||||
<div class='card m-0 pt-20 pb-15 d-flex flex-md-column flex-row text-capitalize align-items-start'>
|
||||
<div class='progress w-full'>
|
||||
<div class='progress-bar' role='progressbar' style='width: {media.mediaListEntry?.progress / getMediaMaxEp(media) * 100}%;' />
|
||||
</div>
|
||||
<div class='font-weight-bold pt-10'>
|
||||
{media.mediaListEntry?.progress} / {getMediaMaxEp(media)} Available Episodes
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<Details {media} />
|
||||
<Following {media} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.pre-wrap {
|
||||
white-space: pre-wrap
|
||||
}
|
||||
.banner {
|
||||
background: no-repeat center center;
|
||||
background-size: cover;
|
||||
background-image: linear-gradient(0deg, rgba(17, 20, 23, 1) 0%, rgba(17, 20, 23, 0.8) 25%, rgba(17, 20, 23, 0.4) 50%, rgba(37, 40, 44, 0) 100%), var(--bannerurl) !important;
|
||||
}
|
||||
|
||||
.d-table-cell {
|
||||
display: table-cell !important;
|
||||
}
|
||||
|
||||
.top {
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.title {
|
||||
font-size: 4rem;
|
||||
}
|
||||
.pr-50 {
|
||||
padding-right: 5rem;
|
||||
}
|
||||
.close {
|
||||
top: 4rem !important;
|
||||
left: unset !important;
|
||||
right: 2.5rem !important;
|
||||
}
|
||||
.badge {
|
||||
background-color: var(--dm-button-bg-color) !important;
|
||||
padding: 0.6rem 2rem;
|
||||
font-size: 1.4rem;
|
||||
border: none;
|
||||
margin-right: 0.6rem;
|
||||
}
|
||||
.rel-img{
|
||||
height: 27rem;
|
||||
width: 17rem
|
||||
}
|
||||
|
||||
.cover-img {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.rel {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.rel:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
import SmallCard from "@/components/cards/SmallCard.svelte"
|
||||
import SkeletonCard from "@/components/cards/SkeletonCard.svelte"
|
||||
import Helper from "@/modules/helper.js"
|
||||
import { ArrowLeft, Clapperboard, ExternalLink, Users, Heart, Play, Share2, Timer, TrendingUp, Tv } from 'lucide-svelte'
|
||||
|
||||
export let overlay
|
||||
const view = getContext('view')
|
||||
|
|
@ -92,13 +93,24 @@
|
|||
close()
|
||||
}
|
||||
})
|
||||
|
||||
// async function score (media, score) {
|
||||
// const variables = {
|
||||
// id: media.id,
|
||||
// score: score * 10
|
||||
// }
|
||||
// await anilistClient.entry(variables)
|
||||
// media = (await anilistClient.searchIDSingle({ id: media.id })).data.Media
|
||||
// }
|
||||
</script>
|
||||
|
||||
<div class='modal modal-full z-50' class:show={media} on:keydown={checkClose} tabindex='-1' role='button' bind:this={modal}>
|
||||
{#if media}
|
||||
<div class='h-full modal-content bg-very-dark p-0 overflow-y-auto position-relative' bind:this={container} use:smoothScroll>
|
||||
{#if mediaList.length > 1}
|
||||
<button class='close back pointer z-30 bg-dark top-20 left-0 position-fixed material-symbols-outlined filled' type='button' use:click={back}>arrow_back</button>
|
||||
<button class='close back pointer z-30 bg-dark top-20 left-0 position-fixed' use:click={back}>
|
||||
<ArrowLeft size='1.8rem' />
|
||||
</button>
|
||||
{/if}
|
||||
<button class='close pointer z-30 bg-dark top-20 right-0 position-fixed' type='button' use:click={() => close()}> × </button>
|
||||
<img class='w-full cover-img banner position-absolute' alt='banner' src={media.bannerImage || ' '} />
|
||||
|
|
@ -113,7 +125,7 @@
|
|||
<div class='d-flex flex-row font-size-18 flex-wrap mt-5'>
|
||||
{#if media.averageScore}
|
||||
<div class='d-flex flex-row mt-10' title='{media.averageScore / 10} by {anilistClient.reviews(media)} reviews'>
|
||||
<span class='material-symbols-outlined mx-10 font-size-24'> trending_up </span>
|
||||
<TrendingUp class='mx-10' size='2.2rem' />
|
||||
<span class='mr-20'>
|
||||
Rating: {media.averageScore + '%'}
|
||||
</span>
|
||||
|
|
@ -121,7 +133,7 @@
|
|||
{/if}
|
||||
{#if media.format}
|
||||
<div class='d-flex flex-row mt-10'>
|
||||
<span class='material-symbols-outlined mx-10 font-size-24'> monitor </span>
|
||||
<Tv class='mx-10' size='2.2rem' />
|
||||
<span class='mr-20 text-capitalize'>
|
||||
Format: {formatMap[media.format]}
|
||||
</span>
|
||||
|
|
@ -129,14 +141,14 @@
|
|||
{/if}
|
||||
{#if media.episodes !== 1 && getMediaMaxEp(media)}
|
||||
<div class='d-flex flex-row mt-10'>
|
||||
<span class='material-symbols-outlined mx-10 font-size-24'> theaters </span>
|
||||
<Clapperboard class='mx-10' size='2.2rem' />
|
||||
<span class='mr-20'>
|
||||
Episodes: {getMediaMaxEp(media)}
|
||||
</span>
|
||||
</div>
|
||||
{:else if media.duration}
|
||||
<div class='d-flex flex-row mt-10'>
|
||||
<span class='material-symbols-outlined mx-10 font-size-24'> timer </span>
|
||||
<Timer class='mx-10' size='2.2rem' />
|
||||
<span class='mr-20'>
|
||||
Length: {media.duration + ' min'}
|
||||
</span>
|
||||
|
|
@ -144,7 +156,7 @@
|
|||
{/if}
|
||||
{#if media.stats?.scoreDistribution}
|
||||
<div class='d-flex flex-row mt-10'>
|
||||
<span class='material-symbols-outlined mx-10 font-size-24'> group </span>
|
||||
<Users class='mx-10' size='2.2rem' />
|
||||
<span class='mr-20' title='{media.averageScore / 10} by {anilistClient.reviews(media)} reviews'>
|
||||
Reviews: {anilistClient.reviews(media)}
|
||||
</span>
|
||||
|
|
@ -158,22 +170,38 @@
|
|||
<button class='btn btn-lg btn-secondary w-250 text-dark font-weight-bold shadow-none border-0 d-flex align-items-center justify-content-center mr-10 mt-20'
|
||||
use:click={() => play()}
|
||||
disabled={media.status === 'NOT_YET_RELEASED'}>
|
||||
<span class='material-symbols-outlined font-size-24 filled pr-10'>
|
||||
play_arrow
|
||||
</span>
|
||||
<Play class='mr-10' fill='currentColor' size='1.6rem' />
|
||||
{playButtonText}
|
||||
</button>
|
||||
<div class='mt-20'>
|
||||
<button class='btn bg-dark btn-lg btn-square material-symbols-outlined font-size-20 shadow-none border-0' class:filled={media.isFavourite} use:click={toggleFavourite} disabled={!Helper.isAniAuth()}>
|
||||
favorite
|
||||
<div class='mt-20 d-flex'>
|
||||
<button class='btn bg-dark btn-lg btn-square d-flex align-items-center justify-content-center shadow-none border-0' use:click={toggleFavourite} disabled={!Helper.isAniAuth()}>
|
||||
<Heart fill={media.isFavourite ? 'currentColor' : 'transparent'} size='1.7rem' />
|
||||
</button>
|
||||
<Scoring {media} viewAnime={true} />
|
||||
<button class='btn bg-dark btn-lg btn-square ml-10 material-symbols-outlined font-size-20 shadow-none border-0' use:click={() => copyToClipboard(`https://miru.watch/anime/${media.id}`)}>
|
||||
share
|
||||
<button class='btn bg-dark btn-lg btn-square d-flex align-items-center justify-content-center shadow-none border-0 ml-10' use:click={() => copyToClipboard(`https://miru.watch/anime/${media.id}`)}>
|
||||
<Share2 size='1.7rem' />
|
||||
</button>
|
||||
<button class='btn bg-dark btn-lg btn-square ml-10 material-symbols-outlined font-size-20 shadow-none border-0' use:click={() => openInBrowser(`https://anilist.co/anime/${media.id}`)}>
|
||||
open_in_new
|
||||
<button class='btn bg-dark btn-lg btn-square d-flex align-items-center justify-content-center shadow-none border-0 ml-10' use:click={() => openInBrowser(`https://anilist.co/anime/${media.id}`)}>
|
||||
<ExternalLink size='1.7rem' />
|
||||
</button>
|
||||
<!-- <div class='input-group shadow-lg mb-5 font-size-16'>
|
||||
<div class='input-group-prepend'>
|
||||
<span class='input-group-text bg-tp pl-15 d-flex font-size-18'>hotel_class</span> stars
|
||||
</div>
|
||||
<select class='form-control' required value={(media.mediaListEntry?.score || '').toString()} on:change={({ target }) => { score(media, Number(target.value)) }}>
|
||||
<option value selected disabled hidden>Score</option>
|
||||
<option>1</option>
|
||||
<option>2</option>
|
||||
<option>3</option>
|
||||
<option>4</option>
|
||||
<option>5</option>
|
||||
<option>6</option>
|
||||
<option>7</option>
|
||||
<option>8</option>
|
||||
<option>9</option>
|
||||
<option>10</option>
|
||||
</select>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { click } from '@/modules/click.js'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
import { ExternalLink, User } from 'lucide-svelte'
|
||||
export let peers
|
||||
export let invite
|
||||
export let cleanup
|
||||
|
|
@ -17,15 +18,12 @@
|
|||
{#if peer.user?.avatar?.medium}
|
||||
<img src={peer.user?.avatar?.medium} alt='avatar' class='w-50 h-50 img-fluid rounded' />
|
||||
{:else}
|
||||
<span class='material-symbols-outlined w-50 h-50 anon'> person </span>
|
||||
<span class='w-50 h-50 anon d-flex align-items-center'><User size='4rem' /></span>
|
||||
{/if}
|
||||
<h4 class='my-0 pl-20 mr-auto'>{peer.user?.name || 'Anonymous'}</h4>
|
||||
<h4 class='my-0 pl-20 mr-auto line-height-normal'>{peer.user?.name || 'Anonymous'}</h4>
|
||||
{#if peer.user?.name}
|
||||
<span class='material-symbols-outlined pointer text-primary' use:click={() => IPC.emit('open', 'https://anilist.co/user/' + peer.user?.name)}> open_in_new </span>
|
||||
<span class='pointer text-primary d-flex align-items-center' use:click={() => IPC.emit('open', 'https://anilist.co/user/' + peer.user?.name)}><ExternalLink size='2.5rem' /></span>
|
||||
{/if}
|
||||
<!-- {#if state === 'host'}
|
||||
<span class='material-symbols-outlined ml-15 pointer text-danger' use:click={() => peer.peer.pc.close()}> logout </span>
|
||||
{/if} -->
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@
|
|||
|
||||
<script>
|
||||
import Lobby from './Lobby.svelte'
|
||||
import { Plus, UserPlus } from 'lucide-svelte'
|
||||
|
||||
let joinText
|
||||
|
||||
|
|
@ -179,11 +180,11 @@
|
|||
{#if !$state}
|
||||
<div class='d-flex flex-row flex-wrap justify-content-center align-items-center h-full mb-20 pb-20 root'>
|
||||
<div class='card d-flex flex-column align-items-center w-300 h-300 justify-content-end'>
|
||||
<span class='font-size-80 material-symbols-outlined d-flex align-items-center h-full'>add</span>
|
||||
<Plus size='6rem' class='d-flex align-items-center h-full' />
|
||||
<button class='btn btn-primary btn-lg mt-10 btn-block' type='button' use:click={() => joinLobby()}>Create Lobby</button>
|
||||
</div>
|
||||
<div class='card d-flex flex-column align-items-center w-300 h-300 justify-content-end'>
|
||||
<span class='font-size-80 material-symbols-outlined d-flex align-items-center h-full'>group_add</span>
|
||||
<UserPlus size='6rem' class='d-flex align-items-center h-full' />
|
||||
<h2 class='font-weight-bold'>Join Lobby</h2>
|
||||
<input
|
||||
type='text'
|
||||
|
|
@ -203,7 +204,4 @@
|
|||
.font-size-50 {
|
||||
font-size: 5rem;
|
||||
}
|
||||
.font-size-80 {
|
||||
font-size: 8rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ module.exports = (parentDir, alias = {}, aliasFields = 'browser', filename = 'ap
|
|||
module: false,
|
||||
url: false,
|
||||
debug: resolve(__dirname, './modules/debug.js'),
|
||||
'svelte-radix': resolve(__dirname, '../node_modules/svelte-radix/dist/index.js'),
|
||||
'bittorrent-tracker/lib/client/websocket-tracker.js': resolve('../node_modules/bittorrent-tracker/lib/client/websocket-tracker.js')
|
||||
},
|
||||
extensions: ['.mjs', '.js', '.svelte']
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "Miru",
|
||||
"version": "5.3.4",
|
||||
"version": "5.4.1",
|
||||
"private": true,
|
||||
"author": "ThaUnknown_ <ThaUnknown@users.noreply.github.com>",
|
||||
"description": "Stream anime torrents, real-time with no waiting for downloads.",
|
||||
|
|
|
|||
873
pnpm-lock.yaml
873
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import { formatMap } from './anime.js'
|
||||
import { click } from '@/modules/click.js'
|
||||
import { alToken } from '@/modules/settings.js'
|
||||
export let media
|
||||
|
||||
let hide = true
|
||||
|
|
@ -21,9 +20,6 @@
|
|||
}
|
||||
const playButtonText = getPlayButtonText(media)
|
||||
|
||||
function volume (video) {
|
||||
video.volume = 0.1
|
||||
}
|
||||
let muted = true
|
||||
function toggleMute () {
|
||||
muted = !muted
|
||||
|
|
@ -32,19 +28,19 @@
|
|||
function play () {
|
||||
open('miru://anime/' + media.id)
|
||||
}
|
||||
function lazyload (video) {
|
||||
function lazyload (iframe) {
|
||||
if ('IntersectionObserver' in window) {
|
||||
const lazyVideoObserver = new IntersectionObserver(entries => {
|
||||
for (const { target, isIntersecting } of entries) {
|
||||
if (isIntersecting) {
|
||||
video.src = video.dataset.src
|
||||
iframe.src = iframe.dataset.src
|
||||
lazyVideoObserver.unobserve(target)
|
||||
}
|
||||
}
|
||||
})
|
||||
lazyVideoObserver.observe(video.parentNode)
|
||||
lazyVideoObserver.observe(iframe.parentNode)
|
||||
} else {
|
||||
video.src = video.dataset.src
|
||||
iframe.src = iframe.dataset.src
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -56,18 +52,15 @@
|
|||
<div class='material-symbols-outlined filled position-absolute z-10 top-0 right-0 p-15 font-size-22' class:d-none={hide} use:click={toggleMute}>{muted ? 'volume_off' : 'volume_up'}</div>
|
||||
<!-- for now we use some invidious instance, would be nice to somehow get these links outselves, this redirects straight to some google endpoint -->
|
||||
<!-- eslint-disable-next-line svelte/valid-compile -->
|
||||
<video data-src={`https://inv.tux.pizza/latest_version?id=${media.trailer.id}&itag=18`}
|
||||
class='w-full h-full position-absolute left-0'
|
||||
<iframe
|
||||
class='w-full border-0 position-absolute left-0'
|
||||
class:d-none={hide}
|
||||
playsinline
|
||||
preload='none'
|
||||
loading='lazy'
|
||||
title={media.title.userPreferred}
|
||||
allow='autoplay'
|
||||
use:lazyload
|
||||
loop
|
||||
use:volume
|
||||
bind:muted
|
||||
on:loadeddata={() => { hide = false }}
|
||||
autoplay />
|
||||
on:load={() => { hide = false }}
|
||||
data-src={`https://www.youtube-nocookie.com/embed/${media.trailer?.id}?autoplay=1&controls=0&mute=${muted ? 1 : 0}&disablekb=1&loop=1&vq=medium&playlist=${media.trailer?.id}&cc_lang_pref=ja`}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class='w-full px-20'>
|
||||
|
|
@ -83,10 +76,10 @@
|
|||
</span>
|
||||
{playButtonText}
|
||||
</button>
|
||||
<button class='btn btn-square ml-10 material-symbols-outlined font-size-16 shadow-none border-0' class:filled={media.isFavourite} use:click={noop} disabled={!alToken}>
|
||||
<button class='btn btn-square ml-10 material-symbols-outlined font-size-16 shadow-none border-0' class:filled={media.isFavourite} use:click={noop}>
|
||||
favorite
|
||||
</button>
|
||||
<button class='btn btn-square ml-10 material-symbols-outlined font-size-16 shadow-none border-0' class:filled={media.mediaListEntry} use:click={noop} disabled={!alToken}>
|
||||
<button class='btn btn-square ml-10 material-symbols-outlined font-size-16 shadow-none border-0' class:filled={media.mediaListEntry} use:click={noop}>
|
||||
bookmark
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,28 @@
|
|||
@import '@fontsource-variable/material-symbols-outlined/full.css';
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-family: "Material Symbols Outlined Variable";
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-feature-settings: "liga";
|
||||
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 64;
|
||||
}
|
||||
|
||||
.filled {
|
||||
font-variation-settings: 'FILL' 1;
|
||||
}
|
||||
|
||||
.dark-mode {
|
||||
background-color: #101113 !important;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue