21 Commits

Author SHA1 Message Date
Shaun Tenner
e0b4e3edf0 Update +page.svelte 2025-04-15 15:28:10 +02:00
Shaun Tenner
683a456202 Merge pull request #50 from Gnathonic/improve_compatibility_with_extensions
Improve language extension compatibility
2025-02-07 19:02:32 +02:00
Shaun Tenner
9a4c86870a Merge pull request #49 from v2lmmj04/patch-1
Update underscore prefix for image filenames
2025-02-07 18:38:34 +02:00
Gnathonic
bc5b9ccd16 Add keyed blocks to enhance MangaPage reactivity
Keyed blocks improve DOM update handling. This improves compatibility with some language learning extensions. For example, this prevents Migaku from persisting textboxes from previous pages as one pages through their comics.
2025-01-24 18:19:35 -07:00
v2lmmj04
6c16008aae Change prefix to "mokuro_*"
This was an alternative I was thinking of initially since it better namespaces the files, but wasn't sure about.

I saw this commit ended up going with this approach, so I'm aligning the PR to it in the end:

6d550cbe8a
2025-01-09 19:51:49 -08:00
v2lmmj04
b281052908 Remove underscore prefix for image filenames 2025-01-05 14:51:37 -08:00
Shaun Tenner
52619b1a9b Merge pull request #42 from nyanSpruk/patch-1
Update README.md - typo
2024-11-16 10:18:42 +02:00
Nik Jan Špruk
a30c526a63 Update README.md - typo
Fixed typo from useage to usage.
2024-11-15 20:55:48 +01:00
Shaun Tenner
7fb55f5399 Merge pull request #41 from AnonMiraj/main
Add way to import from other sources
2024-11-14 19:34:43 +02:00
AnonMiraj
6220b6172e Add way to import from other sources 2024-11-13 20:54:10 +02:00
ZXY101
a9f7bca27c Fix title text 2024-08-19 22:02:24 +02:00
Shaun Tenner
0ebeb46171 Merge pull request #23 from kenrick95/kenrick/invert-colors-settings
Settings to invert colors of the images
2024-07-10 23:46:54 +02:00
Kenrick
1fdbdf5185 Settings to invert colors of the images
Useful for novels, for reading in dark mode
2024-07-08 23:15:17 +08:00
ZXY101
109241e584 Add default fullscreen setting 2024-05-25 22:30:16 +02:00
ZXY101
ec632b534b Fix progress 2024-05-18 19:25:19 +02:00
ZXY101
bc7cb721d3 Update loading 2024-05-18 17:26:12 +02:00
ZXY101
5a5dc189fb Improve download 2024-05-18 17:08:40 +02:00
ZXY101
7baf586d3b Merge branch 'main' of https://github.com/ZXY101/z-reader 2024-05-18 16:02:36 +02:00
ZXY101
3e487caad9 Update version 2024-05-18 16:02:19 +02:00
Shaun Tenner
44b8989f2e Merge pull request #17 from ZXY101/gdrive-integration
Add google drive intergration
2024-05-18 15:47:06 +02:00
ZXY101
b4573fd955 Complete Gdrive integration 2024-05-18 15:44:16 +02:00
19 changed files with 227 additions and 221 deletions

View File

@@ -10,7 +10,7 @@ https://github.com/ZXY101/mokuro-reader/assets/39561296/45a214a8-3f69-461c-87d7-
- Anki connect integration & image cropping
- Installation and offline support
## Useage:
## Usage:
You can find the reader hosted [here](https://reader.mokuro.app/).
To import your manga, process it with mokuro and then upload your manga along with the generated `.mokuro` file.

71
package-lock.json generated
View File

@@ -1,15 +1,16 @@
{
"name": "z-reader",
"version": "0.9.0",
"version": "0.9.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "z-reader",
"version": "0.9.0",
"version": "0.9.1",
"dependencies": {
"@types/gapi": "^0.0.47",
"@types/google.accounts": "^0.0.14",
"@types/google.picker": "^0.0.42",
"@vercel/analytics": "^1.1.0",
"@zip.js/zip.js": "^2.7.20",
"dexie": "^4.0.1-alpha.25",
@@ -484,10 +485,11 @@
}
},
"node_modules/@fastify/busboy": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz",
"integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
}
@@ -688,11 +690,12 @@
}
},
"node_modules/@sveltejs/kit": {
"version": "1.30.3",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.30.3.tgz",
"integrity": "sha512-0DzVXfU4h+tChFvoc8C61IqErCyskD4ydSIDjpKS2lYlEzIYrtYrY7juSqACFxqcvZAnOEXvSY+zZ8br0+ZMMg==",
"version": "1.30.4",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.30.4.tgz",
"integrity": "sha512-JSQIQT6XvdchCRQEm7BABxPC56WP5RYVONAi+09S8tmzeP43fBsRlr95bFmsTQM2RHBldfgQk+jgdnsKI75daA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@sveltejs/vite-plugin-svelte": "^2.5.0",
"@types/cookie": "^0.5.1",
@@ -706,7 +709,7 @@
"set-cookie-parser": "^2.6.0",
"sirv": "^2.0.2",
"tiny-glob": "^0.2.9",
"undici": "~5.26.2"
"undici": "^5.28.3"
},
"bin": {
"svelte-kit": "svelte-kit.js"
@@ -804,6 +807,11 @@
"resolved": "https://registry.npmjs.org/@types/google.accounts/-/google.accounts-0.0.14.tgz",
"integrity": "sha512-HqIVkVzpiLWhlajhQQd4rIV7czanFvXblJI2J1fSrL+VKQuQwwZ63m35D/mI0flsqKE6p/hNrAG0Yn4FD6JvNA=="
},
"node_modules/@types/google.picker": {
"version": "0.0.42",
"resolved": "https://registry.npmjs.org/@types/google.picker/-/google.picker-0.0.42.tgz",
"integrity": "sha512-7Ut1zDLCGNhyN+0fuIcncpBP9eUsIjLKz6qmVKvdkBUi1EzAskxTcIZcU/w2cO1ens/hEXlqeLd5qAKs/Kqqyw=="
},
"node_modules/@types/json-schema": {
"version": "7.0.12",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
@@ -3911,10 +3919,11 @@
}
},
"node_modules/undici": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.26.5.tgz",
"integrity": "sha512-cSb4bPFd5qgR7qr2jYAi0hlX9n5YKK2ONKkLFkxl+v/9BvC0sOpZjBHDBSXc5lWAf5ty9oZdRXytBIHzgUcerw==",
"version": "5.28.4",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
"integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@fastify/busboy": "^2.0.0"
},
@@ -3968,10 +3977,11 @@
"dev": true
},
"node_modules/vite": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz",
"integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==",
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.18.10",
"postcss": "^8.4.27",
@@ -4306,9 +4316,9 @@
"dev": true
},
"@fastify/busboy": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz",
"integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
"dev": true
},
"@floating-ui/core": {
@@ -4474,9 +4484,9 @@
}
},
"@sveltejs/kit": {
"version": "1.30.3",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.30.3.tgz",
"integrity": "sha512-0DzVXfU4h+tChFvoc8C61IqErCyskD4ydSIDjpKS2lYlEzIYrtYrY7juSqACFxqcvZAnOEXvSY+zZ8br0+ZMMg==",
"version": "1.30.4",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.30.4.tgz",
"integrity": "sha512-JSQIQT6XvdchCRQEm7BABxPC56WP5RYVONAi+09S8tmzeP43fBsRlr95bFmsTQM2RHBldfgQk+jgdnsKI75daA==",
"dev": true,
"requires": {
"@sveltejs/vite-plugin-svelte": "^2.5.0",
@@ -4491,7 +4501,7 @@
"set-cookie-parser": "^2.6.0",
"sirv": "^2.0.2",
"tiny-glob": "^0.2.9",
"undici": "~5.26.2"
"undici": "^5.28.3"
}
},
"@sveltejs/vite-plugin-svelte": {
@@ -4564,6 +4574,11 @@
"resolved": "https://registry.npmjs.org/@types/google.accounts/-/google.accounts-0.0.14.tgz",
"integrity": "sha512-HqIVkVzpiLWhlajhQQd4rIV7czanFvXblJI2J1fSrL+VKQuQwwZ63m35D/mI0flsqKE6p/hNrAG0Yn4FD6JvNA=="
},
"@types/google.picker": {
"version": "0.0.42",
"resolved": "https://registry.npmjs.org/@types/google.picker/-/google.picker-0.0.42.tgz",
"integrity": "sha512-7Ut1zDLCGNhyN+0fuIcncpBP9eUsIjLKz6qmVKvdkBUi1EzAskxTcIZcU/w2cO1ens/hEXlqeLd5qAKs/Kqqyw=="
},
"@types/json-schema": {
"version": "7.0.12",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
@@ -6717,9 +6732,9 @@
"dev": true
},
"undici": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.26.5.tgz",
"integrity": "sha512-cSb4bPFd5qgR7qr2jYAi0hlX9n5YKK2ONKkLFkxl+v/9BvC0sOpZjBHDBSXc5lWAf5ty9oZdRXytBIHzgUcerw==",
"version": "5.28.4",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
"integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
"dev": true,
"requires": {
"@fastify/busboy": "^2.0.0"
@@ -6751,9 +6766,9 @@
"dev": true
},
"vite": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz",
"integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==",
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dev": true,
"requires": {
"esbuild": "^0.18.10",

View File

@@ -1,6 +1,6 @@
{
"name": "z-reader",
"version": "0.9.0",
"version": "0.9.1",
"private": true,
"scripts": {
"dev": "vite dev",
@@ -39,6 +39,7 @@
"dependencies": {
"@types/gapi": "^0.0.47",
"@types/google.accounts": "^0.0.14",
"@types/google.picker": "^0.0.42",
"@vercel/analytics": "^1.1.0",
"@zip.js/zip.js": "^2.7.20",
"dexie": "^4.0.1-alpha.25",

View File

@@ -6,7 +6,7 @@
<link rel="manifest" crossorigin="use-credentials" href="manifest.json" />
<meta
name="viewport"
content="width=device-width, height=device-height,initial-scale=1, minimum-scale=1"
content="width=device-width, height=device-height,initial-scale=1, minimum-scale=1, user-scalable=no"
/>
<script src="https://apis.google.com/js/api.js"></script>
<script src="https://accounts.google.com/gsi/client"></script>

View File

@@ -102,7 +102,7 @@ export async function updateLastCard(imageData: string | null | undefined, sente
id,
fields,
picture: {
filename: `_${id}.webp`,
filename: `mokuro_${id}.webp`,
data: imageData.split(';base64,')[1],
fields: [pictureField],
},

View File

@@ -18,7 +18,9 @@
class="object-contain sm:w-[250px] sm:h-[350px] bg-black border-gray-900 border"
/>
{/if}
<p class="font-semibold">{manga.mokuroData.title}</p>
<p class="font-semibold sm:w-[250px] line-clamp-1">
{manga.mokuroData.title}
</p>
</div>
</a>
{/if}

View File

@@ -37,7 +37,7 @@
<div class="flex md:order-2 gap-5">
<UserSettingsSolid class="hover:text-primary-700" on:click={openSettings} />
<UploadSolid class="hover:text-primary-700" on:click={() => (uploadModalOpen = true)} />
<!-- <CloudArrowUpOutline class="hover:text-primary-700" on:click={() => goto('/cloud')} /> -->
<CloudArrowUpOutline class="hover:text-primary-700" on:click={() => goto('/cloud')} />
</div>
</Navbar>
</div>

View File

@@ -35,17 +35,10 @@
<div
draggable="false"
style:width={`${window.innerWidth}px`}
style:height={`${(window.innerWidth * page.img_height) / page.img_width}px`}
style:width={`${page.img_width}px`}
style:height={`${page.img_height}px`}
style:background-image={url}
class="relative"
>
<TextBoxes {page} {src} />
</div>
<style>
div {
background-size: contain;
background-repeat: no-repeat;
}
</style>

View File

@@ -23,6 +23,7 @@
import { getCharCount } from '$lib/util/count-chars';
import QuickActions from './QuickActions.svelte';
import { beforeNavigate } from '$app/navigation';
import { onMount } from 'svelte';
// TODO: Refactor this whole mess
export let volumeSettings: VolumeSettings;
@@ -220,7 +221,17 @@
}
}
onMount(() => {
if ($settings.defaultFullscreen) {
document.documentElement.requestFullscreen();
}
});
beforeNavigate(() => {
if (document.exitFullscreen) {
document.exitFullscreen();
}
if (volume) {
const { charCount, lineCount } = getCharCount(pages, page);
@@ -329,14 +340,17 @@
<div
class="flex flex-row"
class:flex-row-reverse={!volumeSettings.rightToLeft}
style:filter={`invert(${$settings.invertColors ? 1 : 0})`}
on:dblclick={onDoubleTap}
role="none"
id="manga-panel"
>
{#key page}
{#if showSecondPage()}
<MangaPage page={pages[index + 1]} src={Object.values(volume?.files)[index + 1]} />
{/if}
<MangaPage page={pages[index]} src={Object.values(volume?.files)[index]} />
{/key}
</div>
</Panzoom>
</div>

View File

@@ -9,20 +9,15 @@
$: textBoxes = page.blocks
.map((block) => {
const { img_height: imgh, img_width: imgw } = page;
const img_height = (window.innerWidth * imgh) / imgw;
const img_width = window.innerWidth;
const { img_height, img_width } = page;
const { box, font_size, lines, vertical } = block;
const resizeFactor = window.innerWidth / imgw;
let [_xmin, _ymin, _xmax, _ymax] = box;
console.log(box);
const xmin = clamp(_xmin * resizeFactor, 0, img_width);
const ymin = clamp(_ymin * resizeFactor, 0, img_height);
const xmax = clamp(_xmax * resizeFactor, 0, img_width);
const ymax = clamp(_ymax * resizeFactor, 0, img_height);
const xmin = clamp(_xmin, 0, img_width);
const ymin = clamp(_ymin, 0, img_height);
const xmax = clamp(_xmax, 0, img_width);
const ymax = clamp(_ymax, 0, img_height);
const width = xmax - xmin;
const height = ymax - ymin;

View File

@@ -1,26 +0,0 @@
<script lang="ts">
import { volume } from '$lib/catalog';
import MangaPage from '../Reader/MangaPage.svelte';
$: files = $volume ? Object.values($volume?.files).slice(0, 10) : [];
$: pages = $volume?.mokuroData?.pages || [];
$: {
const { innerWidth, innerHeight } = window;
console.log(innerWidth, innerHeight);
}
</script>
<section>
{#each files as file, i}
<MangaPage src={file} page={pages[i]} />
{/each}
</section>
<style>
section {
display: flex;
flex-direction: column;
align-items: center;
}
</style>

View File

@@ -5,12 +5,6 @@
$: zoomModeValue = $settings.zoomDefault;
$: fontSizeValue = $settings.fontSize;
$: readerValue = $settings.reader || 'classic';
let readers = [
{ value: 'classic', name: 'Classic' },
{ value: 'v2', name: 'V2' }
];
let zoomModes = [
{ value: 'zoomFitToScreen', name: 'Fit to screen' },
@@ -46,11 +40,6 @@
}
</script>
<div>
<Label>Reader:</Label>
<Select items={readers} value={readerValue} on:change={(e) => onSelectChange(e, 'reader')} />
</div>
<div>
<Label>On page zoom:</Label>
<Select

View File

@@ -3,6 +3,7 @@
import { Toggle } from 'flowbite-svelte';
$: toggles = [
{ key: 'defaultFullscreen', text: 'Open reader in fullscreen', value: $settings.defaultFullscreen },
{ key: 'textEditable', text: 'Editable text', value: $settings.textEditable },
{ key: 'textBoxBorders', text: 'Text box borders', value: $settings.textBoxBorders },
{ key: 'displayOCR', text: 'OCR enabled', value: $settings.displayOCR },
@@ -12,7 +13,8 @@
{ key: 'bounds', text: 'Bounds', value: $settings.bounds },
{ key: 'mobile', text: 'Mobile', value: $settings.mobile },
{ key: 'showTimer', text: 'Show timer', value: $settings.showTimer },
{ key: 'quickActions', text: 'Show quick actions', value: $settings.quickActions }
{ key: 'quickActions', text: 'Show quick actions', value: $settings.quickActions },
{ key: 'invertColors', text: 'Invert colors of the images', value: $settings.invertColors }
] as { key: SettingsKey; text: string; value: any }[];
</script>

View File

@@ -1 +1 @@
export const READER_VERSION = '1.0.0'
export const READER_VERSION = '0.9.1'

View File

@@ -1,8 +1,6 @@
import { browser } from '$app/environment';
import { derived, get, writable } from 'svelte/store';
export type Readers = 'classic' | 'v2'
export type FontSize =
| 'auto'
| '9'
@@ -43,7 +41,7 @@ export type VolumeDefaults = {
}
export type Settings = {
reader: Readers,
defaultFullscreen: boolean;
textEditable: boolean;
textBoxBorders: boolean;
displayOCR: boolean;
@@ -59,6 +57,7 @@ export type Settings = {
quickActions: boolean;
fontSize: FontSize;
zoomDefault: ZoomModes;
invertColors: boolean;
volumeDefaults: VolumeDefaults;
ankiConnectSettings: AnkiConnectSettings;
};
@@ -70,7 +69,7 @@ export type AnkiSettingsKey = keyof AnkiConnectSettings;
export type VolumeDefaultsKey = keyof VolumeDefaults;
const defaultSettings: Settings = {
reader: 'classic',
defaultFullscreen: false,
displayOCR: true,
textEditable: false,
textBoxBorders: false,
@@ -86,6 +85,7 @@ const defaultSettings: Settings = {
quickActions: true,
fontSize: 'auto',
zoomDefault: 'zoomFitToScreen',
invertColors: false,
volumeDefaults: {
singlePageView: false,
rightToLeft: true,

View File

@@ -1,8 +1,8 @@
<slot />
<style>
/* :global(body.reader) {
:global(body.reader) {
overflow: hidden !important;
overscroll-behavior: contain;
} */
}
</style>

View File

@@ -4,7 +4,6 @@
import Timer from '$lib/components/Reader/Timer.svelte';
import { initializeVolume, settings, startCount, volumeSettings, volumes } from '$lib/settings';
import { onMount } from 'svelte';
import ReaderV2 from '$lib/components/ReaderV2/ReaderV2.svelte';
const volumeId = $page.params.volume;
let count: undefined | number = undefined;
@@ -27,9 +26,5 @@
{#if $settings.showTimer}
<Timer bind:count {volumeId} />
{/if}
{#if $settings.reader === 'classic'}
<Reader volumeSettings={$volumeSettings[volumeId]} />
{:else}
<ReaderV2 />
{/if}
{/if}

View File

@@ -2,7 +2,7 @@
import { processFiles } from '$lib/upload';
import Loader from '$lib/components/Loader.svelte';
import { formatBytes, showSnackbar, uploadFile } from '$lib/util';
import { Button, Frame, Listgroup, ListgroupItem } from 'flowbite-svelte';
import { Button, P, Progressbar } from 'flowbite-svelte';
import { onMount } from 'svelte';
import { promptConfirmation } from '$lib/util';
import { GoogleSolid } from 'flowbite-svelte-icons';
@@ -22,70 +22,70 @@
const type = 'application/json';
let tokenClient: any;
let zips: gapi.client.drive.File[];
let loadingMessage = '';
let accessToken = '';
let readerFolderId = '';
let volumeDataId = '';
let profilesId = '';
async function fetchZips(folderId: string) {
const { result } = await gapi.client.drive.files.list({
q: `'${folderId}' in parents and (mimeType='${FOLDER_MIME_TYPE}' or (fileExtension='zip' or fileExtension='cbz'))`,
fields: 'files(id, name, mimeType, size)'
});
let loadingMessage = '';
if (!result.files) return;
let completed = 0;
let totalSize = 0;
$: progress = Math.floor((completed / totalSize) * 100).toString();
let zipFiles: gapi.client.drive.File[] = [];
function xhrDownloadFileId(fileId: string) {
return new Promise<Blob>((resolve, reject) => {
const { access_token } = gapi.auth.getToken();
const xhr = new XMLHttpRequest();
for (const file of result.files) {
if (!file.id) continue;
completed = 0;
totalSize = 0;
if (file.mimeType === FOLDER_MIME_TYPE) {
zipFiles = [...zipFiles, ...((await fetchZips(file.id)) || [])];
} else {
zipFiles.push(file);
}
}
xhr.open('GET', `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`);
xhr.setRequestHeader('Authorization', `Bearer ${access_token}`);
xhr.responseType = 'blob';
return zipFiles;
}
async function downloadFile(fileId: string) {
loadingMessage = 'Downloading from drive';
const { body, headers } = await gapi.client.drive.files.get({
fileId,
alt: 'media'
});
const type = headers?.['Content-Type'] || '';
const blob = new Blob([new Uint8Array(body.length).map((_, i) => body.charCodeAt(i))], {
type
});
const file = new File([blob], fileId + '.zip');
loadingMessage = 'Adding to catalog';
await processFiles([file]);
xhr.onprogress = ({ loaded, total }) => {
loadingMessage = '';
}
completed = loaded;
totalSize = total;
};
function onClick({ id, name }: gapi.client.drive.File) {
if (id) {
promptConfirmation(`Would you like to download and add ${name} to your collection?`, () => {
downloadFile(id);
xhr.onabort = (event) => {
console.warn(`xhr ${fileId}: download aborted at ${event.loaded} of ${event.total}`);
showSnackbar('Download failed');
reject(new Error('Download aborted'));
};
xhr.onerror = (event) => {
console.error(`xhr ${fileId}: download error at ${event.loaded} of ${event.total}`);
showSnackbar('Download failed');
reject(new Error('Error downloading file'));
};
xhr.onload = () => {
completed = 0;
totalSize = 0;
resolve(xhr.response);
};
xhr.ontimeout = (event) => {
console.warn(`xhr ${fileId}: download timeout after ${event.loaded} of ${event.total}`);
showSnackbar('Download timed out');
reject(new Error('Timout downloading file'));
};
xhr.send();
});
}
}
async function connectDrive(resp?: any) {
if (resp?.error !== undefined) {
throw resp;
}
accessToken = resp?.access_token;
loadingMessage = 'Connecting to drive';
const { result: readerFolderRes } = await gapi.client.drive.files.list({
@@ -102,7 +102,6 @@
readerFolderId = createReaderFolderRes.id || '';
} else {
const id = readerFolderRes.files?.[0]?.id || '';
zips = [...((await fetchZips(id)) || [])];
readerFolderId = id || '';
}
@@ -126,8 +125,11 @@
}
loadingMessage = '';
if (accessToken) {
showSnackbar('Connected to Google Drive');
}
}
function signIn() {
if (gapi.client.getToken() === null) {
@@ -143,13 +145,10 @@
apiKey: API_KEY,
discoveryDocs: [DISCOVERY_DOC]
});
if (gapi.client.getToken() !== null) {
loadingMessage = 'Connecting to drive';
connectDrive();
}
});
gapi.load('picker', () => {});
tokenClient = google.accounts.oauth2.initTokenClient({
client_id: CLIENT_ID,
scope: SCOPES,
@@ -157,18 +156,56 @@
});
});
function createPicker() {
const docsView = new google.picker.DocsView(google.picker.ViewId.DOCS)
.setMimeTypes('application/zip,application/x-zip-compressed')
.setMode(google.picker.DocsViewMode.LIST)
.setIncludeFolders(true)
.setParent(readerFolderId);
const picker = new google.picker.PickerBuilder()
.addView(docsView)
.setOAuthToken(accessToken)
.setAppId(CLIENT_ID)
.setDeveloperKey(API_KEY)
.enableFeature(google.picker.Feature.NAV_HIDDEN)
.setCallback(pickerCallback)
.build();
picker.setVisible(true);
}
async function pickerCallback(data: google.picker.ResponseObject) {
try {
if (data[google.picker.Response.ACTION] == google.picker.Action.PICKED) {
loadingMessage = 'Downloading from drive...';
const docs = data[google.picker.Response.DOCUMENTS];
const blob = await xhrDownloadFileId(docs[0].id);
loadingMessage = 'Adding to catalog...';
const file = new File([blob], docs[0].name);
await processFiles([file]);
loadingMessage = '';
}
} catch (error) {
showSnackbar('Something went wrong');
loadingMessage = '';
console.error(error);
}
}
async function onUploadVolumeData() {
const metadata = {
mimeType: type,
name: VOLUME_DATA_FILE,
parents: [volumeDataId ? null : readerFolderId]
};
const { access_token } = gapi.auth.getToken();
loadingMessage = 'Uploading volume data';
const res = await uploadFile({
accessToken: access_token,
accessToken,
fileId: volumeDataId,
metadata,
localStorageId: 'volumes',
@@ -178,8 +215,10 @@
volumeDataId = res.id;
loadingMessage = '';
if (volumeDataId) {
showSnackbar('Volume data uploaded');
}
}
async function onUploadProfiles() {
const metadata = {
@@ -187,12 +226,11 @@
name: PROFILES_FILE,
parents: [profilesId ? null : readerFolderId]
};
const { access_token } = gapi.auth.getToken();
loadingMessage = 'Uploading profiles';
const res = await uploadFile({
accessToken: access_token,
accessToken,
fileId: profilesId,
metadata,
localStorageId: 'profiles',
@@ -202,8 +240,10 @@
profilesId = res.id;
loadingMessage = '';
if (profilesId) {
showSnackbar('Profiles uploaded');
}
}
async function onDownloadVolumeData() {
loadingMessage = 'Downloading volume data';
@@ -253,15 +293,25 @@
</svelte:head>
<div class="p-2 h-[90svh]">
{#if loadingMessage}
{#if loadingMessage || completed > 0}
<Loader>
{#if completed > 0}
<P>{formatBytes(completed)} / {formatBytes(totalSize)}</P>
<Progressbar {progress} />
{:else}
{loadingMessage}
{/if}
</Loader>
{:else if zips}
<div class="flex flex-col gap-2">
<div class="flex justify-between items-center gap-2 flex-col sm:flex-row">
<h2 class="text-2xl font-semibold text-center">Google Drive:</h2>
<div class="flex flex-col sm:flex-row gap-2 sm:w-auto w-full">
{:else if accessToken}
<div class="flex justify-between items-center gap-6 flex-col">
<h2 class="text-3xl font-semibold text-center pt-2">Google Drive:</h2>
<p class="text-center">
Add your zipped manga files to the <span class="text-primary-700">{READER_FOLDER}</span> folder
in your Google Drive.
</p>
<div class="flex flex-col gap-4 w-full max-w-3xl">
<Button color="blue" on:click={createPicker}>Download manga</Button>
<div class="flex-col gap-2 flex">
<Button
color="dark"
on:click={() => promptConfirmation('Upload volume data?', onUploadVolumeData)}
@@ -277,6 +327,8 @@
Download volume data
</Button>
{/if}
</div>
<div class="flex-col gap-2 flex">
<Button
color="dark"
on:click={() => promptConfirmation('Upload profiles?', onUploadProfiles)}
@@ -294,38 +346,11 @@
{/if}
</div>
</div>
<div class="flex gap-2 justify-center flex-wrap">
{#if zips.length > 0}
<Listgroup active class="w-full">
{#each zips as zip}
<Frame
on:click={() => onClick(zip)}
rounded
border
class="divide-y divide-gray-200 dark:divide-gray-600"
>
<ListgroupItem normalClass="py-4">
<div class="flex flex-col gap-2">
<h2 class="font-semibold">{zip.name}</h2>
<p>{formatBytes(parseInt(zip.size || '0'))}</p>
</div>
</ListgroupItem>
</Frame>
{/each}
</Listgroup>
{:else}
<div class="h-[70svh] items-center justify-center flex">
<p class="text-center">
Add your zip files to the <span class="text-primary-700">{READER_FOLDER}</span> folder
in your Google Drive.
</p>
</div>
{/if}
</div>
</div>
{:else}
<div class="flex justify-center pt-0 sm:pt-32">
<button
class="w-full border rounded-lg border-slate-600 p-10 border-opacity-50 hover:bg-slate-800"
class="w-full border rounded-lg border-slate-600 p-10 border-opacity-50 hover:bg-slate-800 max-w-3xl"
on:click={signIn}
>
<div class="flex sm:flex-row flex-col gap-2 items-center justify-center">
@@ -333,5 +358,6 @@
<h2 class="text-lg">Connect to Google Drive</h2>
</div>
</button>
</div>
{/if}
</div>

View File

@@ -6,7 +6,7 @@
import { promptConfirmation, showSnackbar } from '$lib/util';
import { P, Progressbar } from 'flowbite-svelte';
import { onMount } from 'svelte';
export const BASE_URL = 'https://www.mokuro.moe/manga';
export const BASE_URL = $page.url.searchParams.get('source') || 'https://mokuro.moe/manga';
const manga = $page.url.searchParams.get('manga');
const volume = $page.url.searchParams.get('volume');