mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-05-03 17:09:09 +00:00
feat: viewAnime improvements
added recommendations added friends anime status added an option to show more/less relations and recommendations improved reliability of quitting using ESC improved autofocus on title search on home page removed some unused CSS
This commit is contained in:
parent
ee530a5d9a
commit
27ac5cf0e8
5 changed files with 141 additions and 100 deletions
|
|
@ -12,10 +12,29 @@
|
||||||
$view = null
|
$view = null
|
||||||
}
|
}
|
||||||
$: media = $view
|
$: media = $view
|
||||||
|
let modal
|
||||||
|
$: media && 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 = [
|
const detailsMap = [
|
||||||
{ property: 'episode', label: 'Airing', icon: 'schedule', custom: 'property' },
|
{ property: 'episode', label: 'Airing', icon: 'schedule', custom: 'property' },
|
||||||
{ property: 'genres', label: 'Genres', icon: 'theater_comedy' },
|
{ property: 'genres', label: 'Genres', icon: 'theater_comedy' },
|
||||||
|
|
@ -93,9 +112,17 @@
|
||||||
function openInBrowser (url) {
|
function openInBrowser (url) {
|
||||||
window.IPC.emit('open', 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">
|
<div class="modal modal-full" class:show={media} on:keydown={checkClose} tabindex="-1" bind:this={modal}>
|
||||||
{#if media}
|
{#if media}
|
||||||
<div class="h-full modal-content bg-very-dark p-0 overflow-y-auto">
|
<div class="h-full modal-content bg-very-dark p-0 overflow-y-auto">
|
||||||
<button class="close pointer z-30 bg-dark shadow-lg top-20 right-0" type="button" on:click={close}> × </button>
|
<button class="close pointer z-30 bg-dark shadow-lg top-20 right-0" type="button" on:click={close}> × </button>
|
||||||
|
|
@ -213,13 +240,17 @@
|
||||||
<div class="container-xl bg-very-dark z-10">
|
<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="row p-20 px-xl-0 flex-column-reverse flex-md-row">
|
||||||
<div class="col-md-9 px-20">
|
<div class="col-md-9 px-20">
|
||||||
<h1 class="title font-weight-bold text-white">Sypnosis</h1>
|
<h1 class="title font-weight-bold text-white">Synopsis</h1>
|
||||||
<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}
|
{#if media.relations?.edges?.filter(({ node }) => node.type === 'ANIME').length}
|
||||||
|
<span class="d-flex align-items-end pointer text-decoration-none mt-20 pt-20" on:click={toggleRelations}>
|
||||||
|
<h1 class="font-weight-bold text-white">Relations</h1>
|
||||||
|
<h6 class="ml-auto font-size-12 more text-muted">{showMoreRelations ? 'Show Less' : 'Show More'}</h6>
|
||||||
|
</span>
|
||||||
<div class="d-flex text-capitalize flex-wrap pt-20 justify-center">
|
<div class="d-flex text-capitalize flex-wrap pt-20 justify-center">
|
||||||
{#each media.relations?.edges.filter(({ node }) => node.type === 'ANIME') as { relationType, node }}
|
{#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 }}>
|
<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" />
|
<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>
|
<div class="pt-5">{relationType.replace(/_/g, ' ').toLowerCase()}</div>
|
||||||
|
|
@ -251,10 +282,24 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if media.recommendations?.edges?.length}
|
||||||
|
<span class="d-flex align-items-end pointer text-decoration-none mt-20 pt-20" on:click={toggleRecommendations}>
|
||||||
|
<h1 class="font-weight-bold text-white">Recommendations</h1>
|
||||||
|
<h6 class="ml-auto font-size-12 more text-muted">{showMoreRecommendations ? 'Show Less' : 'Show More'}</h6>
|
||||||
|
</span>
|
||||||
|
<div class="d-flex text-capitalize flex-wrap pt-20 justify-center">
|
||||||
|
{#each media.recommendations.edges.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>
|
||||||
|
{/if}
|
||||||
</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>
|
<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" id="viewDetails">
|
<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}
|
{#each detailsMap as detail}
|
||||||
{@const property = getProperty(detail.property, media)}
|
{@const property = getProperty(detail.property, media)}
|
||||||
{#if property}
|
{#if property}
|
||||||
|
|
@ -278,6 +323,19 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</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>
|
||||||
|
|
@ -286,6 +344,9 @@
|
||||||
</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;
|
||||||
|
|
@ -344,4 +405,10 @@
|
||||||
border: none;
|
border: none;
|
||||||
margin-right: 0.6rem;
|
margin-right: 0.6rem;
|
||||||
}
|
}
|
||||||
|
.w-30 {
|
||||||
|
width: 3rem
|
||||||
|
}
|
||||||
|
.h-30 {
|
||||||
|
height: 3rem
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,17 @@
|
||||||
import { getContext } from 'svelte'
|
import { getContext } from 'svelte'
|
||||||
const url = getContext('trailer')
|
const url = getContext('trailer')
|
||||||
|
|
||||||
function close() {
|
let modal
|
||||||
|
function close () {
|
||||||
$url = null
|
$url = null
|
||||||
}
|
}
|
||||||
function checkClose({ keyCode }) {
|
function checkClose ({ keyCode }) {
|
||||||
if (keyCode == 27) close()
|
if (keyCode === 27) close()
|
||||||
}
|
}
|
||||||
|
$: $url && modal?.focus()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="modal" class:show={$url} on:keydown={checkClose} tabindex="-1">
|
<div class="modal" class:show={$url} on:keydown={checkClose} tabindex="-1" bind:this={modal}>
|
||||||
{#if $url}
|
{#if $url}
|
||||||
<div class="modal-dialog" role="document" on:click|self={close}>
|
<div class="modal-dialog" role="document" on:click|self={close}>
|
||||||
<div class="modal-content w-three-quarter h-full bg-transparent d-flex justify-content-center flex-column">
|
<div class="modal-content w-three-quarter h-full bg-transparent d-flex justify-content-center flex-column">
|
||||||
|
|
|
||||||
|
|
@ -1428,95 +1428,4 @@
|
||||||
filter: invert(0.84);
|
filter: invert(0.84);
|
||||||
padding-top: 2rem;
|
padding-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Radio debloat for halfmoon */
|
|
||||||
.custom-radio {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.custom-radio label {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.custom-radio input[type='radio']:hover + label:before {
|
|
||||||
background-color: var(--lm-radio-bg-color-hover);
|
|
||||||
border-color: var(--lm-radio-border-color-hover);
|
|
||||||
}
|
|
||||||
.custom-radio input[type='radio']:focus + label:before {
|
|
||||||
border-color: var(--lm-radio-border-color-focus);
|
|
||||||
-moz-box-shadow: var(--lm-radio-box-shadow-focus);
|
|
||||||
-webkit-box-shadow: var(--lm-radio-box-shadow-focus);
|
|
||||||
box-shadow: var(--lm-radio-box-shadow-focus);
|
|
||||||
}
|
|
||||||
.custom-radio input[type='radio']:checked + label:before {
|
|
||||||
background-color: var(--lm-radio-bg-color-checked);
|
|
||||||
border-color: var(--lm-radio-border-color-checked);
|
|
||||||
}
|
|
||||||
.custom-radio input[type='radio']:checked:focus + label:before {
|
|
||||||
border-color: var(--lm-radio-border-color-checked-focus);
|
|
||||||
-moz-box-shadow: var(--lm-radio-box-shadow-checked-focus);
|
|
||||||
-webkit-box-shadow: var(--lm-radio-box-shadow-checked-focus);
|
|
||||||
box-shadow: var(--lm-radio-box-shadow-checked-focus);
|
|
||||||
}
|
|
||||||
.dark-mode .custom-radio label:before {
|
|
||||||
background-color: var(--dm-radio-bg-color);
|
|
||||||
border-color: var(--dm-radio-border-color);
|
|
||||||
}
|
|
||||||
.dark-mode .custom-radio input[type='radio']:hover + label:before {
|
|
||||||
background-color: var(--dm-radio-bg-color-hover);
|
|
||||||
border-color: var(--dm-radio-border-color-hover);
|
|
||||||
}
|
|
||||||
.dark-mode .custom-radio input[type='radio']:focus + label:before {
|
|
||||||
border-color: var(--dm-radio-border-color-focus);
|
|
||||||
-moz-box-shadow: var(--dm-radio-box-shadow-focus);
|
|
||||||
-webkit-box-shadow: var(--dm-radio-box-shadow-focus);
|
|
||||||
box-shadow: var(--dm-radio-box-shadow-focus);
|
|
||||||
}
|
|
||||||
.dark-mode .custom-radio input[type='radio']:checked + label:before {
|
|
||||||
background-color: var(--dm-radio-bg-color-checked);
|
|
||||||
border-color: var(--dm-radio-border-color-checked);
|
|
||||||
}
|
|
||||||
.dark-mode .custom-radio input[type='radio']:checked:focus + label:before {
|
|
||||||
border-color: var(--dm-radio-border-color-checked-focus);
|
|
||||||
-moz-box-shadow: var(--dm-radio-box-shadow-checked-focus);
|
|
||||||
-webkit-box-shadow: var(--dm-radio-box-shadow-checked-focus);
|
|
||||||
box-shadow: var(--dm-radio-box-shadow-checked-focus);
|
|
||||||
}
|
|
||||||
.custom-radio label:after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
display: none;
|
|
||||||
top: var(--radio-checkmark-top);
|
|
||||||
left: var(--radio-checkmark-left);
|
|
||||||
width: var(--radio-checkmark-width-height);
|
|
||||||
height: var(--radio-checkmark-width-height);
|
|
||||||
background-color: var(--lm-radio-checkmark-color);
|
|
||||||
border-radius: var(--radio-checkmark-border-radius);
|
|
||||||
}
|
|
||||||
.custom-radio input[type='radio']:checked + label:after {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.custom-radio input[type='radio']:disabled + label {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.custom-radio input[type='radio']:disabled + label:before,
|
|
||||||
.custom-radio input[type='radio']:hover:disabled + label:before {
|
|
||||||
background-color: var(--lm-radio-bg-color);
|
|
||||||
border-color: var(--lm-radio-border-color);
|
|
||||||
}
|
|
||||||
.custom-radio input[type='radio']:disabled:checked + label:before,
|
|
||||||
.custom-radio input[type='radio']:hover:disabled:checked + label:before {
|
|
||||||
background-color: var(--lm-radio-bg-color-checked);
|
|
||||||
border-color: var(--lm-radio-border-color-checked);
|
|
||||||
}
|
|
||||||
.dark-mode .custom-radio input[type='radio']:disabled + label:before,
|
|
||||||
.dark-mode .custom-radio input[type='radio']:hover:disabled + label:before {
|
|
||||||
background-color: var(--dm-radio-bg-color);
|
|
||||||
border-color: var(--dm-radio-border-color);
|
|
||||||
}
|
|
||||||
.dark-mode .custom-radio input[type='radio']:disabled:checked + label:before,
|
|
||||||
.dark-mode .custom-radio input[type='radio']:hover:disabled:checked + label:before {
|
|
||||||
background-color: var(--dm-radio-bg-color-checked);
|
|
||||||
border-color: var(--dm-radio-border-color-checked);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { getContext } from 'svelte'
|
||||||
|
|
||||||
export let search
|
export let search
|
||||||
export let current
|
export let current
|
||||||
export let media = null
|
export let media = null
|
||||||
export let loadCurrent
|
export let loadCurrent
|
||||||
let searchTimeout = null
|
let searchTimeout = null
|
||||||
|
let searchTextInput
|
||||||
|
|
||||||
|
const view = getContext('view')
|
||||||
|
|
||||||
|
$: !$view && searchTextInput?.focus()
|
||||||
function searchClear () {
|
function searchClear () {
|
||||||
search = {
|
search = {
|
||||||
format: '',
|
format: '',
|
||||||
|
|
@ -14,6 +20,7 @@
|
||||||
status: ''
|
status: ''
|
||||||
}
|
}
|
||||||
current = null
|
current = null
|
||||||
|
searchTextInput?.focus()
|
||||||
}
|
}
|
||||||
function input () {
|
function input () {
|
||||||
if (!searchTimeout) {
|
if (!searchTimeout) {
|
||||||
|
|
@ -47,6 +54,7 @@
|
||||||
<span class="input-group-text d-flex material-icons bg-dark pr-0 font-size-18">search</span>
|
<span class="input-group-text d-flex material-icons bg-dark pr-0 font-size-18">search</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- svelte-ignore missing-declaration -->
|
<!-- svelte-ignore missing-declaration -->
|
||||||
|
<!-- svelte-ignore a11y-autofocus -->
|
||||||
<input
|
<input
|
||||||
on:input={({ target }) => {
|
on:input={({ target }) => {
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
|
|
@ -54,6 +62,8 @@
|
||||||
input()
|
input()
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
bind:this={searchTextInput}
|
||||||
|
autofocus
|
||||||
type="search"
|
type="search"
|
||||||
class="form-control bg-dark border-left-0 shadow-none text-capitalize"
|
class="form-control bg-dark border-left-0 shadow-none text-capitalize"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
|
@ -102,7 +112,13 @@
|
||||||
<option value="SUMMER">Summer</option>
|
<option value="SUMMER">Summer</option>
|
||||||
<option value="FALL">Fall</option>
|
<option value="FALL">Fall</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="number" placeholder="Any" min="1940" max="2100" class="form-control bg-dark shadow-none" bind:value={search.year} />
|
<input type="number" placeholder="Any" min="1940" max="2100" list="search-year" class="form-control bg-dark shadow-none" bind:value={search.year} />
|
||||||
|
<datalist id="search-year">
|
||||||
|
{#each Array(new Date().getFullYear() - 1940 + 2) as _, i}
|
||||||
|
{@const year = new Date().getFullYear() + 2 - i}
|
||||||
|
<option>{year}</option>
|
||||||
|
{/each}
|
||||||
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col p-10 d-flex flex-column justify-content-end">
|
<div class="col p-10 d-flex flex-column justify-content-end">
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,21 @@ relations {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
recommendations {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
mediaRecommendation {
|
||||||
|
id,
|
||||||
|
title {
|
||||||
|
userPreferred
|
||||||
|
},
|
||||||
|
coverImage {
|
||||||
|
medium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}`
|
}`
|
||||||
if (opts.status) variables.status = opts.status
|
if (opts.status) variables.status = opts.status
|
||||||
if (localStorage.getItem('ALtoken')) options.headers.Authorization = alToken
|
if (localStorage.getItem('ALtoken')) options.headers.Authorization = alToken
|
||||||
|
|
@ -366,6 +381,38 @@ query ($page: Int, $perPage: Int, $sort: [MediaSort], $type: MediaType, $search:
|
||||||
deleted
|
deleted
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
break
|
||||||
|
} case 'Following': {
|
||||||
|
variables.id = opts.id
|
||||||
|
query = /* js */`
|
||||||
|
query($id: Int) {
|
||||||
|
Page {
|
||||||
|
pageInfo {
|
||||||
|
total,
|
||||||
|
perPage,
|
||||||
|
currentPage,
|
||||||
|
lastPage,
|
||||||
|
hasNextPage
|
||||||
|
},
|
||||||
|
mediaList(mediaId: $id, isFollowing: true, sort: UPDATED_TIME_DESC) {
|
||||||
|
id,
|
||||||
|
status,
|
||||||
|
score,
|
||||||
|
progress,
|
||||||
|
user {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
avatar {
|
||||||
|
medium
|
||||||
|
},
|
||||||
|
mediaListOptions {
|
||||||
|
scoreFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
options.body = JSON.stringify({
|
options.body = JSON.stringify({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue