mirror of
https://github.com/danbulant/Mangades
synced 2026-05-24 12:22:10 +00:00
better anilist integration, fix tracing, improved styles
This commit is contained in:
parent
96795593ea
commit
d53f829fed
12 changed files with 535 additions and 73 deletions
|
|
@ -13,7 +13,6 @@
|
||||||
"@sveltejs/kit": "1.0.0-next.572",
|
"@sveltejs/kit": "1.0.0-next.572",
|
||||||
"@types/streamsaver": "^2.0.1",
|
"@types/streamsaver": "^2.0.1",
|
||||||
"fs-extra": "^10.1.0",
|
"fs-extra": "^10.1.0",
|
||||||
"nollup": "^0.16.5",
|
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"postcss": "^8.4.16",
|
"postcss": "^8.4.16",
|
||||||
"postcss-import": "^14.1.0",
|
"postcss-import": "^14.1.0",
|
||||||
|
|
@ -27,6 +26,7 @@
|
||||||
"vite-plugin-windicss": "^1.8.8"
|
"vite-plugin-windicss": "^1.8.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@elastic/apm-rum": "^5.12.0",
|
||||||
"@sentry/browser": "^7.25.0",
|
"@sentry/browser": "^7.25.0",
|
||||||
"@sentry/svelte": "^7.25.0",
|
"@sentry/svelte": "^7.25.0",
|
||||||
"@sentry/tracing": "^7.25.0",
|
"@sentry/tracing": "^7.25.0",
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
<script data-goatcounter="https://mangades.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script>
|
<script data-goatcounter="https://mangades.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script>
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="stylesheet" href="%sveltekit.assets%/global.css" />
|
|
||||||
<link rel="stylesheet" href="%sveltekit.assets%/swiper.min.css" />
|
<link rel="stylesheet" href="%sveltekit.assets%/swiper.min.css" />
|
||||||
|
<link rel="stylesheet" href="%sveltekit.assets%/global.css" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,10 @@
|
||||||
|
|
||||||
{#if selectedImage}
|
{#if selectedImage}
|
||||||
<dialog open class="open" on:click={() => selectedImage = null} transition:fade={{ duration: 200 }}>
|
<dialog open class="open" on:click={() => selectedImage = null} transition:fade={{ duration: 200 }}>
|
||||||
<img src={selectedImage} alt="">
|
<div class="inner">
|
||||||
|
<img src={selectedImage} alt="">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
<button>Tap to close</button>
|
<button>Tap to close</button>
|
||||||
</dialog>
|
</dialog>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -30,6 +33,14 @@
|
||||||
:global(.dark dialog[open].open.open) {
|
:global(.dark dialog[open].open.open) {
|
||||||
background: rgba(0, 0, 0, 0.6);
|
background: rgba(0, 0, 0, 0.6);
|
||||||
}
|
}
|
||||||
|
.inner {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
margin: 2rem;
|
||||||
|
}
|
||||||
dialog img {
|
dialog img {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
|
|
@ -37,7 +48,7 @@
|
||||||
}
|
}
|
||||||
dialog button {
|
dialog button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 5px;
|
bottom: 1rem;
|
||||||
left: 5px;
|
left: 1rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
export var selectedImage = null;
|
export var selectedImage = null;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div class="main">
|
||||||
{#await list}
|
{#await list}
|
||||||
Loading art
|
Loading art
|
||||||
{:then list}
|
{:then list}
|
||||||
|
|
@ -29,27 +29,34 @@
|
||||||
height="805"
|
height="805"
|
||||||
src="{imageproxy}https://uploads.mangadex.org/covers/{mangaId}/{item.attributes.fileName}.512.jpg"
|
src="{imageproxy}https://uploads.mangadex.org/covers/{mangaId}/{item.attributes.fileName}.512.jpg"
|
||||||
alt=""
|
alt=""
|
||||||
|
class="color"
|
||||||
draggable={false} />
|
draggable={false} />
|
||||||
{/each}
|
{/each}
|
||||||
{/await}
|
{/await}
|
||||||
{#each additionalList as item}
|
{#each additionalList as item}
|
||||||
<img
|
<div class="img-container" style="{item.width ? `grid-column: span ${item.width}` : ""}; {item.height ? `grid-row: span ${item.height}` : ""};">
|
||||||
on:click={() => (selectedImage = item.src)}
|
<img
|
||||||
style="{item.color ? '--box-shadow-color: ' + item.color : ''}; width: 100%; height: 100%; {item.width ? `grid-column: span ${item.width}` : ""}; {item.height ? `grid-row: span ${item.height}` : ""};"
|
on:click={() => (selectedImage = item.src)}
|
||||||
src={item.src}
|
style="{item.color ? '--box-shadow-color: ' + item.color : ''};"
|
||||||
alt={item.alt}
|
class:color={item.color}
|
||||||
draggable={false} />
|
src={item.src}
|
||||||
|
alt={item.alt}
|
||||||
|
draggable={false} />
|
||||||
|
{#if !item.color}
|
||||||
|
<img class="img-backdrop" src={item.src} alt="">
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
.main {
|
||||||
display: grid;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
justify-content: start;
|
justify-items: center;
|
||||||
align-items: start;
|
align-items: center;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
|
||||||
}
|
}
|
||||||
div img {
|
div img {
|
||||||
|
|
@ -60,15 +67,46 @@
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
transition: filter 0.2s ease-in-out;
|
transition: filter 0.2s ease-in-out;
|
||||||
filter: drop-shadow(0 0 0 0 var(--box-shadow-color));
|
filter: drop-shadow(0 0 0 0 var(--box-shadow-color));
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
div img:first-child {
|
div .img-container img:not(.color), div .img-container img:not(.color):hover, div .img-container img:not(.color):active, div .img-container img:not(.color):focus {
|
||||||
|
--box-shadow-color: transparent;
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
.img-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.img-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
filter: blur(10px) saturate(100%);
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 0;
|
||||||
|
transition: opacity .3s, filter .3s;
|
||||||
|
}
|
||||||
|
.img-container:hover .img-backdrop {
|
||||||
|
filter: blur(20px) saturate(150%);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
div .img-container img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.main > img:first-child {
|
||||||
grid-column: 1 / span 2;
|
grid-column: 1 / span 2;
|
||||||
grid-row: 1 / span 2;
|
grid-row: 1 / span 2;
|
||||||
height: 20rem;
|
height: 20rem;
|
||||||
}
|
}
|
||||||
div img:hover,
|
div img.color:hover,
|
||||||
div img:active,
|
div img.color:active,
|
||||||
div img:focus {
|
div img.color:focus {
|
||||||
filter: drop-shadow(0 0 0.5rem var(--box-shadow-color));
|
filter: drop-shadow(0 0 0.5rem var(--box-shadow-color));
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import * as Sentry from "@sentry/browser";
|
import * as Sentry from "@sentry/browser";
|
||||||
|
import { apm } from "./tracing";
|
||||||
|
|
||||||
var isLogedInCache: boolean | null = null;
|
var isLogedInCache: boolean | null = null;
|
||||||
var isLogedInCacheTime: number | null = null;
|
var isLogedInCacheTime: number | null = null;
|
||||||
|
|
@ -22,6 +23,7 @@ export function getUserID() {
|
||||||
const token = localStorage.getItem("token")!;
|
const token = localStorage.getItem("token")!;
|
||||||
let data = JSON.parse(atob(token.substring(token.indexOf(".") + 1, token.lastIndexOf("."))));
|
let data = JSON.parse(atob(token.substring(token.indexOf(".") + 1, token.lastIndexOf("."))));
|
||||||
Sentry.setUser({ id: data.sub });
|
Sentry.setUser({ id: data.sub });
|
||||||
|
apm.setUserContext({ id: data.sub });
|
||||||
return data.sub;
|
return data.sub;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
const ratelimits = new Map();
|
const ratelimits = new Map();
|
||||||
|
|
||||||
function callback(func) {
|
function callback({ func }) {
|
||||||
const params = ratelimits.get(func);
|
const params = ratelimits.get(func);
|
||||||
|
console.log(params, func, ratelimits);
|
||||||
func(...params.params).then(params.result.resolve).catch(params.result.reject);
|
func(...params.params).then(params.result.resolve).catch(params.result.reject);
|
||||||
ratelimits.delete(func);
|
ratelimits.delete(func);
|
||||||
}
|
}
|
||||||
|
|
@ -14,8 +15,9 @@ function callback(func) {
|
||||||
* @returns {Promise<ReturnType<T>>}
|
* @returns {Promise<ReturnType<T>>}
|
||||||
*/
|
*/
|
||||||
function ratelimit(func, ...params) {
|
function ratelimit(func, ...params) {
|
||||||
|
console.log("Adding rate limit", func, params);
|
||||||
const data = ratelimits.get(func) || {
|
const data = ratelimits.get(func) || {
|
||||||
timeout: setTimeout(callback, 200, func),
|
timeout: setTimeout(callback, 200, { func }),
|
||||||
params,
|
params,
|
||||||
result: {}
|
result: {}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
22
src/lib/util/tracing.ts
Normal file
22
src/lib/util/tracing.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
import { ApmBase, init as initApm } from '@elastic/apm-rum'
|
||||||
|
|
||||||
|
var apm_: ApmBase;
|
||||||
|
|
||||||
|
if(browser) {
|
||||||
|
apm_ = initApm({
|
||||||
|
// Set required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space)
|
||||||
|
serviceName: 'mangades',
|
||||||
|
|
||||||
|
// Set custom APM Server URL (default: http://localhost:8200)
|
||||||
|
serverUrl: 'https://apm.elasticsearch.danbulant.cloud',
|
||||||
|
|
||||||
|
// Set the service version (required for source map feature)
|
||||||
|
serviceVersion: import.meta.env.VITE_SENTRY_RELEASE,
|
||||||
|
|
||||||
|
// Set the service environment
|
||||||
|
environment: import.meta.env.VITE_SENTRY_ENVIRONMENT || 'production'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apm = apm_;
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
import * as Sentry from '@sentry/svelte';
|
import * as Sentry from '@sentry/svelte';
|
||||||
import { BrowserTracing } from "@sentry/tracing";
|
import { BrowserTracing } from "@sentry/tracing";
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
import { apm } from "$lib/util/tracing";
|
||||||
|
import { page } from "$app/stores";
|
||||||
|
|
||||||
export var data;
|
export var data;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
@ -21,9 +23,13 @@
|
||||||
tracePropagationTargets: ["localhost", "manga.danbulant.eu", "tachiyomi.manga-d7tp.pages.dev", "manga-d7tp.pages.dev", /^\/.*/]
|
tracePropagationTargets: ["localhost", "manga.danbulant.eu", "tachiyomi.manga-d7tp.pages.dev", "manga-d7tp.pages.dev", /^\/.*/]
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
tracesSampleRate: 1
|
tracesSampleRate: 1,
|
||||||
|
autoSessionTracking: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if(browser) {
|
||||||
|
apm.setInitialPageLoadName($page.route.id);
|
||||||
|
}
|
||||||
|
|
||||||
let skipFirst = true;
|
let skipFirst = true;
|
||||||
let last = typeof window !== "undefined" && window.location.pathname;
|
let last = typeof window !== "undefined" && window.location.pathname;
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@
|
||||||
import { EpubGenerator } from "$lib/util/generateEpub";
|
import { EpubGenerator } from "$lib/util/generateEpub";
|
||||||
import { CBZGenerator } from "$lib/util/generateCbz";
|
import { CBZGenerator } from "$lib/util/generateCbz";
|
||||||
import request, { imageproxy } from "$lib/util/request";
|
import request, { imageproxy } from "$lib/util/request";
|
||||||
import { BaseGenerator } from "$lib/util/baseGenerator";
|
|
||||||
import { arraysEqual } from "$lib/util/arrays";
|
import { arraysEqual } from "$lib/util/arrays";
|
||||||
import { makeRequest } from "$lib/util/anilist";
|
|
||||||
import Tabs from "$lib/components/tabs/tabs.svelte";
|
import Tabs from "$lib/components/tabs/tabs.svelte";
|
||||||
import { slide } from "svelte/transition";
|
import { slide } from "svelte/transition";
|
||||||
import { Swiper, SwiperSlide } from 'swiper/svelte';
|
import { Swiper, SwiperSlide } from 'swiper/svelte';
|
||||||
|
|
@ -18,6 +16,7 @@
|
||||||
import { tick } from "svelte";
|
import { tick } from "svelte";
|
||||||
import Favicon from "./favicon.svelte";
|
import Favicon from "./favicon.svelte";
|
||||||
import RelatedManga from "./relatedManga.svelte";
|
import RelatedManga from "./relatedManga.svelte";
|
||||||
|
import { anilistInfo } from "./anilistInfo";
|
||||||
|
|
||||||
export var data;
|
export var data;
|
||||||
|
|
||||||
|
|
@ -30,6 +29,9 @@
|
||||||
var title = manga.title.en || manga.title.jp || Object.values(manga.title)[0];
|
var title = manga.title.en || manga.title.jp || Object.values(manga.title)[0];
|
||||||
$: title = manga.title.en || manga.title.jp || Object.values(manga.title)[0];
|
$: title = manga.title.en || manga.title.jp || Object.values(manga.title)[0];
|
||||||
|
|
||||||
|
let anilistData;
|
||||||
|
$: anilistData = manga.links && manga.links.al && anilistInfo(manga.links.al);
|
||||||
|
|
||||||
let cache: { id: string, data: any, total } | null = null;
|
let cache: { id: string, data: any, total } | null = null;
|
||||||
async function getMangaChapters(id) {
|
async function getMangaChapters(id) {
|
||||||
if(cache?.id === id && cache.data.length >= cache.total) return cache;
|
if(cache?.id === id && cache.data.length >= cache.total) return cache;
|
||||||
|
|
@ -246,43 +248,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const anilistCache = new Map();
|
|
||||||
function anilistInfo(id) {
|
|
||||||
if(!anilistCache.has(id))
|
|
||||||
anilistCache.set(id, makeRequest(`
|
|
||||||
query ($id: Int) {
|
|
||||||
Media(id: $id, format: MANGA) {
|
|
||||||
id
|
|
||||||
type
|
|
||||||
format
|
|
||||||
status
|
|
||||||
chapters
|
|
||||||
volumes
|
|
||||||
countryOfOrigin
|
|
||||||
bannerImage
|
|
||||||
genres
|
|
||||||
synonyms
|
|
||||||
averageScore
|
|
||||||
popularity
|
|
||||||
isFavourite
|
|
||||||
isFavouriteBlocked
|
|
||||||
isAdult
|
|
||||||
siteUrl
|
|
||||||
coverImage {
|
|
||||||
large
|
|
||||||
medium
|
|
||||||
color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`, { id }).then(t => t.data.Media));
|
|
||||||
return anilistCache.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
let anilistData;
|
|
||||||
$: anilistData = manga.links && manga.links.al && anilistInfo(manga.links.al);
|
|
||||||
|
|
||||||
var selectedTab = "Chapters";
|
var selectedTab = "Chapters";
|
||||||
const tabs = ["Chapters", "Art", "More information"];
|
const defaultTabs = ["Chapters", "Art", "More info"];
|
||||||
|
var tabs = defaultTabs;
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if(swiper && tabs.indexOf(selectedTab) !== swiper.realIndex) swiper.slideTo(tabs.indexOf(selectedTab));
|
if(swiper && tabs.indexOf(selectedTab) !== swiper.realIndex) swiper.slideTo(tabs.indexOf(selectedTab));
|
||||||
|
|
@ -310,16 +278,27 @@
|
||||||
|
|
||||||
$: if(anilistData) anilistData.then(data => {
|
$: if(anilistData) anilistData.then(data => {
|
||||||
if(data.bannerImage && !additionalImages.find(t => t.src === data.bannerImage)) {
|
if(data.bannerImage && !additionalImages.find(t => t.src === data.bannerImage)) {
|
||||||
|
additionalImages.push({
|
||||||
|
src: data.coverImage.large,
|
||||||
|
alt: "Cover image from anilist",
|
||||||
|
// color: data.coverImage.color,
|
||||||
|
height: 1,
|
||||||
|
width: 1
|
||||||
|
});
|
||||||
additionalImages.push({
|
additionalImages.push({
|
||||||
src: data.bannerImage,
|
src: data.bannerImage,
|
||||||
alt: "Banner image",
|
alt: "Banner image from anilist",
|
||||||
color: data.coverImage.color,
|
// color: data.coverImage.color,
|
||||||
height: 1,
|
height: 1,
|
||||||
width: 3
|
width: 3
|
||||||
});
|
});
|
||||||
additionalImages = additionalImages;
|
additionalImages = additionalImages;
|
||||||
|
tabs = [...defaultTabs, "Characters"];
|
||||||
}
|
}
|
||||||
});
|
}); else {
|
||||||
|
additionalImages = []
|
||||||
|
tabs = defaultTabs;
|
||||||
|
}
|
||||||
|
|
||||||
$: if(chapters) console.log("ch", chapters, chapters.data.length, chapters.total, chapters.data.length < chapters.total);
|
$: if(chapters) console.log("ch", chapters, chapters.data.length, chapters.total, chapters.data.length < chapters.total);
|
||||||
|
|
||||||
|
|
@ -335,6 +314,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if(!loadingNextPage && chapters && chapters.data.length < chapters.total && scrollY > 300 && scrollY > document.body.scrollHeight * 0.8) loadNextPage();
|
$: if(!loadingNextPage && chapters && chapters.data.length < chapters.total && scrollY > 300 && scrollY > document.body.scrollHeight * 0.8) loadNextPage();
|
||||||
|
|
||||||
|
var selectedCharacter = null;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:beforeunload={beforeUnload} bind:innerWidth={width} bind:scrollY bind:innerHeight />
|
<svelte:window on:beforeunload={beforeUnload} bind:innerWidth={width} bind:scrollY bind:innerHeight />
|
||||||
|
|
@ -346,7 +327,31 @@
|
||||||
|
|
||||||
<Navbar transparent={scrollY < 0.2*innerHeight} {title} />
|
<Navbar transparent={scrollY < 0.2*innerHeight} {title} />
|
||||||
|
|
||||||
<ArtDialog bind:selectedImage />
|
<ArtDialog bind:selectedImage>
|
||||||
|
{#if selectedCharacter}
|
||||||
|
<div class="character-info">
|
||||||
|
<h1>{selectedCharacter.node.name.full}</h1>
|
||||||
|
<h2 style="padding: 0; margin: 0 0 0.5rem; color: rgba(255,255,255,0.7);">{selectedCharacter.node.name.native}</h2>
|
||||||
|
<small style="color: rgba(255,255,255,0.7)">Crossed out text may indicate spoilers!</small>
|
||||||
|
<p style="margin: 0 0 1rem;"><SvelteMarkdown source={selectedCharacter.node.description} isInline /></p>
|
||||||
|
{#if selectedCharacter.node.gender}
|
||||||
|
<div>Gender: {selectedCharacter.node.gender}</div>
|
||||||
|
{/if}
|
||||||
|
{#if selectedCharacter.node.age}
|
||||||
|
<div>Age: {selectedCharacter.node.age}</div>
|
||||||
|
{/if}
|
||||||
|
{#if selectedCharacter.node.dateOfBirth && (selectedCharacter.node.dateOfBirth.day || selectedCharacter.node.dateOfBirth.month || selectedCharacter.node.dateOfBirth.year)}
|
||||||
|
<div>Birthday: {selectedCharacter.node.dateOfBirth.day || "Unknown"}/{selectedCharacter.node.dateOfBirth.month || "Unknown"}/{selectedCharacter.node.dateOfBirth.year || "Unknown"}</div>
|
||||||
|
{/if}
|
||||||
|
{#if selectedCharacter.node.favourites}
|
||||||
|
<div>Favourites: {selectedCharacter.node.favourites}</div>
|
||||||
|
{/if}
|
||||||
|
{#if selectedCharacter.node.bloodType}
|
||||||
|
<div>Blood type: {selectedCharacter.node.bloodType}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</ArtDialog>
|
||||||
|
|
||||||
{#if anilistData} {#await anilistData then data}
|
{#if anilistData} {#await anilistData then data}
|
||||||
{#if data.bannerImage}
|
{#if data.bannerImage}
|
||||||
|
|
@ -360,7 +365,10 @@
|
||||||
<main class:smallScreenMode>
|
<main class:smallScreenMode>
|
||||||
<div class="flex infoflex">
|
<div class="flex infoflex">
|
||||||
{#if relationships.find(t => t.type === "cover_art")}
|
{#if relationships.find(t => t.type === "cover_art")}
|
||||||
<img class="cover" class:r18={!["safe", "suggestive"].includes(manga.contentRating)} draggable="false" src="{imageproxy}https://uploads.mangadex.org/covers/{mangaId}/{relationships.find(t => t.type === "cover_art").attributes.fileName}.512.jpg" alt="" on:click={() => selectedImage = `https://uploads.mangadex.org/covers/${mangaId}/${relationships.find(t => t.type === "cover_art").attributes.fileName}.512.jpg`}>
|
<div class="cover-container">
|
||||||
|
<img class="cover" class:r18={!["safe", "suggestive"].includes(manga.contentRating)} draggable="false" src="{imageproxy}https://uploads.mangadex.org/covers/{mangaId}/{relationships.find(t => t.type === "cover_art").attributes.fileName}.512.jpg" alt="" on:click={() => selectedImage = `https://uploads.mangadex.org/covers/${mangaId}/${relationships.find(t => t.type === "cover_art").attributes.fileName}.512.jpg`}>
|
||||||
|
<img class="cover-backdrop" draggable="false" src="{imageproxy}https://uploads.mangadex.org/covers/{mangaId}/{relationships.find(t => t.type === "cover_art").attributes.fileName}.512.jpg" alt="">
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<h1>{title}</h1>
|
<h1>{title}</h1>
|
||||||
|
|
@ -433,7 +441,7 @@
|
||||||
on:swiper={(e) => console.log(e.detail[0])}
|
on:swiper={(e) => console.log(e.detail[0])}
|
||||||
>
|
>
|
||||||
<SwiperSlide>
|
<SwiperSlide>
|
||||||
<div style="min-height: 30rem;">
|
<div class="chapter-list" style="min-height: 30rem;">
|
||||||
<div class="state {state}">
|
<div class="state {state}">
|
||||||
<div class="progress" style="width: {progress * 100}%;"></div>
|
<div class="progress" style="width: {progress * 100}%;"></div>
|
||||||
|
|
||||||
|
|
@ -483,12 +491,12 @@
|
||||||
</div>
|
</div>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
<SwiperSlide>
|
<SwiperSlide>
|
||||||
<div style="min-height: 30rem;">
|
<div class="art-list" style="min-height: 30rem;">
|
||||||
<ArtList {mangaId} bind:selectedImage additionalList={additionalImages} />
|
<ArtList {mangaId} bind:selectedImage additionalList={additionalImages} />
|
||||||
</div>
|
</div>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
<SwiperSlide>
|
<SwiperSlide>
|
||||||
<div style="min-height: 30rem;">
|
<div class="more-info" style="min-height: 30rem;">
|
||||||
<div class="flex-wrapped">
|
<div class="flex-wrapped">
|
||||||
{#if anilistData} {#await anilistData then data}
|
{#if anilistData} {#await anilistData then data}
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -553,10 +561,78 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
|
<SwiperSlide>
|
||||||
|
<div class="characters" style="min-height: 30rem;">
|
||||||
|
{#await anilistData then data}{#if data}
|
||||||
|
{#each data.characters.edges as character}
|
||||||
|
<div class="character" on:click={() => {selectedImage = character.node.image.large; selectedCharacter = character}} >
|
||||||
|
<div class="container">
|
||||||
|
<img src={character.node.image.large} alt="" />
|
||||||
|
<img class="backdrop" src={character.node.image.large} alt="" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4>{character.node.name.userPreferred}</h4>
|
||||||
|
<span class="role">{character.role}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}{/await}
|
||||||
|
</div>
|
||||||
|
</SwiperSlide>
|
||||||
</Swiper>
|
</Swiper>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.art-list {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
.chapter-list {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.more-info {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
.characters {
|
||||||
|
margin-top: 1rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
grid-gap: 2rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.character {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.character img {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
object-fit: cover;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.container .backdrop {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
filter: blur(10px) saturate(100%);
|
||||||
|
z-index: 0;
|
||||||
|
transition: opacity .3s, filter .3s;
|
||||||
|
}
|
||||||
|
.container:hover .backdrop {
|
||||||
|
opacity: 1;
|
||||||
|
filter: blur(20px) saturate(150%);
|
||||||
|
}
|
||||||
|
.character .role {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: rgb(175, 175, 175);
|
||||||
|
}
|
||||||
h4 {
|
h4 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -578,6 +654,7 @@
|
||||||
.infoflex.flex {
|
.infoflex.flex {
|
||||||
margin: 15px;
|
margin: 15px;
|
||||||
justify-content: start;
|
justify-content: start;
|
||||||
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
.flex-wrapped {
|
.flex-wrapped {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -626,13 +703,37 @@
|
||||||
.tabbed {
|
.tabbed {
|
||||||
min-height: 20rem;
|
min-height: 20rem;
|
||||||
}
|
}
|
||||||
.cover {
|
.cover-container {
|
||||||
border-radius: 10px;
|
position: relative;
|
||||||
height: 20rem;
|
height: 20rem;
|
||||||
|
flex-shrink: 0;
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
transition: height .3s;
|
transition: height .3s;
|
||||||
}
|
}
|
||||||
.smallScreenMode .cover {
|
.cover {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 10px;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.cover-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: calc(100% + 4px);
|
||||||
|
height: calc(100% + 4px);
|
||||||
|
z-index: 0;
|
||||||
|
filter: blur(18px) saturate(100%);
|
||||||
|
transition: filter .3s;
|
||||||
|
}
|
||||||
|
.cover-container:hover .cover-backdrop {
|
||||||
|
filter: blur(30px) saturate(150%);
|
||||||
|
}
|
||||||
|
.smallScreenMode .cover-container {
|
||||||
height: 12rem;
|
height: 12rem;
|
||||||
}
|
}
|
||||||
.block {
|
.block {
|
||||||
|
|
|
||||||
274
src/routes/[manga]/anilistInfo.ts
Normal file
274
src/routes/[manga]/anilistInfo.ts
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
import { makeRequest } from "$lib/util/anilist";
|
||||||
|
|
||||||
|
interface AnilistInfo {
|
||||||
|
Media: Media;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Media {
|
||||||
|
id: number;
|
||||||
|
title: Title;
|
||||||
|
type: "MANGA";
|
||||||
|
format: "MANGA";
|
||||||
|
status: "FINISHED" | "RELEASING" | "NOT_YET_RELEASED" | "CANCELLED";
|
||||||
|
chapters: number | null;
|
||||||
|
volumes: number | null;
|
||||||
|
countryOfOrigin: string;
|
||||||
|
bannerImage: string;
|
||||||
|
genres: string[];
|
||||||
|
synonyms: string[];
|
||||||
|
averageScore: number | null;
|
||||||
|
popularity: number;
|
||||||
|
isFavourite: boolean;
|
||||||
|
isFavouriteBlocked: boolean;
|
||||||
|
isAdult: boolean;
|
||||||
|
siteUrl: string;
|
||||||
|
coverImage: CoverImage;
|
||||||
|
description: string;
|
||||||
|
characters: {
|
||||||
|
edges: {
|
||||||
|
id: number;
|
||||||
|
role: "MAIN" | "SUPPORTING" | "BACKGROUND";
|
||||||
|
node: Character;
|
||||||
|
}[]
|
||||||
|
};
|
||||||
|
tags: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
isMediaSpoiler: boolean;
|
||||||
|
isAdult: boolean;
|
||||||
|
}[];
|
||||||
|
relations: {
|
||||||
|
edges: {
|
||||||
|
id: number;
|
||||||
|
relationType: "ADAPTATION" | "PREQUEL" | "SEQUEL" | "PARENT" | "SIDE_STORY" | "CHARACTER" | "SUMMARY" | "ALTERNATIVE" | "SPIN_OFF" | "OTHER" | "SOURCE" | "COMPILATION" | "CONTAINS";
|
||||||
|
node: {
|
||||||
|
type: "ANIME" | "MANGA";
|
||||||
|
mediaListEntry: MediaListEntry | null;
|
||||||
|
description: string;
|
||||||
|
coverImage: CoverImage;
|
||||||
|
title: Title
|
||||||
|
}
|
||||||
|
}[]
|
||||||
|
};
|
||||||
|
mediaListEntry: MediaListEntry | null;
|
||||||
|
recommendations: {
|
||||||
|
edges: {
|
||||||
|
node: {
|
||||||
|
id: number;
|
||||||
|
rating: number;
|
||||||
|
mediaRecommendation: {
|
||||||
|
title: Title;
|
||||||
|
mediaListEntry: MediaListEntry | null;
|
||||||
|
description: string;
|
||||||
|
coverImage: CoverImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}[]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Title {
|
||||||
|
romaji: string;
|
||||||
|
english: string;
|
||||||
|
native: string;
|
||||||
|
userPreferred: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MediaListEntry {
|
||||||
|
progress: number | null;
|
||||||
|
progressVolumes: number | null;
|
||||||
|
repeat: number | null;
|
||||||
|
priority: number | null;
|
||||||
|
private: boolean;
|
||||||
|
notes?: string;
|
||||||
|
score: number | null;
|
||||||
|
customLists: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Character {
|
||||||
|
name: {
|
||||||
|
first: string;
|
||||||
|
middle: string;
|
||||||
|
last: string;
|
||||||
|
full: string;
|
||||||
|
native: string;
|
||||||
|
userPreferred: string;
|
||||||
|
};
|
||||||
|
image: {
|
||||||
|
large: string;
|
||||||
|
medium: string;
|
||||||
|
};
|
||||||
|
description: string;
|
||||||
|
gender: "Male" | "Female" | null;
|
||||||
|
age: number | null;
|
||||||
|
dateOfBirth: {
|
||||||
|
year: number | null;
|
||||||
|
month: number | null;
|
||||||
|
day: number | null;
|
||||||
|
};
|
||||||
|
favourites: number;
|
||||||
|
bloodType: "A" | "B" | "O" | "AB" | "A+" | "A-" | "B+" | "B-" | "O+" | "O-" | "AB+" | "AB-" | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CoverImage {
|
||||||
|
extraLarge: string;
|
||||||
|
large: string;
|
||||||
|
medium: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const anilistCache = new Map();
|
||||||
|
export function anilistInfo(id): Promise<AnilistInfo> {
|
||||||
|
if(!anilistCache.has(id))
|
||||||
|
anilistCache.set(id, makeRequest(`
|
||||||
|
query($id: Int) {
|
||||||
|
Media(id: $id, format: MANGA) {
|
||||||
|
id
|
||||||
|
title {
|
||||||
|
romaji
|
||||||
|
english
|
||||||
|
native
|
||||||
|
userPreferred
|
||||||
|
}
|
||||||
|
type
|
||||||
|
format
|
||||||
|
status
|
||||||
|
chapters
|
||||||
|
volumes
|
||||||
|
countryOfOrigin
|
||||||
|
bannerImage
|
||||||
|
genres
|
||||||
|
synonyms
|
||||||
|
averageScore
|
||||||
|
popularity
|
||||||
|
isFavourite
|
||||||
|
isFavouriteBlocked
|
||||||
|
isAdult
|
||||||
|
siteUrl
|
||||||
|
coverImage {
|
||||||
|
extraLarge
|
||||||
|
large
|
||||||
|
medium
|
||||||
|
color
|
||||||
|
}
|
||||||
|
characters {
|
||||||
|
edges {
|
||||||
|
id
|
||||||
|
role
|
||||||
|
node {
|
||||||
|
name {
|
||||||
|
first
|
||||||
|
middle
|
||||||
|
last
|
||||||
|
full
|
||||||
|
native
|
||||||
|
userPreferred
|
||||||
|
}
|
||||||
|
description
|
||||||
|
gender
|
||||||
|
age
|
||||||
|
dateOfBirth {
|
||||||
|
year
|
||||||
|
month
|
||||||
|
day
|
||||||
|
}
|
||||||
|
favourites
|
||||||
|
bloodType
|
||||||
|
image {
|
||||||
|
large
|
||||||
|
medium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tags {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
isMediaSpoiler
|
||||||
|
isAdult
|
||||||
|
}
|
||||||
|
relations {
|
||||||
|
edges {
|
||||||
|
id
|
||||||
|
relationType
|
||||||
|
node {
|
||||||
|
type
|
||||||
|
title {
|
||||||
|
romaji
|
||||||
|
english
|
||||||
|
native
|
||||||
|
userPreferred
|
||||||
|
}
|
||||||
|
mediaListEntry {
|
||||||
|
userId
|
||||||
|
status
|
||||||
|
score
|
||||||
|
progress
|
||||||
|
progressVolumes
|
||||||
|
repeat
|
||||||
|
priority
|
||||||
|
private
|
||||||
|
notes
|
||||||
|
}
|
||||||
|
coverImage {
|
||||||
|
extraLarge
|
||||||
|
large
|
||||||
|
medium
|
||||||
|
color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mediaListEntry {
|
||||||
|
userId
|
||||||
|
status
|
||||||
|
score
|
||||||
|
progress
|
||||||
|
progressVolumes
|
||||||
|
repeat
|
||||||
|
priority
|
||||||
|
private
|
||||||
|
notes
|
||||||
|
}
|
||||||
|
recommendations {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
rating
|
||||||
|
mediaRecommendation {
|
||||||
|
title {
|
||||||
|
romaji
|
||||||
|
english
|
||||||
|
native
|
||||||
|
userPreferred
|
||||||
|
}
|
||||||
|
description
|
||||||
|
mediaListEntry {
|
||||||
|
userId
|
||||||
|
status
|
||||||
|
score
|
||||||
|
progress
|
||||||
|
progressVolumes
|
||||||
|
repeat
|
||||||
|
priority
|
||||||
|
private
|
||||||
|
notes
|
||||||
|
}
|
||||||
|
coverImage {
|
||||||
|
extraLarge
|
||||||
|
large
|
||||||
|
medium
|
||||||
|
color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}`, { id }).then(t => t.data.Media));
|
||||||
|
anilistCache.get(id).then(t => console.log("anilist", t));
|
||||||
|
return anilistCache.get(id);
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 0 1rem;
|
padding: 0 2rem;
|
||||||
transition: background 0.3s ease;
|
transition: background 0.3s ease;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
@ -34,10 +34,13 @@
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
.navbar a:hover, .navbar a:active, .navbar a:focus {
|
.navbar a:hover, .navbar a:active, .navbar a:focus {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transform: translateX(-0.5rem);
|
transform: translateX(-0.5rem);
|
||||||
|
margin-right: 0;
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.5rem;
|
||||||
}
|
}
|
||||||
.transparent .title {
|
.transparent .title {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 0 1rem;
|
padding: 0 2rem;
|
||||||
transition: background 0.3s ease;
|
transition: background 0.3s ease;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
@ -34,10 +34,13 @@
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
.navbar a:hover, .navbar a:active, .navbar a:focus {
|
.navbar a:hover, .navbar a:active, .navbar a:focus {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transform: translateX(-0.5rem);
|
transform: translateX(-0.5rem);
|
||||||
|
margin-right: 0;
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.5rem;
|
||||||
}
|
}
|
||||||
.transparent .title {
|
.transparent .title {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue