mirror of
https://github.com/danbulant/Mangades
synced 2026-05-19 04:08:46 +00:00
add cover art and automatic splitting options to downloads along with preview
This commit is contained in:
parent
4bc9a5e733
commit
020bd06203
9 changed files with 397 additions and 154 deletions
3
src/global.d.ts
vendored
3
src/global.d.ts
vendored
|
|
@ -5,4 +5,7 @@ declare namespace App {
|
|||
message: string;
|
||||
code: string;
|
||||
}
|
||||
interface Window {
|
||||
goatcounter: any;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import request, { imageproxy } from "$lib/util/request";
|
||||
import request, { coverUrl, imageproxy } from "$lib/util/request";
|
||||
|
||||
// export var mangaId: string;
|
||||
export var additionalList: {
|
||||
|
|
@ -23,14 +23,18 @@
|
|||
Loading art
|
||||
{:then list}
|
||||
{#each list.data.sort((a, b) => a.attributes.volume - b.attributes.volume) as item}
|
||||
<img
|
||||
on:click={() => (selectedImage = `${imageproxy}https://uploads.mangadex.org/covers/${mangaId}/${item.attributes.fileName}.512.jpg`)}
|
||||
width="512"
|
||||
height="805"
|
||||
src="{imageproxy}https://uploads.mangadex.org/covers/{mangaId}/{item.attributes.fileName}.512.jpg"
|
||||
alt=""
|
||||
class="color"
|
||||
draggable={false} />
|
||||
<div class="img-container">
|
||||
<img
|
||||
on:click={() => (selectedImage = coverUrl(mangaId, item))}
|
||||
width="512"
|
||||
height="805"
|
||||
src={coverUrl(mangaId, item)}
|
||||
alt=""
|
||||
draggable={false} />
|
||||
<img class="img-backdrop"
|
||||
src={coverUrl(mangaId, item)}
|
||||
alt="">
|
||||
</div>
|
||||
{/each}
|
||||
{/await}
|
||||
{#each additionalList as item}
|
||||
|
|
@ -99,7 +103,7 @@
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.main > img:first-child {
|
||||
.main > :first-child {
|
||||
grid-column: 1 / span 2;
|
||||
grid-row: 1 / span 2;
|
||||
height: 20rem;
|
||||
|
|
|
|||
38
src/lib/components/fileItems.svelte
Normal file
38
src/lib/components/fileItems.svelte
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<script lang="ts">
|
||||
import { flip } from "svelte/animate";
|
||||
import { blur } from "svelte/transition";
|
||||
import Item from "./item.svelte";
|
||||
import { showType } from "./showTypeChooser.svelte";
|
||||
|
||||
export var chapters: {
|
||||
cover: string;
|
||||
title: string;
|
||||
chapters: string[]
|
||||
}[];
|
||||
</script>
|
||||
|
||||
|
||||
<div class="items" class:list={$showType == "list"}>
|
||||
{#each chapters as entry (entry.title)}
|
||||
<div animate:flip>
|
||||
<Item
|
||||
cover={entry.cover}
|
||||
title={entry.title}
|
||||
description="Chapters {entry.chapters.join(", ")}"
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
.items {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(7rem, 12rem));
|
||||
}
|
||||
.items.list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import SvelteMarkdown from "svelte-markdown";
|
||||
import { flip } from "svelte/animate";
|
||||
import { blur, crossfade } from "svelte/transition";
|
||||
import { showNsfw } from "./showNsfwChooser.svelte";
|
||||
import { showType } from "./showTypeChooser.svelte";
|
||||
|
|
@ -36,7 +35,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
Broken art
|
||||
Missing cover
|
||||
{/if}
|
||||
<div class="info">
|
||||
<h3>{title}</h3>
|
||||
|
|
|
|||
|
|
@ -6,15 +6,16 @@ interface Chapter {
|
|||
id: string,
|
||||
number: string,
|
||||
volume?: string,
|
||||
links: string[],
|
||||
hash: string,
|
||||
hashes: string[],
|
||||
baseUrl: string,
|
||||
links?: string[],
|
||||
hash?: string,
|
||||
hashes?: string[],
|
||||
// baseUrl?: string,
|
||||
title: string
|
||||
}
|
||||
|
||||
interface Opts {
|
||||
quality: string,
|
||||
coverUrl?: string
|
||||
quality?: string,
|
||||
file: WritableStream,
|
||||
title: string,
|
||||
id: string,
|
||||
|
|
@ -26,6 +27,30 @@ interface Opts {
|
|||
onerror?: (error: Error) => void
|
||||
}
|
||||
|
||||
let cache = new Map
|
||||
|
||||
/**
|
||||
* @param {string | Chapter | any} chapter
|
||||
* @returns {Promise<{ urls: string[], hashes: string[], hash: string[] }>}
|
||||
*/
|
||||
export function getURLs(chapter: string | Chapter): Promise<{ urls: string[], hashes: string[], hash: string }> {
|
||||
if(typeof chapter === "object") chapter = chapter.id;
|
||||
const quality = "data";
|
||||
if (!cache.has(chapter))
|
||||
cache.set(chapter, (async() => {
|
||||
console.log(chapter)
|
||||
const data = await request("at-home/server/" + chapter);
|
||||
console.log(data)
|
||||
let obj = {
|
||||
urls: data.chapter[quality].map(t => `${data.baseUrl}/${quality}/${data.chapter.hash}/${t}`),
|
||||
hashes: data.chapter[quality],
|
||||
hash: data.chapter.hash
|
||||
}
|
||||
return obj;
|
||||
})())
|
||||
return cache.get(chapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base generator, to be extended
|
||||
*/
|
||||
|
|
@ -42,15 +67,8 @@ export class BaseGenerator {
|
|||
* @param {string | Chapter | any} chapter
|
||||
* @returns {Promise<{ urls: string[], hashes: string[], hash: string[] }>}
|
||||
*/
|
||||
async getURLs(chapter) {
|
||||
if(typeof chapter === "object") chapter = chapter.id;
|
||||
const data = await request("at-home/server/" + chapter);
|
||||
console.log(data, this.opts);
|
||||
return {
|
||||
urls: data.chapter[this.opts.quality].map(t => `${data.baseUrl}/${this.opts.quality}/${data.chapter.hash}/${t}`),
|
||||
hashes: data.chapter[this.opts.quality],
|
||||
hash: data.chapter.hash
|
||||
}
|
||||
async getURLs(chapter: string | Chapter): Promise<{ urls: string[], hashes: string[], hash: string }> {
|
||||
return getURLs(chapter);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -58,7 +76,7 @@ export class BaseGenerator {
|
|||
* @param {Chapter} chapter
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async fetchImage(url, chapter) {
|
||||
async fetchImage(url: string, chapter: Chapter): Promise<Response> {
|
||||
var res;
|
||||
try {
|
||||
res = await fetch(imageproxy + url);
|
||||
|
|
@ -68,8 +86,8 @@ export class BaseGenerator {
|
|||
}
|
||||
if(Math.floor(res.status / 100) !== 2) {
|
||||
for(var i = 0; i < RETRY_LIMIT; i++) {
|
||||
chapter.baseUrl = await this.getURLs(chapter);
|
||||
res = await fetch(chapter.baseUrl + "/" + url);
|
||||
let baseUrl = await this.getURLs(chapter);
|
||||
res = await fetch(baseUrl + "/" + url);
|
||||
if(Math.floor(res.status / 100) === 2) return res;
|
||||
}
|
||||
throw new Error("Retry limit reached");
|
||||
|
|
|
|||
|
|
@ -34,6 +34,14 @@ export class CBZGenerator extends BaseGenerator {
|
|||
chapter.number = chapterI;
|
||||
}
|
||||
const imageCountLength = chapter.links.length.toString().length;
|
||||
if(this.opts.coverUrl) {
|
||||
let coverExtension = this.opts.coverUrl.substr(this.opts.coverUrl.lastIndexOf(".") + 1);
|
||||
const cover = new ZipPassThrough(`${this.opts.title} 00 cover.${coverExtension}`);
|
||||
this.zip.add(cover);
|
||||
const res = await fetch(this.opts.coverUrl);
|
||||
const data = new Uint8Array(await res.arrayBuffer());
|
||||
cover.push(data, true);
|
||||
}
|
||||
for(const i in chapter.links) {
|
||||
let url = chapter.links[i];
|
||||
let hash = chapter.hashes[i];
|
||||
|
|
@ -57,5 +65,6 @@ export class CBZGenerator extends BaseGenerator {
|
|||
}
|
||||
}
|
||||
this.zip.end();
|
||||
this.callback(-1, -1, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -50,6 +50,27 @@ export class EpubGenerator extends BaseGenerator {
|
|||
|
||||
this.callback(); // signals the template is ready
|
||||
|
||||
if(this.opts.coverUrl) {
|
||||
const res = await fetch(this.opts.coverUrl);
|
||||
const cover = new ZipPassThrough("OEBPS/cover." + this.opts.coverUrl.substr(this.opts.coverUrl.lastIndexOf(".") + 1));
|
||||
this.zip.add(cover);
|
||||
const data = new Uint8Array(await res.arrayBuffer());
|
||||
cover.push(data, true);
|
||||
const textContent = new ZipPassThrough("OEBPS/cover.xhtml");
|
||||
this.zip.add(textContent);
|
||||
textContent.push(enc.encode(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
|
||||
<head>
|
||||
<title>Cover</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<meta name="EPB-UUID" content=""/>
|
||||
</head>
|
||||
<body>
|
||||
<img style="margin:auto;height:100%;" src="cover.${this.opts.coverUrl.substr(this.opts.coverUrl.lastIndexOf(".") + 1)}" />
|
||||
</body>
|
||||
</html>`), true);
|
||||
}
|
||||
|
||||
for(const chapterI in this.opts.chapters) {
|
||||
const chapter = this.opts.chapters[chapterI];
|
||||
if(chapter.number == null || chapter.number == undefined) chapter.number = chapterI;
|
||||
|
|
@ -131,6 +152,8 @@ export class EpubGenerator extends BaseGenerator {
|
|||
</metadata>
|
||||
|
||||
<manifest>
|
||||
${this.opts.coverUrl ? `<item id="icover" href="cover.${this.opts.coverUrl.substr(this.opts.coverUrl.lastIndexOf(".") + 1)}" media-type="image/${this.opts.coverUrl.substr(this.opts.coverUrl.lastIndexOf(".") + 1) === "jpg" ? "jpeg" : this.opts.coverUrl.substr(this.opts.coverUrl.lastIndexOf(".") + 1)}"/>` : ""}
|
||||
${this.opts.coverUrl ? `<item id="pcover" href="cover.xhtml" media-type="application/xhtml+xml" />` : ""}
|
||||
${this.hashes.map((t, i) => ` <item id="i${i}" href="${t.hash}" fallback="fallback" media-type="image/${t.hash.substr(t.hash.lastIndexOf(".") + 1) === "jpg" ? "jpeg" : t.hash.substr(t.hash.lastIndexOf(".") + 1)}"/>`).join("\n")}
|
||||
${this.hashes.map((t, i) => ` <item id="p${i}" href="${i}.xhtml" media-type="application/xhtml+xml" />`).join("\n")}
|
||||
|
||||
|
|
@ -138,6 +161,7 @@ export class EpubGenerator extends BaseGenerator {
|
|||
</manifest>
|
||||
|
||||
<spine toc="ncxtoc">
|
||||
${this.opts.coverUrl ? `<itemref idref="pcover" linear="yes" />` : ""}
|
||||
${this.hashes.map((t, i) => ` <itemref idref="p${i}" linear="yes" />`).join("\n")}
|
||||
<itemref idref="fallback" linear="no" />
|
||||
</spine>
|
||||
|
|
@ -165,6 +189,12 @@ export class EpubGenerator extends BaseGenerator {
|
|||
</docAuthor>
|
||||
|
||||
<navMap>
|
||||
${this.opts.coverUrl ? `<navPoint id="cover" playOrder="0">
|
||||
<navLabel>
|
||||
<text>Cover</text>
|
||||
</navLabel>
|
||||
<content src="cover.xhtml"/>
|
||||
</navPoint>` : ""}
|
||||
${this.opts.chapters.map((t, i) => `
|
||||
<navPoint id="p${this.hashes.findIndex(h => h.hash === t.hashes[0])}" playOrder="${i + 1}">
|
||||
<navLabel>
|
||||
|
|
|
|||
|
|
@ -18,4 +18,8 @@ function request(endpoint, query, type = "GET", body) {
|
|||
}).then(resp => resp.json());
|
||||
}
|
||||
|
||||
export function coverUrl(mangaId, item) {
|
||||
return `${imageproxy}https://uploads.mangadex.org/covers/${mangaId}/${item.attributes.fileName}.512.jpg`
|
||||
}
|
||||
|
||||
export default request;
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
import Chapter from "$lib/components/chapter.svelte";
|
||||
import { EpubGenerator } from "$lib/util/generateEpub";
|
||||
import { CBZGenerator } from "$lib/util/generateCbz";
|
||||
import request, { imageproxy } from "$lib/util/request";
|
||||
import request, { coverUrl, imageproxy } from "$lib/util/request";
|
||||
import { arraysEqual } from "$lib/util/arrays";
|
||||
import Tabs from "$lib/components/tabs/tabs.svelte";
|
||||
import { slide } from "svelte/transition";
|
||||
|
|
@ -18,6 +18,23 @@
|
|||
import RelatedManga from "./relatedManga.svelte";
|
||||
import { anilistInfo } from "./anilistInfo";
|
||||
import { isLogedIn } from "$lib/util/anilist";
|
||||
import { BaseGenerator, getURLs } from "$lib/util/baseGenerator";
|
||||
import FileItems from "$lib/components/fileItems.svelte";
|
||||
import ShowTypeChooser from "$lib/components/showTypeChooser.svelte";
|
||||
|
||||
enum CoverArt {
|
||||
AutoVolume = "auto-volume",
|
||||
FirstPage = "first-page"
|
||||
}
|
||||
enum Group {
|
||||
Single = "single",
|
||||
Chapter = "chapter",
|
||||
Volume = "volume"
|
||||
}
|
||||
enum Format {
|
||||
Epub = "epub",
|
||||
Cbz = "cbz"
|
||||
}
|
||||
|
||||
export var data;
|
||||
|
||||
|
|
@ -30,7 +47,7 @@
|
|||
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];
|
||||
|
||||
const defaultLanguages = ["en", "uk"];
|
||||
const defaultLanguages = ["en"];
|
||||
let languages = defaultLanguages;
|
||||
|
||||
let anilistData;
|
||||
|
|
@ -121,40 +138,11 @@
|
|||
processQueue();
|
||||
}
|
||||
|
||||
const generators = {
|
||||
const generators: Record<Format, typeof BaseGenerator> = {
|
||||
epub: EpubGenerator,
|
||||
cbz: CBZGenerator
|
||||
}
|
||||
|
||||
function createGenerator(chapter, file) {
|
||||
return new generators[format]({
|
||||
file,
|
||||
id: window.location.toString() + "-" + chapter.id,
|
||||
language: chapter.attributes.translatedLanguage,
|
||||
updatedAt: chapter.attributes.updatedAt,
|
||||
title,
|
||||
author: relationships.find(t => t.type === "author").attributes.name || "Unknown",
|
||||
chapters: [{
|
||||
id: chapter.id,
|
||||
number: chapter.attributes.chapter,
|
||||
volume: chapter.attributes.volume
|
||||
}]
|
||||
});
|
||||
}
|
||||
async function downloadSingle(chapter) {
|
||||
const file = streamSaver.createWriteStream(`${title} ${chapter.attributes.chapter}.${format}`, {
|
||||
writableStrategy: undefined, // (optional)
|
||||
readableStrategy: undefined, // (optional)
|
||||
});
|
||||
|
||||
const generator = createGenerator(chapter, file)
|
||||
|
||||
console.log(generator);
|
||||
queue.push(generator);
|
||||
processQueue();
|
||||
}
|
||||
|
||||
var format = "cbz";
|
||||
var format: Format = Format.Cbz;
|
||||
var selected = [];
|
||||
function select(chapter) {
|
||||
console.log("Selecting", chapter);
|
||||
|
|
@ -170,63 +158,156 @@
|
|||
text = defaultText;
|
||||
}
|
||||
}
|
||||
function downloadMulti() {
|
||||
selected.sort((a, b) => a.attributes.chapter - b.attributes.chapter);
|
||||
if(!selected.length) return;
|
||||
if(selected.length === 1) {
|
||||
downloadSingle(selected.shift());
|
||||
selected = [];
|
||||
return;
|
||||
}
|
||||
const file = streamSaver.createWriteStream(`${title}.${format}`, {
|
||||
writableStrategy: undefined, // (optional)
|
||||
readableStrategy: undefined, // (optional)
|
||||
});
|
||||
const generator = new generators[format]({
|
||||
file,
|
||||
id: window.location.toString(),
|
||||
language: selected[0].attributes.translatedLanguage,
|
||||
updatedAt: new Date,
|
||||
title: title,
|
||||
author: relationships.find(t => t.type === "author").attributes.name || "Unknown",
|
||||
chapters: selected.map(chapter => ({
|
||||
id: chapter.id,
|
||||
title: chapter.attributes.title,
|
||||
number: chapter.attributes.chapter,
|
||||
volume: chapter.attributes.volume
|
||||
}))
|
||||
});
|
||||
|
||||
console.log(generator);
|
||||
queue.push(generator);
|
||||
selected = [];
|
||||
processQueue();
|
||||
function splitSelectionIntoFiles(selected, group: Group) {
|
||||
if(!selected.length) return [];
|
||||
selected.sort((a, b) => a.attributes.chapter - b.attributes.chapter);
|
||||
let files = [];
|
||||
|
||||
switch(group) {
|
||||
case Group.Single:
|
||||
files[0] = selected
|
||||
break;
|
||||
case Group.Chapter:
|
||||
files = selected.map(chapter => [chapter]);
|
||||
break;
|
||||
case Group.Volume:
|
||||
files = selected.reduce((acc, chapter) => {
|
||||
let last = acc[acc.length - 1];
|
||||
if(!last || last[0].attributes.volume !== chapter.attributes.volume) {
|
||||
acc.push([chapter]);
|
||||
} else {
|
||||
last.push(chapter);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
break;
|
||||
}
|
||||
return files
|
||||
}
|
||||
function downloadSeparate() {
|
||||
selected.sort((a, b) => a.attributes.chapter - b.attributes.chapter);
|
||||
if(!selected.length) return;
|
||||
if(selected.length === 1) {
|
||||
downloadSingle(selected.shift());
|
||||
selected = [];
|
||||
return;
|
||||
|
||||
function getNumberRanges(numbers) {
|
||||
let ranges = [];
|
||||
let last = numbers[0];
|
||||
let start = last;
|
||||
for(let i = 1; i < numbers.length; i++) {
|
||||
if(numbers[i] - last > 1) {
|
||||
ranges.push([start, last]);
|
||||
start = numbers[i];
|
||||
}
|
||||
last = numbers[i];
|
||||
}
|
||||
ranges.push([start, last]);
|
||||
return ranges;
|
||||
}
|
||||
|
||||
function nameRanges(ranges) {
|
||||
return ranges.map(range => {
|
||||
if(range[0] === range[1]) return range[0];
|
||||
return `${range[0]}-${range[1]}`;
|
||||
}).join(", ");
|
||||
}
|
||||
|
||||
function getNameOf(chapters) {
|
||||
if(!chapters) return "Unknown";
|
||||
if(chapters.length == 1) {
|
||||
return `ch ${chapters[0].attributes.chapter}`;
|
||||
}
|
||||
// todo: check that it's the whole volume
|
||||
let isSingleVolume = chapters[0].attributes.volume && chapters.find(t => t.attributes.volume !== chapters[0].attributes.volume) === undefined;
|
||||
if(isSingleVolume) {
|
||||
return `vol ${chapters[0].attributes.volume}`;
|
||||
}
|
||||
|
||||
for (const chapter of selected) {
|
||||
const file = streamSaver.createWriteStream(`${title} ${chapter.attributes.chapter}.${format}`, {
|
||||
let chapterNumbers = chapters.map(t => t.attributes.chapter);
|
||||
let ranges = getNumberRanges(chapterNumbers);
|
||||
return `ch ${nameRanges(ranges)}`;
|
||||
}
|
||||
|
||||
function coverForVolumeFromArt(volume, art) {
|
||||
if(!volume) volume = "1"
|
||||
let cover = art.data.find(t => t.attributes.volume === volume);
|
||||
if(!cover) return null;
|
||||
return coverUrl(mangaId, cover);
|
||||
}
|
||||
|
||||
async function previewItems(selected, groupMode, coverArtMode, format) {
|
||||
let files = splitSelectionIntoFiles(selected, groupMode);
|
||||
// const author = relationships.find(t => t.type === "author").attributes.name || "Unknown";
|
||||
let items: {
|
||||
cover: string;
|
||||
title: string;
|
||||
chapters: string[]
|
||||
}[] = [];
|
||||
|
||||
switch(coverArtMode) {
|
||||
case CoverArt.FirstPage:
|
||||
items = await Promise.all(files.map(async file => {
|
||||
let urls = await getURLs(file[0])
|
||||
console.log(file, urls)
|
||||
return {
|
||||
title: `${title} - ${getNameOf(file)}.${format}`,
|
||||
chapters: file.map(chapter => `${chapter.attributes.chapter}`),
|
||||
cover: imageproxy + urls.urls[0]
|
||||
};
|
||||
}));
|
||||
break;
|
||||
case CoverArt.AutoVolume:
|
||||
let art = await list;
|
||||
items = files.map(file => {
|
||||
return {
|
||||
title: `${title} - ${getNameOf(file)}.${format}`,
|
||||
chapters: file.map(chapter => `${chapter.attributes.chapter}`),
|
||||
cover: coverForVolumeFromArt(file[0].attributes.volume, art),
|
||||
};
|
||||
});
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
async function downloadMulti() {
|
||||
if(!selected.length) return;
|
||||
|
||||
// @ts-ignore
|
||||
if(window.goatcounter) window.goatcounter.count({
|
||||
path: window.location.pathname + "/download",
|
||||
title: "Download " + title,
|
||||
});
|
||||
else console.warn("Page change; GoatCounter not loaded (yet?)", window.location.pathname);
|
||||
|
||||
let files = splitSelectionIntoFiles(selected, group);
|
||||
const author = relationships.find(t => t.type === "author").attributes.name || "Unknown";
|
||||
let art = coverArt == CoverArt.AutoVolume ? await list : null;
|
||||
|
||||
for(let file of files) {
|
||||
let name = `${title} - ${getNameOf(file)}`;
|
||||
const stream = streamSaver.createWriteStream(`${name}.${format}`, {
|
||||
writableStrategy: undefined, // (optional)
|
||||
readableStrategy: undefined, // (optional)
|
||||
});
|
||||
|
||||
const generator = createGenerator(chapter, file)
|
||||
|
||||
const generator = new generators[format]({
|
||||
file: stream,
|
||||
id: window.location.toString(),
|
||||
language: file[0].attributes.translatedLanguage,
|
||||
updatedAt: file.map(t => t.attributes.updatedAt).sort((a, b) => a - b)[0],
|
||||
title: name,
|
||||
author,
|
||||
coverUrl: art && coverForVolumeFromArt(file[0].attributes.volume, art),
|
||||
chapters: file.map(chapter => ({
|
||||
id: chapter.id,
|
||||
title: chapter.attributes.title,
|
||||
number: chapter.attributes.chapter,
|
||||
volume: chapter.attributes.volume
|
||||
}))
|
||||
});
|
||||
|
||||
console.log(generator);
|
||||
queue.push(generator);
|
||||
}
|
||||
|
||||
queue = queue;
|
||||
selected = [];
|
||||
processQueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BeforeUnloadEvent} e
|
||||
*/
|
||||
|
|
@ -286,14 +367,12 @@
|
|||
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 from anilist",
|
||||
// color: data.coverImage.color,
|
||||
height: 1,
|
||||
width: 3
|
||||
});
|
||||
|
|
@ -313,7 +392,7 @@
|
|||
if(loadingNextPage) return;
|
||||
console.log("Loading next page");
|
||||
loadingNextPage = true;
|
||||
chapters = await getMangaChapters(mangaId);
|
||||
chapters = await getMangaChapters(mangaId, languages);
|
||||
await tick();
|
||||
loadingNextPage = false;
|
||||
swiper.slideToClosest();
|
||||
|
|
@ -330,7 +409,14 @@
|
|||
let uniqueChapterCount;
|
||||
$: uniqueChapterCount = chapters?.data.filter((t, i, a) => a.findIndex(t2 => Math.floor(t2.attributes.chapter) === Math.floor(t.attributes.chapter)) === i).length;
|
||||
|
||||
let list: any;
|
||||
let list: Promise<{ data: {
|
||||
id: string,
|
||||
type: string,
|
||||
attributes: {
|
||||
fileName: string,
|
||||
volume: string,
|
||||
}
|
||||
}[] }>;
|
||||
$: list = request(
|
||||
"cover?limit=50&manga[]=" + mangaId + languages.map(t => "&locales[]=" + t).join("")
|
||||
);
|
||||
|
|
@ -345,6 +431,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
let coverArt: CoverArt = CoverArt.FirstPage;
|
||||
let group: Group = Group.Single;
|
||||
</script>
|
||||
|
||||
<svelte:window on:beforeunload={beforeUnload} bind:innerWidth={width} bind:scrollY bind:innerHeight />
|
||||
|
|
@ -500,28 +589,79 @@
|
|||
>
|
||||
<SwiperSlide>
|
||||
<div class="chapter-list" style="min-height: 30rem;">
|
||||
<div class="state {state}">
|
||||
<div class="progress" style="width: {progress * 100}%;"></div>
|
||||
|
||||
<p>
|
||||
{text}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if queue.length > 0}
|
||||
{#if queue && queue.length > 0}
|
||||
<p><i>{queue.length} downloads queued.</i></p>
|
||||
{/if}
|
||||
|
||||
|
||||
<div class="download">
|
||||
<select name="format" bind:value={format} id="select-format">
|
||||
<option value="cbz"><b>.cbz</b> Comic Book Zip</option>
|
||||
<option value="epub"><b>.epub</b> Electronic publication</option>
|
||||
</select>
|
||||
<button disabled={!selected.length} on:click={downloadMulti}>Download</button>
|
||||
<button disabled={!selected.length} on:click={downloadSeparate}>Download Separate</button>
|
||||
</div>
|
||||
<div class="download-options">
|
||||
<div class="status">
|
||||
<div class="state {state}">
|
||||
<div class="progress" style="width: {progress * 100}%;"></div>
|
||||
|
||||
<p>
|
||||
{text}
|
||||
</p>
|
||||
</div>
|
||||
<button class="download-btn" disabled={!selected.length} on:click={downloadMulti}>Download</button>
|
||||
</div>
|
||||
<div class="options">
|
||||
<fieldset>
|
||||
<legend>Format</legend>
|
||||
|
||||
<label>
|
||||
<input type="radio" name="format" value={Format.Cbz} bind:group={format}>
|
||||
<b>.<abbr title="Comic Book Zip">cbz</abbr></b>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="format" value={Format.Epub} bind:group={format}>
|
||||
<b>.<abbr title="Electronic Publication">epub</abbr></b>
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Cover art</legend>
|
||||
|
||||
<label>
|
||||
<input type="radio" name="cover" value={CoverArt.FirstPage} bind:group={coverArt}>
|
||||
|
||||
First page
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="cover" value={CoverArt.AutoVolume} bind:group={coverArt}>
|
||||
|
||||
From art, by volume
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Split files</legend>
|
||||
|
||||
<label>
|
||||
<input type="radio" name="group" value={Group.Single} bind:group={group}>
|
||||
|
||||
Single file
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="group" value={Group.Chapter} bind:group={group}>
|
||||
|
||||
By chapter
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="group" value={Group.Volume} bind:group={group}>
|
||||
|
||||
By volume
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
<p class="note">Splitting into multiple files may require browser permission.</p>
|
||||
|
||||
<h3>Preview</h3>
|
||||
|
||||
<ShowTypeChooser />
|
||||
|
||||
{#await previewItems(selected, group, coverArt, format)}
|
||||
<div>Loading preview...</div>
|
||||
{:then items}
|
||||
<FileItems chapters={items} />
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
|
|
@ -548,7 +688,7 @@
|
|||
<table>
|
||||
<tbody>
|
||||
{#each chapters.data as chapter}
|
||||
<Chapter read={alReadProgress && alReadProgress >= parseInt(chapter.attributes.chapter)} progress={(progressMap.get(chapter.id) || 0) / chapter.attributes.pages} {chapter} disabledDownload={!!progress} selected={selected.includes(chapter)} on:select={() => select(chapter)} on:download={() => downloadSingle(chapter)} />
|
||||
<Chapter read={alReadProgress && alReadProgress >= parseInt(chapter.attributes.chapter)} progress={(progressMap.get(chapter.id) || 0) / chapter.attributes.pages} {chapter} disabledDownload={!!progress} selected={selected.includes(chapter)} on:select={() => select(chapter)} />
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
@ -556,7 +696,7 @@
|
|||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div class="art-list" style="min-height: 30rem; overflow: hidden;">
|
||||
<div class="art-list" style="min-height: 30rem;">
|
||||
<ArtList {mangaId} {list} bind:selectedImage additionalList={additionalImages} />
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
|
|
@ -583,6 +723,7 @@
|
|||
{#if manga.links.al}
|
||||
<a target="_blank" href="https://anilist.co/manga/{manga.links.al}"><Favicon url="https://anilist.co" /> Anilist</a> <br>
|
||||
{/if}
|
||||
<a target="_blank" href="https://mangadex.org/title/{mangaId}"><Favicon url="https://mangadex.org"/> Mangadex.org</a> <br>
|
||||
{#if manga.links.ap}
|
||||
<a target="_blank" href="https://www.anime-planet.com/manga/{manga.links.ap}"><Favicon url="https://anime-planet.com" /> Animeplanet</a> <br>
|
||||
{/if}
|
||||
|
|
@ -613,8 +754,6 @@
|
|||
{#if manga.links.engtl}
|
||||
<a target="_blank" href="{manga.links.engtl}"><Favicon url={manga.links.engtl} /> engtl</a> <br>
|
||||
{/if}
|
||||
|
||||
<a target="_blank" href="https://mangadex.org/title/{mangaId}"><Favicon url="https://mangadex.org"/> Mangadex.org</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -653,7 +792,6 @@
|
|||
}
|
||||
.langs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin: 0 1rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
|
@ -664,6 +802,9 @@
|
|||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
.langs button.enabled {
|
||||
background: rgb(107, 107, 107);
|
||||
|
|
@ -866,14 +1007,14 @@
|
|||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
padding: 10px;
|
||||
background: rgb(214, 214, 214);
|
||||
background: rgb(64,64,64);
|
||||
border-radius: 5px 0 5px 5px;
|
||||
}
|
||||
:global(.dark main > .copyright.copyright.copyright) {
|
||||
background: rgb(64, 64, 64);
|
||||
}
|
||||
.copyright-header {
|
||||
background: rgb(214, 214, 214);
|
||||
background: rgb(64,64,64);
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
user-select: none;
|
||||
|
|
@ -885,18 +1026,6 @@
|
|||
.copyright-header-active {
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
.download {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.download select {
|
||||
flex-grow: 1;
|
||||
margin-inline: 5px;
|
||||
}
|
||||
.download button {
|
||||
margin-inline: 5px;
|
||||
}
|
||||
main {
|
||||
font-size: 1.1rem;
|
||||
position: relative;
|
||||
|
|
@ -917,28 +1046,37 @@
|
|||
|
||||
.state {
|
||||
border-radius: 10px;
|
||||
border-width: 4px;
|
||||
border-style: solid;
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
transition: all .3s;
|
||||
flex-grow: 1;
|
||||
}
|
||||
:global(.dark .state) {
|
||||
color: black;
|
||||
}
|
||||
.download .status {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
.download .options {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
.download .note {
|
||||
font-size: 0.8rem;
|
||||
color: rgb(175, 175, 175);
|
||||
}
|
||||
|
||||
.state.idle {
|
||||
background: rgb(140, 209, 255);
|
||||
border-color: rgb(77, 184, 255);
|
||||
background: transparent;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.state.active {
|
||||
background: rgb(255, 255, 81);
|
||||
border-color: yellow;
|
||||
}
|
||||
.state.error {
|
||||
background: rgb(255, 103, 103);
|
||||
border-color: rgb(255, 59, 59);
|
||||
}
|
||||
|
||||
.state p {
|
||||
|
|
@ -957,6 +1095,6 @@
|
|||
border-bottom-left-radius: 10px;
|
||||
}
|
||||
.state.active .progress {
|
||||
background: rgb(140, 209, 255);
|
||||
background: rgba(255,255,255, .1);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue