mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-01-12 00:03:44 +00:00
feat: colored anime pages, clickable genres, and meta
fix: better color contrast text calculation fix: bad bg color for spoilers
This commit is contained in:
parent
fde2528df1
commit
4206bef69b
14 changed files with 104 additions and 76 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ui",
|
||||
"version": "6.4.93",
|
||||
"version": "6.4.94",
|
||||
"license": "BUSL-1.1",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.15.5",
|
||||
|
|
|
|||
17
src/app.css
17
src/app.css
|
|
@ -48,6 +48,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
.text-contrast {
|
||||
--accessible-color: calc(((((var(--red) * 299) + (var(--green) * 587) + (var(--blue) * 114)) / 1000) - 128) * -1000);
|
||||
|
||||
color: rgb(var(--accessible-color),
|
||||
var(--accessible-color),
|
||||
var(--accessible-color));
|
||||
fill: rgb(var(--accessible-color),
|
||||
var(--accessible-color),
|
||||
var(--accessible-color));
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
|
|
@ -79,6 +90,8 @@
|
|||
--ring: 240 10% 3.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
--custom: #fff;
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
|
@ -110,6 +123,8 @@
|
|||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--ring: 240 4.9% 83.9%;
|
||||
|
||||
--custom: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -246,7 +261,7 @@ details,
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.text-contrast {
|
||||
.text-contrast-filter {
|
||||
filter: invert(1) grayscale(1) brightness(1.2) contrast(9000);
|
||||
mix-blend-mode: luminosity;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
|
||||
<Dialog.Root portal='#root'>
|
||||
<Dialog.Trigger let:builder asChild>
|
||||
<Button size='icon' class='rounded-l-none bg-primary/85 select:bg-primary/75 shrink-0' builders={[builder]}>
|
||||
<Button size='icon' class='rounded-l-none bg-custom-400 select:!bg-custom-700 shrink-0 text-contrast' builders={[builder]}>
|
||||
<PencilLine class='size-4' />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@
|
|||
<div use:click={() => play(episode)}
|
||||
class={cn(
|
||||
'select:scale-[1.05] select:shadow-lg scale-100 transition-[transform,box-shadow] duration-200 shrink-0 ease-out focus-visible:ring-ring focus-visible:ring-1 rounded-md bg-neutral-950 text-secondary-foreground select:bg-neutral-900 flex w-full max-h-28 cursor-pointer relative overflow-hidden group',
|
||||
target && 'ring-ring ring-1',
|
||||
target && 'ring-custom ring-1',
|
||||
filler && '!ring-yellow-400 ring-1'
|
||||
)}>
|
||||
{#if image}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
background: #0f0f0f;
|
||||
background: #0003;
|
||||
display: inline-block;
|
||||
padding: 0.4em 0.8em;
|
||||
border-radius: 0.5em;
|
||||
|
|
|
|||
|
|
@ -40,10 +40,10 @@
|
|||
{/if}
|
||||
</div>
|
||||
<div class='w-full px-4 bg-neutral-950'>
|
||||
<div class='text-lg font-bold truncate inline-block w-full text-white' title={title(media)}>
|
||||
<div class='text-lg font-bold truncate inline-block w-full text-white pt-2' title={title(media)}>
|
||||
{title(media)}
|
||||
</div>
|
||||
<div class='flex flex-row pt-1'>
|
||||
<div class='flex flex-row'>
|
||||
<PlayButton {media} class='grow' />
|
||||
<FavoriteButton {media} class='ml-2' />
|
||||
<BookmarkButton {media} class='ml-2' />
|
||||
|
|
|
|||
|
|
@ -315,16 +315,13 @@
|
|||
}
|
||||
|
||||
let completed = false
|
||||
function checkCompletion () {
|
||||
async function checkCompletion () {
|
||||
if (!completed && $settings.playerAutocomplete) {
|
||||
checkCompletionByTime(currentTime, safeduration)
|
||||
}
|
||||
}
|
||||
function checkCompletionByTime (currentTime: number, safeduration: number) {
|
||||
const fromend = Math.max(180, safeduration / 10)
|
||||
if (safeduration && currentTime && readyState && safeduration - fromend < currentTime) {
|
||||
authAggregator.watch(mediaInfo.media, mediaInfo.episode)
|
||||
completed = true
|
||||
const fromend = Math.max(180, safeduration / 10)
|
||||
if (safeduration && currentTime && readyState && safeduration - fromend < currentTime) {
|
||||
authAggregator.watch(mediaInfo.media, mediaInfo.episode)
|
||||
completed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
{#if bubble && bubble !== 'Donator'}
|
||||
<div class='-left-5 -top-11 absolute text-sm'>
|
||||
<div class='px-4 py-2 rounded-2xl bg-mix bubbles relative leading-tight'>
|
||||
<span class='text-contrast'>
|
||||
<span class='text-contrast-filter'>
|
||||
{bubble}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@
|
|||
Episodes {entry.episodes}
|
||||
</div>
|
||||
{#if entry.videos?.length}
|
||||
<Button size='icon-sm' class='ml-auto font-bold rounded-full' on:click={() => playVideo(url)}><Play fill='currentColor' size={iconSizes['icon-sm']} /></Button>
|
||||
<Button size='icon-sm' class='ml-auto font-bold rounded-full bg-custom select:!bg-custom-600 text-contrast' on:click={() => playVideo(url)}><Play fill='currentColor' size={iconSizes['icon-sm']} /></Button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if src === url}
|
||||
|
|
|
|||
|
|
@ -115,12 +115,15 @@ export default new class AuthAggregator {
|
|||
return null
|
||||
})
|
||||
|
||||
watch (media: Media, progress: number) {
|
||||
async watch (outdated: Media, progress: number) {
|
||||
const media = (await client.single(outdated.id)).data?.Media ?? outdated
|
||||
// TODO: auto re-watch status
|
||||
const totalEps = episodes(media) ?? 1 // episodes or movie which is single episode
|
||||
if (totalEps < progress) return // woah, bad data from resolver?!
|
||||
|
||||
const currentProgress = media.mediaListEntry?.progress ?? 0
|
||||
const mediaList = this.mediaListEntry(media)
|
||||
|
||||
const currentProgress = mediaList?.progress ?? 0
|
||||
if (currentProgress >= progress) return
|
||||
|
||||
// there's an edge case here that episodes returns 1, because anilist doesn't have episode count for an airing show without an expected end date
|
||||
|
|
@ -130,11 +133,11 @@ export default new class AuthAggregator {
|
|||
const status =
|
||||
totalEps === progress && canBeCompleted
|
||||
? 'COMPLETED'
|
||||
: media.mediaListEntry?.status === 'REPEATING' ? 'REPEATING' : 'CURRENT'
|
||||
: mediaList?.status === 'REPEATING' ? 'REPEATING' : 'CURRENT'
|
||||
|
||||
const lists = (media.mediaListEntry?.customLists as Array<{enabled: boolean, name: string}> | undefined)?.filter(({ enabled }) => enabled).map(({ name }) => name) ?? []
|
||||
const lists = (mediaList?.customLists as Array<{enabled: boolean, name: string}> | undefined)?.filter(({ enabled }) => enabled).map(({ name }) => name) ?? []
|
||||
|
||||
this.entry({ id: media.id, progress, status, lists })
|
||||
return await this.entry({ id: media.id, progress, status, lists })
|
||||
}
|
||||
|
||||
delete (media: Media) {
|
||||
|
|
|
|||
|
|
@ -308,3 +308,11 @@ export function arrayEqual <T> (a: T[], b: T[]) {
|
|||
export function nextTick () {
|
||||
return new Promise<void>(resolve => queueMicrotask(resolve))
|
||||
}
|
||||
|
||||
export function colors (hex = '#ffffff') {
|
||||
const bigint = parseInt(hex.slice(1), 16)
|
||||
const r = (bigint >> 16) & 255
|
||||
const g = (bigint >> 8) & 255
|
||||
const b = bigint & 255
|
||||
return { r, g, b }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang='ts'>
|
||||
import Bell from 'lucide-svelte/icons/bell'
|
||||
// import Bell from 'lucide-svelte/icons/bell'
|
||||
import Clapperboard from 'lucide-svelte/icons/clapperboard'
|
||||
import Maximize2 from 'lucide-svelte/icons/maximize-2'
|
||||
import Share2 from 'lucide-svelte/icons/share-2'
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
import type { LayoutData } from './$types'
|
||||
|
||||
import { onNavigate } from '$app/navigation'
|
||||
import { goto, onNavigate } from '$app/navigation'
|
||||
import Anilist from '$lib/components/icons/Anilist.svelte'
|
||||
import MyAnimeList from '$lib/components/icons/MyAnimeList.svelte'
|
||||
import { bannerSrc, hideBanner } from '$lib/components/ui/banner'
|
||||
|
|
@ -22,6 +22,7 @@
|
|||
import { authAggregator, of } from '$lib/modules/auth'
|
||||
import native from '$lib/modules/native'
|
||||
import { dragScroll } from '$lib/modules/navigate'
|
||||
import { colors } from '$lib/utils'
|
||||
|
||||
export let data: LayoutData
|
||||
|
||||
|
|
@ -51,9 +52,9 @@
|
|||
}
|
||||
|
||||
function getColorForRating (rating: number) {
|
||||
if (rating >= 75) return 'bg-green-700'
|
||||
if (rating >= 65) return 'bg-orange-400'
|
||||
return 'bg-red-400'
|
||||
if (rating >= 75) return 'bg-green-700 select:bg-green-800'
|
||||
if (rating >= 65) return 'bg-orange-400 select:bg-orange-500'
|
||||
return 'bg-red-400 select:bg-red-500'
|
||||
}
|
||||
|
||||
$: mediaId = media.id
|
||||
|
|
@ -71,9 +72,11 @@
|
|||
behavior: 'smooth'
|
||||
})
|
||||
})
|
||||
|
||||
$: ({ r, g, b } = colors(media.coverImage?.color ?? undefined))
|
||||
</script>
|
||||
|
||||
<div class='min-w-0 -ml-14 pl-14 grow items-center flex flex-col h-full overflow-y-auto z-10 pointer-events-none pb-10' use:dragScroll on:scroll={handleScroll} bind:this={container}>
|
||||
<div class='min-w-0 -ml-14 pl-14 grow items-center flex flex-col h-full overflow-y-auto z-10 pointer-events-none pb-10' use:dragScroll on:scroll={handleScroll} bind:this={container} style:--custom={media.coverImage?.color ?? '#fff'} style:--red={r} style:--green={g} style:--blue={b}>
|
||||
<div class='gap-6 w-full pt-4 md:pt-32 flex flex-col items-center justify-center max-w-[1600px] px-3 xl:px-14 pointer-events-auto'>
|
||||
<div class='flex flex-col md:flex-row w-full items-center md:items-end gap-5 pt-12'>
|
||||
<Dialog.Root portal='#root'>
|
||||
|
|
@ -92,34 +95,22 @@
|
|||
<h2 class='line-clamp-1 text-base md:text-lg font-light text-muted-foreground select-text'>{media.title?.romaji?.toLowerCase().trim() === title(media).toLowerCase().trim() ? nativeTitle : romajiTitle}</h2>
|
||||
<h1 class='font-black text-3xl md:text-4xl line-clamp-2 text-white select-text'>{title(media)}</h1>
|
||||
<div class='flex-wrap w-full justify-start md:pt-1 gap-4 hidden md:flex'>
|
||||
<div class='rounded px-3.5 font-bold' style:background={media.coverImage?.color ?? '#27272a'}>
|
||||
<div class='text-contrast'>
|
||||
{of(media) ?? duration(media) ?? 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
<div class='rounded px-3.5 font-bold' style:background={media.coverImage?.color ?? '#27272a'}>
|
||||
<div class='text-contrast'>
|
||||
{format(media)}
|
||||
</div>
|
||||
</div>
|
||||
<div class='rounded px-3.5 font-bold' style:background={media.coverImage?.color ?? '#27272a'}>
|
||||
<div class='text-contrast'>
|
||||
{status(media)}
|
||||
</div>
|
||||
<div class='rounded px-3.5 font-bold bg-custom text-contrast'>
|
||||
{of(media) ?? duration(media) ?? 'N/A'}
|
||||
</div>
|
||||
<Button class='rounded px-3.5 font-bold bg-custom select:!bg-custom-600 text-contrast h-6 py-0 text-base' on:click={() => goto('/app/search', { state: { search: { format: [media.format] } } })}>
|
||||
{format(media)} </Button>
|
||||
<Button class='rounded px-3.5 font-bold bg-custom select:!bg-custom-600 text-contrast h-6 py-0 text-base' on:click={() => goto('/app/search', { state: { search: { status: [media.status] } } })}>
|
||||
{status(media)} </Button>
|
||||
{#if season(media)}
|
||||
<div class='rounded px-3.5 font-bold' style:background={media.coverImage?.color ?? '#27272a'}>
|
||||
<div class='text-contrast capitalize'>
|
||||
{season(media)}
|
||||
</div>
|
||||
</div>
|
||||
<Button class='rounded px-3.5 font-bold bg-custom select:!bg-custom-600 text-contrast h-6 py-0 text-base capitalize' on:click={() => goto('/app/search', { state: { search: { season: media.season, seasonYear: media.seasonYear } } })}>
|
||||
{season(media)}
|
||||
</Button>
|
||||
{/if}
|
||||
{#if media.averageScore}
|
||||
<div class='rounded px-3.5 font-bold {getColorForRating(media.averageScore)}'>
|
||||
<div class='text-contrast'>
|
||||
{media.averageScore}%
|
||||
</div>
|
||||
</div>
|
||||
<Button class='rounded px-3.5 font-bold text-contrast h-6 py-0 text-base {getColorForRating(media.averageScore)}' on:click={() => goto('/app/search', { state: { search: { sort: ['SCORE_DESC'] } } })}>
|
||||
{media.averageScore}%
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class='md:block hidden relative pb-6 md:pt-2 md:pb-0'>
|
||||
|
|
@ -130,26 +121,18 @@
|
|||
</div>
|
||||
<div class='flex gap-2 items-center justify-center md:justify-start md:self-start w-full overflow-x-clip [&>*]:flex-shrink-0'>
|
||||
<div class='flex md:mr-3 w-full min-[380px]:w-[180px]'>
|
||||
<PlayButton size='default' {media} class='rounded-r-none w-full' />
|
||||
<PlayButton size='default' {media} class='rounded-r-none w-full bg-custom select:!bg-custom-600 text-contrast' />
|
||||
<EntryEditor {media} />
|
||||
</div>
|
||||
<FavoriteButton {media} variant='secondary' size='icon' class='min-[380px]:-order-1 md:order-none' />
|
||||
<BookmarkButton {media} variant='secondary' size='icon' class='min-[380px]:-order-2 md:order-none' />
|
||||
<Button size='icon' variant='secondary' on:click={share}>
|
||||
<FavoriteButton {media} variant='secondary' size='icon' class='min-[380px]:-order-1 md:order-none select:!text-custom' />
|
||||
<BookmarkButton {media} variant='secondary' size='icon' class='min-[380px]:-order-2 md:order-none select:!text-custom' />
|
||||
<Button size='icon' variant='secondary' on:click={share} class='select:!text-custom'>
|
||||
<Share2 class='size-4' />
|
||||
</Button>
|
||||
<Button size='icon' variant='secondary' class='hidden md:flex' on:click={() => native.openURL(`https://anilist.co/anime/${media.id}`)}>
|
||||
<Anilist class='size-4' />
|
||||
</Button>
|
||||
{#if media.idMal}
|
||||
<Button size='icon' variant='secondary' class='hidden md:flex' on:click={() => native.openURL(`https://myanimelist.net/anime/${media.idMal}`)}>
|
||||
<MyAnimeList class='size-4 flex-center' />
|
||||
</Button>
|
||||
{/if}
|
||||
{#if media.trailer?.id}
|
||||
<Dialog.Root portal='#root'>
|
||||
<Dialog.Trigger let:builder asChild>
|
||||
<Button size='icon' variant='secondary' class='hidden md:flex' builders={[builder]}>
|
||||
<Button size='icon' variant='secondary' class='select:!text-custom' builders={[builder]}>
|
||||
<Clapperboard class='size-4' />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
|
@ -158,9 +141,17 @@
|
|||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
{/if}
|
||||
<Button size='icon' variant='secondary' disabled>
|
||||
<Bell class='size-4' />
|
||||
<Button size='icon' variant='secondary' class='hidden md:flex' on:click={() => native.openURL(`https://anilist.co/anime/${media.id}`)}>
|
||||
<Anilist class='size-4' />
|
||||
</Button>
|
||||
{#if media.idMal}
|
||||
<Button size='icon' variant='secondary' class='hidden md:flex' on:click={() => native.openURL(`https://myanimelist.net/anime/${media.idMal}`)}>
|
||||
<MyAnimeList class='size-4 flex-center' />
|
||||
</Button>
|
||||
{/if}
|
||||
<!-- <Button size='icon' variant='secondary' disabled>
|
||||
<Bell class='size-4' />
|
||||
</Button> -->
|
||||
<div class='-space-x-1 md:ml-3 hidden md:flex'>
|
||||
{#each followerEntries as followerEntry, i (followerEntry?.user?.id ?? i)}
|
||||
{#if followerEntry?.user}
|
||||
|
|
@ -169,13 +160,13 @@
|
|||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class='flex gap-2 items-center md:justify-start md:self-start flex-wrap'>
|
||||
<div class='flex gap-2 items-center md:justify-start md:self-start flex-wrap'>
|
||||
{#each media.genres ?? [] as genre (genre)}
|
||||
<div class='bg-secondary text-secondary-foreground text-sm font-medium rounded-md h-9 items-center justify-center flex px-4 text-nowrap'>
|
||||
<Button variant='secondary' class='select:!text-custom h-7 text-nowrap' on:click={() => goto('/app/search', { state: { search: { genre: [genre] } } })}>
|
||||
{genre}
|
||||
</div>
|
||||
</Button>
|
||||
{/each}
|
||||
</div> -->
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@
|
|||
<Load src={media.coverImage?.medium} class='object-cover h-full w-full shrink-0 rounded-l-md' />
|
||||
</div>
|
||||
<div class='h-full grid px-3 items-center'>
|
||||
<div class='text-theme font-bold capitalize'>{relation(rel.relationType)}</div>
|
||||
<div class='text-custom font-bold capitalize'>{relation(rel.relationType)}</div>
|
||||
<div class='line-clamp-2'>{media.title?.userPreferred ?? 'N/A'}</div>
|
||||
<div class='font-thin'>{format(media)}</div>
|
||||
</div>
|
||||
|
|
@ -63,9 +63,9 @@
|
|||
<Tabs.Root bind:value class='w-full' activateOnFocus={false}>
|
||||
<div class='flex justify-between items-center gap-3 sm:flex-row flex-col'>
|
||||
<Tabs.List class='flex'>
|
||||
<Tabs.Trigger value='episodes' tabindex={0} class='px-8 data-[state=active]:font-bold'>Episodes</Tabs.Trigger>
|
||||
<Tabs.Trigger value='threads' tabindex={0} class='px-8 data-[state=active]:font-bold'>Threads</Tabs.Trigger>
|
||||
<Tabs.Trigger value='themes' tabindex={0} class='px-8 data-[state=active]:font-bold'>Themes</Tabs.Trigger>
|
||||
<Tabs.Trigger value='episodes' tabindex={0} class='px-8 data-[state=active]:bg-custom data-[state=active]:text-contrast data-[state=active]:font-bold'>Episodes</Tabs.Trigger>
|
||||
<Tabs.Trigger value='threads' tabindex={0} class='px-8 data-[state=active]:bg-custom data-[state=active]:text-contrast data-[state=active]:font-bold'>Threads</Tabs.Trigger>
|
||||
<Tabs.Trigger value='themes' tabindex={0} class='px-8 data-[state=active]:bg-custom data-[state=active]:text-contrast data-[state=active]:font-bold'>Themes</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</div>
|
||||
<Tabs.Content value='episodes' tabindex={-1}>
|
||||
|
|
|
|||
|
|
@ -58,6 +58,20 @@ const config: Config = {
|
|||
},
|
||||
extend: {
|
||||
colors: {
|
||||
custom: {
|
||||
DEFAULT: 'hsl(from var(--custom) h s l / <alpha-value>)',
|
||||
50: 'hsl(from var(--custom) h s 95% / <alpha-value>)',
|
||||
100: 'hsl(from var(--custom) h s 90% / <alpha-value>)',
|
||||
200: 'hsl(from var(--custom) h s 80% / <alpha-value>)',
|
||||
300: 'hsl(from var(--custom) h s 70% / <alpha-value>)',
|
||||
400: 'hsl(from var(--custom) h s 60% / <alpha-value>)',
|
||||
500: 'hsl(from var(--custom) h s l / <alpha-value>)',
|
||||
600: 'hsl(from var(--custom) h s 40% / <alpha-value>)',
|
||||
700: 'hsl(from var(--custom) h s 30% / <alpha-value>)',
|
||||
800: 'hsl(from var(--custom) h s 20% / <alpha-value>)',
|
||||
900: 'hsl(from var(--custom) h s 10% / <alpha-value>)',
|
||||
950: 'hsl(from var(--custom) h s 5% / <alpha-value>)'
|
||||
},
|
||||
theme: 'hsl(346.6deg 79.12% 51.18% / <alpha-value>)',
|
||||
border: 'hsl(var(--border) / <alpha-value>)',
|
||||
input: 'hsl(var(--input) / <alpha-value>)',
|
||||
|
|
|
|||
Loading…
Reference in a new issue