mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-04-21 02:32:06 +00:00
feat: viewAnime improvements
This commit is contained in:
parent
f1c9761630
commit
02bf9a9d60
9 changed files with 357 additions and 277 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "Miru",
|
"name": "Miru",
|
||||||
"version": "2.9.5",
|
"version": "2.9.6",
|
||||||
"author": "ThaUnknown_ <ThaUnknown@users.noreply.github.com>",
|
"author": "ThaUnknown_ <ThaUnknown@users.noreply.github.com>",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"homepage": "https://github.com/ThaUnknown/miru#readme",
|
"homepage": "https://github.com/ThaUnknown/miru#readme",
|
||||||
|
|
@ -122,7 +122,7 @@
|
||||||
"matroska-subtitles": "github:ThaUnknown/matroska-subtitles#patch",
|
"matroska-subtitles": "github:ThaUnknown/matroska-subtitles#patch",
|
||||||
"mime": "^3.0.0",
|
"mime": "^3.0.0",
|
||||||
"pump": "^3.0.0",
|
"pump": "^3.0.0",
|
||||||
"quartermoon": "^1.2.0",
|
"quartermoon": "^1.2.1",
|
||||||
"range-parser": "^1.2.1",
|
"range-parser": "^1.2.1",
|
||||||
"svelte-keybinds": "^1.0.4",
|
"svelte-keybinds": "^1.0.4",
|
||||||
"svelte-miniplayer": "^1.0.2",
|
"svelte-miniplayer": "^1.0.2",
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@
|
||||||
location.hash = ''
|
location.hash = ''
|
||||||
location.reload()
|
location.reload()
|
||||||
} else {
|
} else {
|
||||||
window.IPC.emit('open', 'https://anilist.co/api/v2/oauth/authorize?client_id=4254&response_type=token') //Change redirect_url to miru://auth
|
window.IPC.emit('open', 'https://anilist.co/api/v2/oauth/authorize?client_id=4254&response_type=token') // Change redirect_url to miru://auth
|
||||||
if (platformMap[window.version.platform] === 'Linux') {
|
if (platformMap[window.version.platform] === 'Linux') {
|
||||||
addToast({
|
addToast({
|
||||||
text: "If your linux distribution doesn't support custom protocol handlers, you can simply paste the full URL into the app.",
|
text: "If your linux distribution doesn't support custom protocol handlers, you can simply paste the full URL into the app.",
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
.alert {
|
.alert {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
animation: 0.3s ease 0s 1 fly-in;
|
animation: 0.3s ease 0s 1 fly-in;
|
||||||
right: 0 !important;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fly-in {
|
@keyframes fly-in {
|
||||||
|
|
|
||||||
|
|
@ -3,124 +3,24 @@
|
||||||
import { alRequest } from '@/modules/anilist.js'
|
import { alRequest } from '@/modules/anilist.js'
|
||||||
import { getMediaMaxEp } from '@/modules/anime.js'
|
import { getMediaMaxEp } from '@/modules/anime.js'
|
||||||
import { getContext } from 'svelte'
|
import { getContext } from 'svelte'
|
||||||
import { alToken } from '@/lib/pages/Settings.svelte'
|
import Details from './ViewAnime/Details.svelte'
|
||||||
import { countdown } from '@/modules/util.js'
|
import Following from './ViewAnime/Following.svelte'
|
||||||
import { addToast } from './Toasts.svelte'
|
import Controls from './ViewAnime/Controls.svelte'
|
||||||
|
import ToggleList from './ViewAnime/ToggleList.svelte'
|
||||||
|
|
||||||
const view = getContext('view')
|
const view = getContext('view')
|
||||||
|
const trailer = getContext('trailer')
|
||||||
function close () {
|
function close () {
|
||||||
$view = null
|
$view = null
|
||||||
}
|
}
|
||||||
$: console.log(media)
|
|
||||||
$: media = $view
|
$: media = $view
|
||||||
let modal
|
let modal
|
||||||
$: media && modal?.focus()
|
$: media && modal?.focus()
|
||||||
$: !$trailer && modal?.focus()
|
$: !$trailer && modal?.focus()
|
||||||
let following = null
|
|
||||||
async function updateFollowing (media) {
|
|
||||||
if (media) {
|
|
||||||
following = null
|
|
||||||
following = (await alRequest({ method: 'Following', id: media.id })).data?.Page?.mediaList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$: updateFollowing(media)
|
|
||||||
$: maxPlayEp = getMediaMaxEp($view || {}, true)
|
$: maxPlayEp = getMediaMaxEp($view || {}, true)
|
||||||
function checkClose ({ keyCode }) {
|
function checkClose ({ keyCode }) {
|
||||||
if (keyCode === 27) close()
|
if (keyCode === 27) close()
|
||||||
}
|
}
|
||||||
const statusMap = {
|
|
||||||
CURRENT: 'Watching',
|
|
||||||
PLANNING: 'Planning',
|
|
||||||
COMPLETED: 'Completed',
|
|
||||||
DROPPED: 'Dropped',
|
|
||||||
PAUSED: 'Paused',
|
|
||||||
REPEATING: 'Re-Watching'
|
|
||||||
}
|
|
||||||
const detailsMap = [
|
|
||||||
{ property: 'episode', label: 'Airing', icon: 'schedule', custom: 'property' },
|
|
||||||
{ property: 'genres', label: 'Genres', icon: 'theater_comedy' },
|
|
||||||
{ property: 'season', label: 'Season', icon: 'spa', custom: 'property' },
|
|
||||||
{ property: 'episodes', label: 'Episodes', icon: 'theaters', custom: 'property' },
|
|
||||||
{ property: 'duration', label: 'Duration', icon: 'timer', custom: 'property' },
|
|
||||||
{ property: 'format', label: 'Format', icon: 'monitor' },
|
|
||||||
{ property: 'status', label: 'Status', icon: 'live_tv' },
|
|
||||||
{ property: 'nodes', label: 'Studio', icon: 'business' },
|
|
||||||
{ property: 'source', label: 'Source', icon: 'source' },
|
|
||||||
{ property: 'averageScore', label: 'Rating', icon: 'trending_up', custom: 'property' },
|
|
||||||
{ property: 'english', label: 'English', icon: 'title' },
|
|
||||||
{ property: 'romaji', label: 'Romaji', icon: 'translate' },
|
|
||||||
{ property: 'native', label: 'Native', icon: '語', custom: 'icon' }
|
|
||||||
]
|
|
||||||
function getCustomProperty (detail, media) {
|
|
||||||
if (detail.property === 'episodes') {
|
|
||||||
if (media.mediaListEntry?.progress) {
|
|
||||||
return `Watched <b>${media.mediaListEntry.progress}</b> of <b>${getMediaMaxEp(media)}</b>`
|
|
||||||
}
|
|
||||||
return `${getMediaMaxEp(media)} Episodes`
|
|
||||||
} else if (detail.property === 'averageScore') {
|
|
||||||
return media.averageScore + '%'
|
|
||||||
} else if (detail.property === 'duration') {
|
|
||||||
return `${media.duration} minutes`
|
|
||||||
} else if (detail.property === 'season') {
|
|
||||||
return [media.season?.toLowerCase(), media.seasonYear].filter(f => f).join(' ')
|
|
||||||
} else if (detail.property === 'episode') {
|
|
||||||
return `Ep ${media.nextAiringEpisode.episode}: ${countdown(media.nextAiringEpisode.timeUntilAiring)}`
|
|
||||||
} else {
|
|
||||||
return media[detail.property]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function getProperty (property, media) {
|
|
||||||
if (property === 'episode') {
|
|
||||||
return media.nextAiringEpisode?.episode
|
|
||||||
} else if (property === 'english' || property === 'romaji' || property === 'native') {
|
|
||||||
return media.title[property]
|
|
||||||
}
|
|
||||||
return media[property]
|
|
||||||
}
|
|
||||||
async function addToList (media) {
|
|
||||||
if (media.mediaListEntry?.status !== 'CURRENT' && media.mediaListEntry?.status !== 'COMPLETED') {
|
|
||||||
const variables = {
|
|
||||||
method: media.mediaListEntry?.status !== 'PLANNING' ? 'Entry' : 'Delete',
|
|
||||||
id: media.mediaListEntry?.status !== 'PLANNING' ? media.id : media.mediaListEntry.id,
|
|
||||||
status: 'PLANNING'
|
|
||||||
}
|
|
||||||
await alRequest(variables)
|
|
||||||
$view = (await alRequest({ method: 'SearchIDSingle', id: media.id })).data.Media
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function score (media, score) {
|
|
||||||
const variables = {
|
|
||||||
method: 'Entry',
|
|
||||||
id: media.id,
|
|
||||||
score: score * 10
|
|
||||||
}
|
|
||||||
await alRequest(variables)
|
|
||||||
$view = (await alRequest({ method: '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) {
|
|
||||||
window.IPC.emit('open', url)
|
|
||||||
}
|
|
||||||
let showMoreRelations = false
|
|
||||||
function toggleRelations () {
|
|
||||||
showMoreRelations = !showMoreRelations
|
|
||||||
}
|
|
||||||
let showMoreRecommendations = false
|
|
||||||
function toggleRecommendations () {
|
|
||||||
showMoreRecommendations = !showMoreRecommendations
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="modal modal-full" class:show={media} on:keydown={checkClose} tabindex="-1" bind:this={modal}>
|
<div class="modal modal-full" class:show={media} on:keydown={checkClose} tabindex="-1" bind:this={modal}>
|
||||||
|
|
@ -144,27 +44,23 @@
|
||||||
<div class="d-flex flex-row font-size-18 pb-sm-15">
|
<div class="d-flex flex-row font-size-18 pb-sm-15">
|
||||||
{#if media.averageScore}
|
{#if media.averageScore}
|
||||||
<span class="material-icons mr-10 font-size-24"> trending_up </span>
|
<span class="material-icons mr-10 font-size-24"> trending_up </span>
|
||||||
<span>
|
<span class="mr-20">
|
||||||
Rating: {media.averageScore + '%'}
|
Rating: {media.averageScore + '%'}
|
||||||
<span class="font-weight-bold mr-20" />
|
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="material-icons mx-10 font-size-24"> monitor </span>
|
<span class="material-icons mx-10 font-size-24"> monitor </span>
|
||||||
<span>
|
<span class="mr-20 text-capitalize">
|
||||||
Format: {media.format === 'TV' ? media.format : media.format?.toLowerCase()}
|
Format: {media.format === 'TV' ? media.format : media.format?.replace(/_/g, ' ').toLowerCase()}
|
||||||
<span class="font-weight-bold mr-20 text-capitalize" />
|
|
||||||
</span>
|
</span>
|
||||||
{#if media.episodes !== 1 && getMediaMaxEp(media)}
|
{#if media.episodes !== 1 && getMediaMaxEp(media)}
|
||||||
<span class="material-icons mx-10 font-size-24"> theaters </span>
|
<span class="material-icons mx-10 font-size-24"> theaters </span>
|
||||||
<span>
|
<span class="mr-20">
|
||||||
Episodes: {getMediaMaxEp(media)}
|
Episodes: {getMediaMaxEp(media)}
|
||||||
<span class="font-weight-bold mr-20" />
|
|
||||||
</span>
|
</span>
|
||||||
{:else if media.duration}
|
{:else if media.duration}
|
||||||
<span class="material-icons mx-10 font-size-24"> timer </span>
|
<span class="material-icons mx-10 font-size-24"> timer </span>
|
||||||
<span>
|
<span class="mr-20">
|
||||||
Length: {media.duration + ' min'}
|
Length: {media.duration + ' min'}
|
||||||
<span class="font-weight-bold mr-20" />
|
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -177,62 +73,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 d-flex justify-content-end flex-column">
|
<Controls bind:media={$view} />
|
||||||
<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 shadow-lg"
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
playAnime(media, Math.min(maxPlayEp, media.mediaListEntry?.progress + 1))
|
|
||||||
close()
|
|
||||||
}}>
|
|
||||||
<span class="material-icons mr-10 font-size-24 w-30"> play_arrow </span>
|
|
||||||
<span>{(media.mediaListEntry?.progress && media.mediaListEntry?.status !== 'COMPLETED') ? 'Continue ' + Math.min(maxPlayEp, media.mediaListEntry?.progress + 1) : 'Play'}</span>
|
|
||||||
</button>
|
|
||||||
{#if alToken}
|
|
||||||
{#if media.mediaListEntry?.status !== 'CURRENT' && media.mediaListEntry?.status !== 'COMPLETED'}
|
|
||||||
<button class="btn d-flex align-items-center mb-5 font-weight-bold font-size-16 btn-primary shadow-lg" on:click={() => addToList(media)}>
|
|
||||||
<span class="material-icons mr-10 font-size-18 w-30"> {media.mediaListEntry?.status !== 'PLANNING' ? 'add' : 'remove'} </span>
|
|
||||||
{media.mediaListEntry?.status !== 'PLANNING' ? 'Add To List' : 'Remove From List'}
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
<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-icons font-size-18">hotel_class</span>
|
|
||||||
</div>
|
|
||||||
<select class="form-control" required value={(media.mediaListEntry?.score || '').toString()} on:change={({ target }) => { score(media, target.value) }}>
|
|
||||||
<option value selected disabled hidden>Score</option>
|
|
||||||
<option value="1">1</option>
|
|
||||||
<option value="2">2</option>
|
|
||||||
<option value="3">3</option>
|
|
||||||
<option value="4">4</option>
|
|
||||||
<option value="5">5</option>
|
|
||||||
<option value="6">6</option>
|
|
||||||
<option value="7">7</option>
|
|
||||||
<option value="8">8</option>
|
|
||||||
<option value="9">9</option>
|
|
||||||
<option value="10">10</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if media.trailer}
|
|
||||||
<button class="btn d-flex align-items-center mb-5 font-weight-bold font-size-16 shadow-lg" on:click={() => viewTrailer(media)}>
|
|
||||||
<span class="material-icons 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 shadow-lg d-flex align-items-center" on:click={() => { openInBrowser(`https://anilist.co/anime/${media.id}`) }}>
|
|
||||||
<span class="material-icons 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 shadow-lg d-flex align-items-center" on:click={() => { copyToClipboard(`<miru://anime/${media.id}>`) }}>
|
|
||||||
<span class="material-icons mr-15 font-size-18 w-30"> share </span>
|
|
||||||
Share
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -245,21 +86,13 @@
|
||||||
<div class="font-size-16 pr-15">
|
<div class="font-size-16 pr-15">
|
||||||
{@html media.description}
|
{@html media.description}
|
||||||
</div>
|
</div>
|
||||||
{#if media.relations?.edges?.filter(({ node }) => node.type === 'ANIME').length}
|
<ToggleList list={media.relations?.edges?.filter(({ node }) => node.type === 'ANIME')} let:item>
|
||||||
<span class="d-flex align-items-end pointer text-decoration-none mt-20 pt-20" on:click={toggleRelations}>
|
<div class="w-150 mx-15 mb-10 rel pointer" on:click={async () => { $view = null; $view = (await alRequest({ method: 'SearchIDSingle', id: item.node.id })).data.Media }}>
|
||||||
<h1 class="font-weight-bold text-white">Relations</h1>
|
<img loading="lazy" src={item.node.coverImage.medium || ''} alt="cover" class="cover-img w-full h-200 rel-img" />
|
||||||
<h6 class="ml-auto font-size-12 more text-muted">{showMoreRelations ? 'Show Less' : 'Show More'}</h6>
|
<div class="pt-5">{item.relationType.replace(/_/g, ' ').toLowerCase()}</div>
|
||||||
</span>
|
<h5 class="font-weight-bold text-white">{item.node.title.userPreferred}</h5>
|
||||||
<div class="d-flex text-capitalize flex-wrap pt-20 justify-center">
|
|
||||||
{#each media.relations?.edges.filter(({ node }) => node.type === 'ANIME').slice(0, showMoreRelations ? 100 : 4) as { relationType, node }}
|
|
||||||
<div class="w-150 mx-15 mb-10 rel pointer" on:click={async () => { $view = null; $view = (await alRequest({ method: 'SearchIDSingle', id: node.id })).data.Media }}>
|
|
||||||
<img loading="lazy" src={node.coverImage.medium || ''} alt="cover" class="cover-img w-full h-200 rel-img" />
|
|
||||||
<div class="pt-5">{relationType.replace(/_/g, ' ').toLowerCase()}</div>
|
|
||||||
<h5 class="font-weight-bold text-white">{node.title.userPreferred}</h5>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</ToggleList>
|
||||||
{#if maxPlayEp}
|
{#if maxPlayEp}
|
||||||
<table class="table table-hover w-500 table-auto">
|
<table class="table table-hover w-500 table-auto">
|
||||||
<thead>
|
<thead>
|
||||||
|
|
@ -283,60 +116,16 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{/if}
|
{/if}
|
||||||
{#if media.recommendations?.edges?.length}
|
<ToggleList list={media.recommendations.edges.filter(edge => edge.node.mediaRecommendation)} let:item>
|
||||||
<span class="d-flex align-items-end pointer text-decoration-none mt-20 pt-20" on:click={toggleRecommendations}>
|
<div class="w-150 mx-15 mb-10 rel pointer" on:click={async () => { $view = null; $view = (await alRequest({ method: 'SearchIDSingle', id: item.node.mediaRecommendation.id })).data.Media }}>
|
||||||
<h1 class="font-weight-bold text-white">Recommendations</h1>
|
<img loading="lazy" src={item.node.mediaRecommendation.coverImage.medium || ''} alt="cover" class="cover-img w-full h-200 rel-img" />
|
||||||
<h6 class="ml-auto font-size-12 more text-muted">{showMoreRecommendations ? 'Show Less' : 'Show More'}</h6>
|
<h5 class="font-weight-bold text-white">{item.node.mediaRecommendation.title.userPreferred}</h5>
|
||||||
</span>
|
|
||||||
<div class="d-flex text-capitalize flex-wrap pt-20 justify-center">
|
|
||||||
{#each media.recommendations.edges.filter(edge => edge.node.mediaRecommendation).slice(0, showMoreRecommendations ? 100 : 4) as { node }}
|
|
||||||
<div class="w-150 mx-15 mb-10 rel pointer" on:click={async () => { $view = null; $view = (await alRequest({ method: 'SearchIDSingle', id: node.mediaRecommendation.id })).data.Media }}>
|
|
||||||
<img loading="lazy" src={node.mediaRecommendation.coverImage.medium || ''} alt="cover" class="cover-img w-full h-200 rel-img" />
|
|
||||||
<h5 class="font-weight-bold text-white">{node.mediaRecommendation.title.userPreferred}</h5>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</ToggleList>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 px-sm-0 px-20">
|
<div class="col-md-3 px-sm-0 px-20">
|
||||||
<h1 class="title font-weight-bold text-white">Details</h1>
|
<Details {media}/>
|
||||||
<div class="card m-0 px-20 py-10 d-flex flex-md-column flex-row overflow-x-auto text-capitalize">
|
<Following {media}/>
|
||||||
{#each detailsMap as detail}
|
|
||||||
{@const property = getProperty(detail.property, media)}
|
|
||||||
{#if property}
|
|
||||||
<div class="d-flex flex-row px-10 py-5">
|
|
||||||
<div class={'mr-10 ' + (detail.custom === 'icon' ? 'd-flex align-items-center text-nowrap font-size-20 font-weight-bold' : 'material-icons font-size-24')}>
|
|
||||||
{detail.icon}
|
|
||||||
</div>
|
|
||||||
<div class="d-flex flex-column justify-content-center text-nowrap">
|
|
||||||
<div class="font-weight-bold">
|
|
||||||
{#if detail.custom === 'property'}
|
|
||||||
{@html getCustomProperty(detail, media)}
|
|
||||||
{:else if property.constructor === Array}
|
|
||||||
{property === 'nodes' ? property[0] && property[0].name : property.join(', ').replace(/_/g, ' ').toLowerCase()}
|
|
||||||
{:else}
|
|
||||||
{property.toString().replace(/_/g, ' ').toLowerCase()}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{#if following?.length}
|
|
||||||
<h2 class="font-weight-bold text-white mt-20">Following</h2>
|
|
||||||
<div class="card m-0 px-20 pt-15 pb-5 flex-column">
|
|
||||||
{#each following as friend}
|
|
||||||
<div class="d-flex align-items-center w-full pb-10 px-10">
|
|
||||||
<img src={friend.user.avatar.medium} alt="avatar" class="w-30 h-30 img-fluid rounded" />
|
|
||||||
<span class="my-0 pl-10 mr-auto text-truncate">{friend.user.name}</span>
|
|
||||||
<span class="my-0 px-10 text-capitalize">{statusMap[friend.status]}</span>
|
|
||||||
<span class="material-icons pointer text-primary font-size-18" on:click={() => window.IPC.emit('open', 'https://anilist.co/user/' + friend.user.name)}> open_in_new </span>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -345,34 +134,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.more:hover {
|
|
||||||
color: var(--dm-link-text-color-hover) !important;
|
|
||||||
}
|
|
||||||
.banner {
|
.banner {
|
||||||
background: no-repeat center center;
|
background: no-repeat center center;
|
||||||
background-size: cover;
|
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;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
select.form-control:invalid {
|
|
||||||
color: var(--dm-input-placeholder-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rel-img{
|
|
||||||
height: 27rem;
|
|
||||||
width: 17rem
|
|
||||||
}
|
|
||||||
|
|
||||||
.cover-img {
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rel {
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
|
||||||
.rel:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
.d-table-cell {
|
.d-table-cell {
|
||||||
display: table-cell !important;
|
display: table-cell !important;
|
||||||
}
|
}
|
||||||
|
|
@ -380,17 +147,6 @@
|
||||||
.top {
|
.top {
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
|
||||||
background-color: var(--dm-button-bg-color) !important;
|
|
||||||
background-image: var(--dm-button-bg-image) !important;
|
|
||||||
box-shadow: var(--dm-button-box-shadow) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-tp {
|
|
||||||
background-color: var(--dm-button-bg-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 4rem;
|
font-size: 4rem;
|
||||||
}
|
}
|
||||||
|
|
@ -406,10 +162,19 @@
|
||||||
border: none;
|
border: none;
|
||||||
margin-right: 0.6rem;
|
margin-right: 0.6rem;
|
||||||
}
|
}
|
||||||
.w-30 {
|
.rel-img{
|
||||||
width: 3rem
|
height: 27rem;
|
||||||
|
width: 17rem
|
||||||
}
|
}
|
||||||
.h-30 {
|
|
||||||
height: 3rem
|
.cover-img {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rel {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
.rel:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
164
src/renderer/src/lib/ViewAnime/Controls.svelte
Normal file
164
src/renderer/src/lib/ViewAnime/Controls.svelte
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
<script>
|
||||||
|
import { alToken } from '@/lib/pages/Settings.svelte'
|
||||||
|
import { addToast } from '../Toasts.svelte'
|
||||||
|
import { alRequest } from '@/modules/anilist.js'
|
||||||
|
import { getContext } from 'svelte'
|
||||||
|
import { getMediaMaxEp } from '@/modules/anime.js'
|
||||||
|
import { playAnime } from '../RSSView.svelte'
|
||||||
|
export let media = null
|
||||||
|
|
||||||
|
const toggleStatusMap = {
|
||||||
|
CURRENT: true,
|
||||||
|
COMPLETED: true,
|
||||||
|
PAUSED: true,
|
||||||
|
REPEATING: true
|
||||||
|
}
|
||||||
|
async function toggleStatus () {
|
||||||
|
if (media.mediaListEntry?.status !== 'PLANNING') {
|
||||||
|
// add
|
||||||
|
await setStatus((media.mediaListEntry?.status in toggleStatusMap) ? 'DROPPED' : 'CURRENT')
|
||||||
|
} else {
|
||||||
|
// delete
|
||||||
|
const variables = {
|
||||||
|
method: 'Delete',
|
||||||
|
id: media.mediaListEntry.id
|
||||||
|
}
|
||||||
|
await alRequest(variables)
|
||||||
|
}
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
function getStatusText () {
|
||||||
|
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 = {
|
||||||
|
method: 'Entry',
|
||||||
|
id: media.id,
|
||||||
|
status,
|
||||||
|
...other
|
||||||
|
}
|
||||||
|
return alRequest(variables)
|
||||||
|
}
|
||||||
|
async function update () {
|
||||||
|
media = (await alRequest({ method: 'SearchIDSingle', id: media.id })).data.Media
|
||||||
|
}
|
||||||
|
async function score (score) {
|
||||||
|
const variables = {
|
||||||
|
method: 'Entry',
|
||||||
|
id: media.id,
|
||||||
|
score: score * 10
|
||||||
|
}
|
||||||
|
await alRequest(variables)
|
||||||
|
media = (await alRequest({ method: '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) {
|
||||||
|
window.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), progress + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'Play'
|
||||||
|
}
|
||||||
|
async function play () {
|
||||||
|
let ep = 1
|
||||||
|
if (media.mediaListEntry) {
|
||||||
|
const { status, progress } = media.mediaListEntry
|
||||||
|
if (progress) {
|
||||||
|
if (status === 'COMPLETED') {
|
||||||
|
setStatus('REPEATING', { episode: 0 })
|
||||||
|
} else {
|
||||||
|
ep = Math.min(getMediaMaxEp(media, true), progress + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
playAnime(media, ep)
|
||||||
|
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 shadow-lg"
|
||||||
|
type="button"
|
||||||
|
on:click={() => play(media)}>
|
||||||
|
<span class="material-icons 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 shadow-lg" on:click={toggleStatus}>
|
||||||
|
<span class="material-icons 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-icons font-size-18">hotel_class</span>
|
||||||
|
</div>
|
||||||
|
<select class="form-control" required value={(media.mediaListEntry?.score || '').toString()} on:change={({ target }) => { score(media, 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 shadow-lg" on:click={() => viewTrailer(media)}>
|
||||||
|
<span class="material-icons 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 shadow-lg d-flex align-items-center" on:click={() => { openInBrowser(`https://anilist.co/anime/${media.id}`) }}>
|
||||||
|
<span class="material-icons 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 shadow-lg d-flex align-items-center" on:click={() => { copyToClipboard(`<miru://anime/${media.id}>`) }}>
|
||||||
|
<span class="material-icons 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>
|
||||||
73
src/renderer/src/lib/ViewAnime/Details.svelte
Normal file
73
src/renderer/src/lib/ViewAnime/Details.svelte
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
<script>
|
||||||
|
import { countdown } from '@/modules/util.js'
|
||||||
|
import { getMediaMaxEp } from '@/modules/anime.js'
|
||||||
|
export let media = null
|
||||||
|
|
||||||
|
const detailsMap = [
|
||||||
|
{ property: 'episode', label: 'Airing', icon: 'schedule', custom: 'property' },
|
||||||
|
{ property: 'genres', label: 'Genres', icon: 'theater_comedy' },
|
||||||
|
{ property: 'season', label: 'Season', icon: 'spa', custom: 'property' },
|
||||||
|
{ property: 'episodes', label: 'Episodes', icon: 'theaters', custom: 'property' },
|
||||||
|
{ property: 'duration', label: 'Duration', icon: 'timer', custom: 'property' },
|
||||||
|
{ property: 'format', label: 'Format', icon: 'monitor' },
|
||||||
|
{ property: 'status', label: 'Status', icon: 'live_tv' },
|
||||||
|
{ property: 'nodes', label: 'Studio', icon: 'business' },
|
||||||
|
{ property: 'source', label: 'Source', icon: 'source' },
|
||||||
|
{ property: 'averageScore', label: 'Rating', icon: 'trending_up', custom: 'property' },
|
||||||
|
{ property: 'english', label: 'English', icon: 'title' },
|
||||||
|
{ property: 'romaji', label: 'Romaji', icon: 'translate' },
|
||||||
|
{ property: 'native', label: 'Native', icon: '語', custom: 'icon' }
|
||||||
|
]
|
||||||
|
function getCustomProperty (detail, media) {
|
||||||
|
if (detail.property === 'episodes') {
|
||||||
|
if (media.mediaListEntry?.progress) {
|
||||||
|
return `Watched <b>${media.mediaListEntry.progress}</b> of <b>${getMediaMaxEp(media)}</b>`
|
||||||
|
}
|
||||||
|
return `${getMediaMaxEp(media)} Episodes`
|
||||||
|
} else if (detail.property === 'averageScore') {
|
||||||
|
return media.averageScore + '%'
|
||||||
|
} else if (detail.property === 'duration') {
|
||||||
|
return `${media.duration} minutes`
|
||||||
|
} else if (detail.property === 'season') {
|
||||||
|
return [media.season?.toLowerCase(), media.seasonYear].filter(f => f).join(' ')
|
||||||
|
} else if (detail.property === 'episode') {
|
||||||
|
return `Ep ${media.nextAiringEpisode.episode}: ${countdown(media.nextAiringEpisode.timeUntilAiring)}`
|
||||||
|
} else {
|
||||||
|
return media[detail.property]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getProperty (property, media) {
|
||||||
|
if (property === 'episode') {
|
||||||
|
return media.nextAiringEpisode?.episode
|
||||||
|
} else if (property === 'english' || property === 'romaji' || property === 'native') {
|
||||||
|
return media.title[property]
|
||||||
|
}
|
||||||
|
return media[property]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1 class="title font-weight-bold text-white">Details</h1>
|
||||||
|
<div class="card m-0 px-20 py-10 d-flex flex-md-column flex-row overflow-x-auto text-capitalize">
|
||||||
|
{#each detailsMap as detail}
|
||||||
|
{@const property = getProperty(detail.property, media)}
|
||||||
|
{#if property}
|
||||||
|
<div class="d-flex flex-row px-10 py-5">
|
||||||
|
<div class={'mr-10 ' + (detail.custom === 'icon' ? 'd-flex align-items-center text-nowrap font-size-20 font-weight-bold' : 'material-icons font-size-24')}>
|
||||||
|
{detail.icon}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-column justify-content-center text-nowrap">
|
||||||
|
<div class="font-weight-bold">
|
||||||
|
{#if detail.custom === 'property'}
|
||||||
|
{@html getCustomProperty(detail, media)}
|
||||||
|
{:else if property.constructor === Array}
|
||||||
|
{property === 'nodes' ? property[0] && property[0].name : property.join(', ').replace(/_/g, ' ').toLowerCase()}
|
||||||
|
{:else}
|
||||||
|
{property.toString().replace(/_/g, ' ').toLowerCase()}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
54
src/renderer/src/lib/ViewAnime/Following.svelte
Normal file
54
src/renderer/src/lib/ViewAnime/Following.svelte
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<script>
|
||||||
|
import { alToken } from '@/lib/pages/Settings.svelte'
|
||||||
|
import { alRequest } from '@/modules/anilist.js'
|
||||||
|
export let media = null
|
||||||
|
let following = null
|
||||||
|
async function updateFollowing (media) {
|
||||||
|
if (media) {
|
||||||
|
following = null
|
||||||
|
following = (await alRequest({ method: 'Following', id: media.id })).data?.Page?.mediaList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$: updateFollowing(media)
|
||||||
|
const statusMap = {
|
||||||
|
CURRENT: 'Watching',
|
||||||
|
PLANNING: 'Planning',
|
||||||
|
COMPLETED: 'Completed',
|
||||||
|
DROPPED: 'Dropped',
|
||||||
|
PAUSED: 'Paused',
|
||||||
|
REPEATING: 'Repeating'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
{#if following?.length && alToken}
|
||||||
|
<h2 class="font-weight-bold text-white mt-20">Following</h2>
|
||||||
|
<div class="card m-0 px-20 pt-15 pb-5 flex-column">
|
||||||
|
{#each following as friend}
|
||||||
|
<div class="d-flex align-items-center w-full pb-10 px-10">
|
||||||
|
<img src={friend.user.avatar.medium} alt="avatar" class="w-30 h-30 img-fluid rounded cover-img" />
|
||||||
|
<span class="my-0 pl-10 mr-auto text-truncate">{friend.user.name}</span>
|
||||||
|
<span class="my-0 px-10 text-capitalize">{statusMap[friend.status]}</span>
|
||||||
|
<span class="material-icons pointer text-primary font-size-18" on:click={() => window.IPC.emit('open', 'https://anilist.co/user/' + friend.user.name)}> open_in_new </span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.h-30 {
|
||||||
|
height: 3rem
|
||||||
|
}
|
||||||
|
.w-30 {
|
||||||
|
width: 3rem
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background-color: var(--dm-button-bg-color) !important;
|
||||||
|
background-image: var(--dm-button-bg-image) !important;
|
||||||
|
box-shadow: var(--dm-button-box-shadow) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-img {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
24
src/renderer/src/lib/ViewAnime/ToggleList.svelte
Normal file
24
src/renderer/src/lib/ViewAnime/ToggleList.svelte
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script>
|
||||||
|
export let list = null
|
||||||
|
let showMore = false
|
||||||
|
function toggleList () {
|
||||||
|
showMore = !showMore
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{#if list?.length}
|
||||||
|
<span class="d-flex align-items-end pointer text-decoration-none mt-20 pt-20" on:click={toggleList}>
|
||||||
|
<h1 class="font-weight-bold text-white">Relations</h1>
|
||||||
|
<h6 class="ml-auto font-size-12 more text-muted">{showMore ? 'Show Less' : 'Show More'}</h6>
|
||||||
|
</span>
|
||||||
|
<div class="d-flex text-capitalize flex-wrap pt-20 justify-center">
|
||||||
|
{#each list.slice(0, showMore ? 100 : 4) as item}
|
||||||
|
<slot {item} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.more:hover {
|
||||||
|
color: var(--dm-link-text-color-hover) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -111,7 +111,7 @@ export function alEntry (filemedia) {
|
||||||
}
|
}
|
||||||
if (videoEpisode === mediaEpisode) {
|
if (videoEpisode === mediaEpisode) {
|
||||||
variables.status = 'COMPLETED'
|
variables.status = 'COMPLETED'
|
||||||
if (media.mediaListEntry?.status === 'COMPLETED') variables.repeat = media.mediaListEntry.repeat + 1
|
if (media.mediaListEntry?.status === 'COMPLETED' || media.mediaListEntry.status === 'REPEATING') variables.repeat = media.mediaListEntry.repeat + 1
|
||||||
}
|
}
|
||||||
alRequest(variables)
|
alRequest(variables)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue