better anilist integration, fix tracing, improved styles

This commit is contained in:
Daniel Bulant 2022-12-18 22:36:30 +01:00
parent 96795593ea
commit d53f829fed
12 changed files with 535 additions and 73 deletions

View file

@ -13,7 +13,6 @@
"@sveltejs/kit": "1.0.0-next.572",
"@types/streamsaver": "^2.0.1",
"fs-extra": "^10.1.0",
"nollup": "^0.16.5",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.16",
"postcss-import": "^14.1.0",
@ -27,6 +26,7 @@
"vite-plugin-windicss": "^1.8.8"
},
"dependencies": {
"@elastic/apm-rum": "^5.12.0",
"@sentry/browser": "^7.25.0",
"@sentry/svelte": "^7.25.0",
"@sentry/tracing": "^7.25.0",

View file

@ -5,8 +5,8 @@
<script data-goatcounter="https://mangades.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script>
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<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%/global.css" />
%sveltekit.head%
</head>
<body>

View file

@ -5,7 +5,10 @@
{#if selectedImage}
<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>
</dialog>
{/if}
@ -30,6 +33,14 @@
:global(.dark dialog[open].open.open) {
background: rgba(0, 0, 0, 0.6);
}
.inner {
display: flex;
gap: 2rem;
align-items: center;
justify-content: center;
color: white;
margin: 2rem;
}
dialog img {
border-radius: 5px;
max-height: 100%;
@ -37,7 +48,7 @@
}
dialog button {
position: absolute;
bottom: 5px;
left: 5px;
bottom: 1rem;
left: 1rem;
}
</style>

View file

@ -18,7 +18,7 @@
export var selectedImage = null;
</script>
<div>
<div class="main">
{#await list}
Loading art
{:then list}
@ -29,27 +29,34 @@
height="805"
src="{imageproxy}https://uploads.mangadex.org/covers/{mangaId}/{item.attributes.fileName}.512.jpg"
alt=""
class="color"
draggable={false} />
{/each}
{/await}
{#each additionalList as item}
<img
on:click={() => (selectedImage = item.src)}
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}` : ""};"
src={item.src}
alt={item.alt}
draggable={false} />
<div class="img-container" style="{item.width ? `grid-column: span ${item.width}` : ""}; {item.height ? `grid-row: span ${item.height}` : ""};">
<img
on:click={() => (selectedImage = item.src)}
style="{item.color ? '--box-shadow-color: ' + item.color : ''};"
class:color={item.color}
src={item.src}
alt={item.alt}
draggable={false} />
{#if !item.color}
<img class="img-backdrop" src={item.src} alt="">
{/if}
</div>
{/each}
</div>
<style>
div {
.main {
display: grid;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: start;
align-items: start;
justify-items: center;
align-items: center;
grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
}
div img {
@ -60,15 +67,46 @@
object-fit: contain;
transition: filter 0.2s ease-in-out;
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-row: 1 / span 2;
height: 20rem;
}
div img:hover,
div img:active,
div img:focus {
div img.color:hover,
div img.color:active,
div img.color:focus {
filter: drop-shadow(0 0 0.5rem var(--box-shadow-color));
}
</style>

View file

@ -1,4 +1,5 @@
import * as Sentry from "@sentry/browser";
import { apm } from "./tracing";
var isLogedInCache: boolean | null = null;
var isLogedInCacheTime: number | null = null;
@ -22,6 +23,7 @@ export function getUserID() {
const token = localStorage.getItem("token")!;
let data = JSON.parse(atob(token.substring(token.indexOf(".") + 1, token.lastIndexOf("."))));
Sentry.setUser({ id: data.sub });
apm.setUserContext({ id: data.sub });
return data.sub;
}

View file

@ -1,7 +1,8 @@
const ratelimits = new Map();
function callback(func) {
function callback({ func }) {
const params = ratelimits.get(func);
console.log(params, func, ratelimits);
func(...params.params).then(params.result.resolve).catch(params.result.reject);
ratelimits.delete(func);
}
@ -14,8 +15,9 @@ function callback(func) {
* @returns {Promise<ReturnType<T>>}
*/
function ratelimit(func, ...params) {
console.log("Adding rate limit", func, params);
const data = ratelimits.get(func) || {
timeout: setTimeout(callback, 200, func),
timeout: setTimeout(callback, 200, { func }),
params,
result: {}
};

22
src/lib/util/tracing.ts Normal file
View 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_;

View file

@ -5,6 +5,8 @@
import * as Sentry from '@sentry/svelte';
import { BrowserTracing } from "@sentry/tracing";
import { browser } from '$app/environment';
import { apm } from "$lib/util/tracing";
import { page } from "$app/stores";
export var data;
// @ts-ignore
@ -21,9 +23,13 @@
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 last = typeof window !== "undefined" && window.location.pathname;

View file

@ -3,9 +3,7 @@
import { EpubGenerator } from "$lib/util/generateEpub";
import { CBZGenerator } from "$lib/util/generateCbz";
import request, { imageproxy } from "$lib/util/request";
import { BaseGenerator } from "$lib/util/baseGenerator";
import { arraysEqual } from "$lib/util/arrays";
import { makeRequest } from "$lib/util/anilist";
import Tabs from "$lib/components/tabs/tabs.svelte";
import { slide } from "svelte/transition";
import { Swiper, SwiperSlide } from 'swiper/svelte';
@ -18,6 +16,7 @@
import { tick } from "svelte";
import Favicon from "./favicon.svelte";
import RelatedManga from "./relatedManga.svelte";
import { anilistInfo } from "./anilistInfo";
export var data;
@ -30,6 +29,9 @@
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];
let anilistData;
$: anilistData = manga.links && manga.links.al && anilistInfo(manga.links.al);
let cache: { id: string, data: any, total } | null = null;
async function getMangaChapters(id) {
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";
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));
@ -310,16 +278,27 @@
$: if(anilistData) anilistData.then(data => {
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({
src: data.bannerImage,
alt: "Banner image",
color: data.coverImage.color,
alt: "Banner image from anilist",
// color: data.coverImage.color,
height: 1,
width: 3
});
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);
@ -335,6 +314,8 @@
}
$: if(!loadingNextPage && chapters && chapters.data.length < chapters.total && scrollY > 300 && scrollY > document.body.scrollHeight * 0.8) loadNextPage();
var selectedCharacter = null;
</script>
<svelte:window on:beforeunload={beforeUnload} bind:innerWidth={width} bind:scrollY bind:innerHeight />
@ -346,7 +327,31 @@
<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 data.bannerImage}
@ -360,7 +365,10 @@
<main class:smallScreenMode>
<div class="flex infoflex">
{#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}
<div class="info">
<h1>{title}</h1>
@ -433,7 +441,7 @@
on:swiper={(e) => console.log(e.detail[0])}
>
<SwiperSlide>
<div style="min-height: 30rem;">
<div class="chapter-list" style="min-height: 30rem;">
<div class="state {state}">
<div class="progress" style="width: {progress * 100}%;"></div>
@ -483,12 +491,12 @@
</div>
</SwiperSlide>
<SwiperSlide>
<div style="min-height: 30rem;">
<div class="art-list" style="min-height: 30rem;">
<ArtList {mangaId} bind:selectedImage additionalList={additionalImages} />
</div>
</SwiperSlide>
<SwiperSlide>
<div style="min-height: 30rem;">
<div class="more-info" style="min-height: 30rem;">
<div class="flex-wrapped">
{#if anilistData} {#await anilistData then data}
<div>
@ -553,10 +561,78 @@
{/if}
</div>
</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>
</main>
<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 {
margin: 0;
}
@ -578,6 +654,7 @@
.infoflex.flex {
margin: 15px;
justify-content: start;
gap: 1rem;
}
.flex-wrapped {
display: flex;
@ -626,13 +703,37 @@
.tabbed {
min-height: 20rem;
}
.cover {
border-radius: 10px;
.cover-container {
position: relative;
height: 20rem;
flex-shrink: 0;
margin-right: 15px;
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;
}
.block {

View 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);
}

View file

@ -21,7 +21,7 @@
display: flex;
align-items: center;
gap: 1rem;
padding: 0 1rem;
padding: 0 2rem;
transition: background 0.3s ease;
user-select: none;
}
@ -34,10 +34,13 @@
font-size: 2rem;
transform: translateX(0);
transition: transform 0.2s ease;
margin-right: 0.5rem;
padding-right: 0;
}
.navbar a:hover, .navbar a:active, .navbar a:focus {
text-decoration: none;
transform: translateX(-0.5rem);
margin-right: 0;
padding-right: 0.5rem;
}
.transparent .title {

View file

@ -21,7 +21,7 @@
display: flex;
align-items: center;
gap: 1rem;
padding: 0 1rem;
padding: 0 2rem;
transition: background 0.3s ease;
user-select: none;
}
@ -34,10 +34,13 @@
font-size: 2rem;
transform: translateX(0);
transition: transform 0.2s ease;
margin-right: 0.5rem;
padding-right: 0;
}
.navbar a:hover, .navbar a:active, .navbar a:focus {
text-decoration: none;
transform: translateX(-0.5rem);
margin-right: 0;
padding-right: 0.5rem;
}
.transparent .title {