Use spaces, run prettier on project
This commit is contained in:
@@ -1,30 +1,30 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended',
|
||||
'prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: ['.svelte']
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
}
|
||||
}
|
||||
]
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended',
|
||||
'prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: ['.svelte']
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
15
.prettierrc
15
.prettierrc
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
82
package.json
82
package.json
@@ -1,43 +1,43 @@
|
||||
{
|
||||
"name": "z-reader",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||
"format": "prettier --plugin-search-dir . --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/kit": "^1.20.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte": "^2.30.0",
|
||||
"flowbite-svelte": "^0.44.5",
|
||||
"flowbite-svelte-icons": "^0.4.2",
|
||||
"postcss": "^8.4.24",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"sass": "^1.64.2",
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-check": "^3.4.3",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.4.2"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "^2.7.20",
|
||||
"dexie": "^4.0.1-alpha.25",
|
||||
"panzoom": "^9.4.3"
|
||||
}
|
||||
"name": "z-reader",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||
"format": "prettier --plugin-search-dir . --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/kit": "^1.20.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte": "^2.30.0",
|
||||
"flowbite-svelte": "^0.44.5",
|
||||
"flowbite-svelte-icons": "^0.4.2",
|
||||
"postcss": "^8.4.24",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"sass": "^1.64.2",
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-check": "^3.4.3",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.4.2"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "^2.7.20",
|
||||
"dexie": "^4.0.1-alpha.25",
|
||||
"panzoom": "^9.4.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ const tailwindcss = require('tailwindcss');
|
||||
const autoprefixer = require('autoprefixer');
|
||||
|
||||
const config = {
|
||||
plugins: [
|
||||
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
|
||||
tailwindcss(),
|
||||
//But others, like autoprefixer, need to run after,
|
||||
autoprefixer
|
||||
]
|
||||
plugins: [
|
||||
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
|
||||
tailwindcss(),
|
||||
//But others, like autoprefixer, need to run after,
|
||||
autoprefixer
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
12
src/app.d.ts
vendored
12
src/app.d.ts
vendored
@@ -1,12 +1,12 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
21
src/app.html
21
src/app.html
@@ -1,12 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, height=device-height,initial-scale=1, minimum-scale=1, user-scalable=no" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, height=device-height,initial-scale=1, minimum-scale=1, user-scalable=no"
|
||||
/>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Volume } from "$lib/types";
|
||||
import { writable } from "svelte/store";
|
||||
import { db } from "$lib/catalog/db";
|
||||
import { liveQuery } from "dexie";
|
||||
import type { Volume } from '$lib/types';
|
||||
import { writable } from 'svelte/store';
|
||||
import { db } from '$lib/catalog/db';
|
||||
import { liveQuery } from 'dexie';
|
||||
export const currentManga = writable<Volume[] | undefined>(undefined);
|
||||
export const currentVolume = writable<Volume | undefined>(undefined);
|
||||
export const catalog = liveQuery(() => db.catalog.toArray());
|
||||
@@ -1,32 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { catalog } from '$lib/catalog';
|
||||
import { Button } from 'flowbite-svelte';
|
||||
import CatalogItem from './CatalogItem.svelte';
|
||||
import { promptConfirmation } from '$lib/util';
|
||||
import { db } from '$lib/catalog/db';
|
||||
import { catalog } from '$lib/catalog';
|
||||
import { Button } from 'flowbite-svelte';
|
||||
import CatalogItem from './CatalogItem.svelte';
|
||||
import { promptConfirmation } from '$lib/util';
|
||||
import { db } from '$lib/catalog/db';
|
||||
|
||||
function onClear() {
|
||||
promptConfirmation('Are you sure you want to clear your catalog?', () => db.catalog.clear());
|
||||
}
|
||||
function onClear() {
|
||||
promptConfirmation('Are you sure you want to clear your catalog?', () => db.catalog.clear());
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $catalog}
|
||||
{#if $catalog.length > 0}
|
||||
<div class="flex flex-col gap-5">
|
||||
<div class="sm:block flex-col flex">
|
||||
<Button outline color="red" class="float-right" on:click={onClear}>Clear catalog</Button>
|
||||
</div>
|
||||
<div class="flex flex-row gap-5 flex-wrap">
|
||||
{#each $catalog as { id, manga } (id)}
|
||||
<CatalogItem {manga} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center p-20">
|
||||
<p>Your catalog is currently empty.</p>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $catalog.length > 0}
|
||||
<div class="flex flex-col gap-5">
|
||||
<div class="sm:block flex-col flex">
|
||||
<Button outline color="red" class="float-right" on:click={onClear}>Clear catalog</Button>
|
||||
</div>
|
||||
<div class="flex flex-row gap-5 flex-wrap">
|
||||
{#each $catalog as { id, manga } (id)}
|
||||
<CatalogItem {manga} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center p-20">
|
||||
<p>Your catalog is currently empty.</p>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<p>Loading...</p>
|
||||
<p>Loading...</p>
|
||||
{/if}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { currentManga } from '$lib/catalog';
|
||||
import type { Volume } from '$lib/types';
|
||||
export let manga: Volume[];
|
||||
const { volumeName, files, mokuroData } = manga[0];
|
||||
import { currentManga } from '$lib/catalog';
|
||||
import type { Volume } from '$lib/types';
|
||||
export let manga: Volume[];
|
||||
const { volumeName, files, mokuroData } = manga[0];
|
||||
|
||||
function onClick() {
|
||||
currentManga.set(manga);
|
||||
}
|
||||
function onClick() {
|
||||
currentManga.set(manga);
|
||||
}
|
||||
</script>
|
||||
|
||||
<a href={volumeName} on:click={onClick}>
|
||||
<div class="flex flex-col gap-[5px] text-center">
|
||||
{mokuroData.title}
|
||||
{#if files}
|
||||
<img
|
||||
src={URL.createObjectURL(Object.values(files)[0])}
|
||||
alt="img"
|
||||
class="object-contain w-[250px] h-[350px] bg-black border-gray-900 border"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-col gap-[5px] text-center">
|
||||
{mokuroData.title}
|
||||
{#if files}
|
||||
<img
|
||||
src={URL.createObjectURL(Object.values(files)[0])}
|
||||
alt="img"
|
||||
class="object-contain w-[250px] h-[350px] bg-black border-gray-900 border"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { confirmationPopupStore } from '$lib/util';
|
||||
import { Button, Modal } from 'flowbite-svelte';
|
||||
import { ExclamationCircleOutline } from 'flowbite-svelte-icons';
|
||||
import { onMount } from 'svelte';
|
||||
import { confirmationPopupStore } from '$lib/util';
|
||||
import { Button, Modal } from 'flowbite-svelte';
|
||||
import { ExclamationCircleOutline } from 'flowbite-svelte-icons';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let open = false;
|
||||
let open = false;
|
||||
|
||||
onMount(() => {
|
||||
confirmationPopupStore.subscribe((value) => {
|
||||
open = Boolean(value);
|
||||
});
|
||||
});
|
||||
onMount(() => {
|
||||
confirmationPopupStore.subscribe((value) => {
|
||||
open = Boolean(value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<Modal bind:open size="xs" autoclose outsideclose>
|
||||
<div class="text-center">
|
||||
<ExclamationCircleOutline class="mx-auto mb-4 text-gray-400 w-12 h-12 dark:text-gray-200" />
|
||||
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">
|
||||
{$confirmationPopupStore?.message}
|
||||
</h3>
|
||||
<Button color="red" class="mr-2" on:click={$confirmationPopupStore?.onConfirm}>Yes</Button>
|
||||
<Button color="alternative">No</Button>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<ExclamationCircleOutline class="mx-auto mb-4 text-gray-400 w-12 h-12 dark:text-gray-200" />
|
||||
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">
|
||||
{$confirmationPopupStore?.message}
|
||||
</h3>
|
||||
<Button color="red" class="mr-2" on:click={$confirmationPopupStore?.onConfirm}>Yes</Button>
|
||||
<Button color="alternative">No</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { A, Fileupload, Label } from 'flowbite-svelte';
|
||||
import { A, Fileupload, Label } from 'flowbite-svelte';
|
||||
|
||||
export let files: FileList | undefined = undefined;
|
||||
export let onUpload: ((files: FileList) => void) | undefined = undefined;
|
||||
export let files: FileList | undefined = undefined;
|
||||
export let onUpload: ((files: FileList) => void) | undefined = undefined;
|
||||
|
||||
let input: HTMLInputElement;
|
||||
let input: HTMLInputElement;
|
||||
|
||||
function handleChange() {
|
||||
if (files && onUpload) {
|
||||
onUpload(files);
|
||||
}
|
||||
}
|
||||
function handleChange() {
|
||||
if (files && onUpload) {
|
||||
onUpload(files);
|
||||
}
|
||||
}
|
||||
|
||||
function onClick() {
|
||||
input.click();
|
||||
}
|
||||
function onClick() {
|
||||
input.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
bind:files
|
||||
bind:this={input}
|
||||
on:change={handleChange}
|
||||
{...$$restProps}
|
||||
class="hidden"
|
||||
type="file"
|
||||
bind:files
|
||||
bind:this={input}
|
||||
on:change={handleChange}
|
||||
{...$$restProps}
|
||||
class="hidden"
|
||||
/>
|
||||
|
||||
<A on:click={onClick}><slot>Upload</slot></A>
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { Navbar, NavBrand } from 'flowbite-svelte';
|
||||
import { UserSettingsSolid, UploadSolid } from 'flowbite-svelte-icons';
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import Settings from './Settings.svelte';
|
||||
import UploadModal from './UploadModal.svelte';
|
||||
import { settings } from '$lib/settings';
|
||||
import { Navbar, NavBrand } from 'flowbite-svelte';
|
||||
import { UserSettingsSolid, UploadSolid } from 'flowbite-svelte-icons';
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import Settings from './Settings.svelte';
|
||||
import UploadModal from './UploadModal.svelte';
|
||||
import { settings } from '$lib/settings';
|
||||
|
||||
let settingsHidden = true;
|
||||
let uploadModalOpen = false;
|
||||
let isReader = false;
|
||||
let settingsHidden = true;
|
||||
let uploadModalOpen = false;
|
||||
let isReader = false;
|
||||
|
||||
function openSettings() {
|
||||
settingsHidden = false;
|
||||
}
|
||||
function openSettings() {
|
||||
settingsHidden = false;
|
||||
}
|
||||
|
||||
afterNavigate(() => {
|
||||
isReader = $page.route.id === '/[manga]/[volume]';
|
||||
afterNavigate(() => {
|
||||
isReader = $page.route.id === '/[manga]/[volume]';
|
||||
|
||||
if (isReader) {
|
||||
window.document.body.classList.add('reader');
|
||||
} else {
|
||||
window.document.body.classList.remove('reader');
|
||||
}
|
||||
});
|
||||
if (isReader) {
|
||||
window.document.body.classList.add('reader');
|
||||
} else {
|
||||
window.document.body.classList.remove('reader');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="relative z-10">
|
||||
<Navbar hidden={isReader}>
|
||||
<NavBrand href="/">
|
||||
<span class="text-xl font-semibold dark:text-white">Mokuro</span>
|
||||
</NavBrand>
|
||||
<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)} />
|
||||
</div>
|
||||
</Navbar>
|
||||
{#if isReader}
|
||||
<button
|
||||
on:click={openSettings}
|
||||
class="hover:text-primary-700 fixed opacity-50 hover:opacity-100 right-10 top-5 p-10 m-[-2.5rem]"
|
||||
>
|
||||
<div style:background-color={$settings.backgroundColor} class="absolute">
|
||||
<UserSettingsSolid class="mix-blend-difference" />
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
<Navbar hidden={isReader}>
|
||||
<NavBrand href="/">
|
||||
<span class="text-xl font-semibold dark:text-white">Mokuro</span>
|
||||
</NavBrand>
|
||||
<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)} />
|
||||
</div>
|
||||
</Navbar>
|
||||
{#if isReader}
|
||||
<button
|
||||
on:click={openSettings}
|
||||
class="hover:text-primary-700 fixed opacity-50 hover:opacity-100 right-10 top-5 p-10 m-[-2.5rem]"
|
||||
>
|
||||
<div style:background-color={$settings.backgroundColor} class="absolute">
|
||||
<UserSettingsSolid class="mix-blend-difference" />
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Settings bind:hidden={settingsHidden} />
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type { Page } from '$lib/types';
|
||||
import TextBoxes from './TextBoxes.svelte';
|
||||
import type { Page } from '$lib/types';
|
||||
import TextBoxes from './TextBoxes.svelte';
|
||||
|
||||
export let page: Page;
|
||||
export let src: Blob;
|
||||
export let page: Page;
|
||||
export let src: Blob;
|
||||
</script>
|
||||
|
||||
<div
|
||||
draggable="false"
|
||||
style:width={`${page.img_width}px`}
|
||||
style:height={`${page.img_height}px`}
|
||||
style:background-image={`url(${URL.createObjectURL(src)})`}
|
||||
class="relative"
|
||||
draggable="false"
|
||||
style:width={`${page.img_width}px`}
|
||||
style:height={`${page.img_height}px`}
|
||||
style:background-image={`url(${URL.createObjectURL(src)})`}
|
||||
class="relative"
|
||||
>
|
||||
<TextBoxes {page} />
|
||||
<TextBoxes {page} />
|
||||
</div>
|
||||
|
||||
@@ -1,133 +1,122 @@
|
||||
<script lang="ts">
|
||||
import { currentVolume } from '$lib/catalog';
|
||||
import {
|
||||
Panzoom,
|
||||
keepZoomStart,
|
||||
zoomDefault,
|
||||
zoomFitToScreen,
|
||||
zoomFitToWidth,
|
||||
zoomOriginal
|
||||
} from '$lib/panzoom';
|
||||
import { progress, settings, updateProgress } from '$lib/settings';
|
||||
import { clamp } from '$lib/util';
|
||||
import { Button, Input, Popover, Range } from 'flowbite-svelte';
|
||||
import MangaPage from './MangaPage.svelte';
|
||||
import {
|
||||
ChervonDoubleLeftSolid,
|
||||
ChervonDoubleRightSolid,
|
||||
ChevronLeftSolid
|
||||
} from 'flowbite-svelte-icons';
|
||||
import { onMount } from 'svelte';
|
||||
import { currentVolume } from '$lib/catalog';
|
||||
import { Panzoom, zoomDefault } from '$lib/panzoom';
|
||||
import { progress, settings, updateProgress } from '$lib/settings';
|
||||
import { clamp } from '$lib/util';
|
||||
import { Input, Popover, Range } from 'flowbite-svelte';
|
||||
import MangaPage from './MangaPage.svelte';
|
||||
import { ChervonDoubleLeftSolid, ChervonDoubleRightSolid } from 'flowbite-svelte-icons';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const volume = $currentVolume;
|
||||
const pages = volume?.mokuroData.pages;
|
||||
const volume = $currentVolume;
|
||||
const pages = volume?.mokuroData.pages;
|
||||
|
||||
$: page = $progress?.[volume?.mokuroData.volume_uuid || 0] || 1;
|
||||
$: index = page - 1;
|
||||
$: navAmount = $settings.singlePageView ? 1 : 2;
|
||||
$: page = $progress?.[volume?.mokuroData.volume_uuid || 0] || 1;
|
||||
$: index = page - 1;
|
||||
$: navAmount = $settings.singlePageView ? 1 : 2;
|
||||
|
||||
let start: Date;
|
||||
let start: Date;
|
||||
|
||||
function mouseDown() {
|
||||
start = new Date();
|
||||
}
|
||||
function mouseDown() {
|
||||
start = new Date();
|
||||
}
|
||||
|
||||
function left() {
|
||||
const newPage = $settings.rightToLeft ? page + navAmount : page - navAmount;
|
||||
changePage(newPage);
|
||||
}
|
||||
function left() {
|
||||
const newPage = $settings.rightToLeft ? page + navAmount : page - navAmount;
|
||||
changePage(newPage);
|
||||
}
|
||||
|
||||
function right() {
|
||||
const newPage = $settings.rightToLeft ? page - navAmount : page + navAmount;
|
||||
changePage(newPage);
|
||||
}
|
||||
function right() {
|
||||
const newPage = $settings.rightToLeft ? page - navAmount : page + navAmount;
|
||||
changePage(newPage);
|
||||
}
|
||||
|
||||
function changePage(newPage: number, ingoreTimeOut = false) {
|
||||
const end = new Date();
|
||||
const clickDuration = ingoreTimeOut ? 0 : end.getTime() - start?.getTime();
|
||||
function changePage(newPage: number, ingoreTimeOut = false) {
|
||||
const end = new Date();
|
||||
const clickDuration = ingoreTimeOut ? 0 : end.getTime() - start?.getTime();
|
||||
|
||||
if (pages && volume && clickDuration < 200) {
|
||||
updateProgress(volume.mokuroData.volume_uuid, clamp(newPage, 1, pages?.length));
|
||||
zoomDefault();
|
||||
}
|
||||
}
|
||||
if (pages && volume && clickDuration < 200) {
|
||||
updateProgress(volume.mokuroData.volume_uuid, clamp(newPage, 1, pages?.length));
|
||||
zoomDefault();
|
||||
}
|
||||
}
|
||||
|
||||
$: manualPage = page;
|
||||
$: pageDisplay = `${page}/${pages?.length}`;
|
||||
$: manualPage = page;
|
||||
$: pageDisplay = `${page}/${pages?.length}`;
|
||||
|
||||
function onInputClick(this: any) {
|
||||
this.select();
|
||||
}
|
||||
function onInputClick(this: any) {
|
||||
this.select();
|
||||
}
|
||||
|
||||
function onManualPageChange() {
|
||||
changePage(manualPage, true);
|
||||
}
|
||||
function onManualPageChange() {
|
||||
changePage(manualPage, true);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
zoomDefault();
|
||||
});
|
||||
onMount(() => {
|
||||
zoomDefault();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={zoomDefault} />
|
||||
{#if volume && pages}
|
||||
<Popover placement="bottom-end" trigger="click" triggeredBy="#page-num" class="z-20">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-row items-center gap-5 z-10">
|
||||
<ChervonDoubleLeftSolid
|
||||
on:click={() => changePage($settings.rightToLeft ? pages.length : 1, true)}
|
||||
class="hover:text-primary-600"
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
size="sm"
|
||||
defaultClass="select-all"
|
||||
bind:value={manualPage}
|
||||
on:click={onInputClick}
|
||||
on:change={onManualPageChange}
|
||||
/>
|
||||
<ChervonDoubleRightSolid
|
||||
on:click={() => changePage($settings.rightToLeft ? 1 : pages.length, true)}
|
||||
class="hover:text-primary-600"
|
||||
/>
|
||||
</div>
|
||||
<Range min={1} max={pages.length} bind:value={manualPage} on:change={onManualPageChange} />
|
||||
</div>
|
||||
</Popover>
|
||||
<button
|
||||
class="absolute opacity-50 left-5 top-5 z-10 mix-blend-difference"
|
||||
class:hidden={!$settings.pageNum}
|
||||
id="page-num"
|
||||
>
|
||||
{pageDisplay}
|
||||
</button>
|
||||
<div class="flex">
|
||||
<Panzoom>
|
||||
<button
|
||||
class="h-full fixed -left-1/2 z-10 w-1/2 hover:bg-slate-400 opacity-[0.01] justify-items-center"
|
||||
on:mousedown={mouseDown}
|
||||
on:mouseup={left}
|
||||
/>
|
||||
<button
|
||||
class="h-full fixed -right-1/2 z-10 w-1/2 hover:bg-slate-400 opacity-[0.01]"
|
||||
on:mousedown={mouseDown}
|
||||
on:mouseup={right}
|
||||
/>
|
||||
<div class="flex flex-row">
|
||||
{#if !$settings.singlePageView && index + 1 < pages.length}
|
||||
<MangaPage page={pages[index + 1]} src={Object.values(volume?.files)[index + 1]} />
|
||||
{/if}
|
||||
<MangaPage page={pages[index]} src={Object.values(volume?.files)[index]} />
|
||||
</div>
|
||||
</Panzoom>
|
||||
</div>
|
||||
<button
|
||||
on:mousedown={mouseDown}
|
||||
on:mouseup={left}
|
||||
class="left-0 top-0 absolute h-full w-[50px] hover:bg-slate-400 opacity-[0.01]"
|
||||
/>
|
||||
<button
|
||||
on:mousedown={mouseDown}
|
||||
on:mouseup={right}
|
||||
class="right-0 top-0 absolute h-full w-[50px] hover:bg-slate-400 opacity-[0.01]"
|
||||
/>
|
||||
<Popover placement="bottom-end" trigger="click" triggeredBy="#page-num" class="z-20">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-row items-center gap-5 z-10">
|
||||
<ChervonDoubleLeftSolid
|
||||
on:click={() => changePage($settings.rightToLeft ? pages.length : 1, true)}
|
||||
class="hover:text-primary-600"
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
size="sm"
|
||||
defaultClass="select-all"
|
||||
bind:value={manualPage}
|
||||
on:click={onInputClick}
|
||||
on:change={onManualPageChange}
|
||||
/>
|
||||
<ChervonDoubleRightSolid
|
||||
on:click={() => changePage($settings.rightToLeft ? 1 : pages.length, true)}
|
||||
class="hover:text-primary-600"
|
||||
/>
|
||||
</div>
|
||||
<Range min={1} max={pages.length} bind:value={manualPage} on:change={onManualPageChange} />
|
||||
</div>
|
||||
</Popover>
|
||||
<button
|
||||
class="absolute opacity-50 left-5 top-5 z-10 mix-blend-difference"
|
||||
class:hidden={!$settings.pageNum}
|
||||
id="page-num"
|
||||
>
|
||||
{pageDisplay}
|
||||
</button>
|
||||
<div class="flex">
|
||||
<Panzoom>
|
||||
<button
|
||||
class="h-full fixed -left-1/2 z-10 w-1/2 hover:bg-slate-400 opacity-[0.01] justify-items-center"
|
||||
on:mousedown={mouseDown}
|
||||
on:mouseup={left}
|
||||
/>
|
||||
<button
|
||||
class="h-full fixed -right-1/2 z-10 w-1/2 hover:bg-slate-400 opacity-[0.01]"
|
||||
on:mousedown={mouseDown}
|
||||
on:mouseup={right}
|
||||
/>
|
||||
<div class="flex flex-row">
|
||||
{#if !$settings.singlePageView && index + 1 < pages.length}
|
||||
<MangaPage page={pages[index + 1]} src={Object.values(volume?.files)[index + 1]} />
|
||||
{/if}
|
||||
<MangaPage page={pages[index]} src={Object.values(volume?.files)[index]} />
|
||||
</div>
|
||||
</Panzoom>
|
||||
</div>
|
||||
<button
|
||||
on:mousedown={mouseDown}
|
||||
on:mouseup={left}
|
||||
class="left-0 top-0 absolute h-full w-[50px] hover:bg-slate-400 opacity-[0.01]"
|
||||
/>
|
||||
<button
|
||||
on:mousedown={mouseDown}
|
||||
on:mouseup={right}
|
||||
class="right-0 top-0 absolute h-full w-[50px] hover:bg-slate-400 opacity-[0.01]"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1,94 +1,94 @@
|
||||
<script lang="ts">
|
||||
import { clamp } from '$lib/util';
|
||||
import type { Page } from '$lib/types';
|
||||
import { settings } from '$lib/settings';
|
||||
import { clamp } from '$lib/util';
|
||||
import type { Page } from '$lib/types';
|
||||
import { settings } from '$lib/settings';
|
||||
|
||||
export let page: Page;
|
||||
export let page: Page;
|
||||
|
||||
$: textBoxes = page.blocks.map((block) => {
|
||||
const { img_height, img_width } = page;
|
||||
const { box, font_size, lines, vertical } = block;
|
||||
$: textBoxes = page.blocks.map((block) => {
|
||||
const { img_height, img_width } = page;
|
||||
const { box, font_size, lines, vertical } = block;
|
||||
|
||||
let [_xmin, _ymin, _xmax, _ymax] = box;
|
||||
let [_xmin, _ymin, _xmax, _ymax] = box;
|
||||
|
||||
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 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;
|
||||
const width = xmax - xmin;
|
||||
const height = ymax - ymin;
|
||||
|
||||
const textBox = {
|
||||
left: `${xmin}px`,
|
||||
top: `${ymin}px`,
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
fontSize: $settings.fontSize === 'auto' ? `${font_size}px` : `${$settings.fontSize}pt`,
|
||||
writingMode: vertical ? 'vertical-rl' : 'horizontal-tb',
|
||||
lines
|
||||
};
|
||||
const textBox = {
|
||||
left: `${xmin}px`,
|
||||
top: `${ymin}px`,
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
fontSize: $settings.fontSize === 'auto' ? `${font_size}px` : `${$settings.fontSize}pt`,
|
||||
writingMode: vertical ? 'vertical-rl' : 'horizontal-tb',
|
||||
lines
|
||||
};
|
||||
|
||||
return textBox;
|
||||
});
|
||||
return textBox;
|
||||
});
|
||||
|
||||
$: fontWeight = $settings.boldFont ? 'bold' : '400';
|
||||
$: display = $settings.displayOCR ? 'block' : 'none';
|
||||
$: border = $settings.textBoxBorders ? '1px solid red' : 'none';
|
||||
$: contenteditable = $settings.textEditable;
|
||||
$: fontWeight = $settings.boldFont ? 'bold' : '400';
|
||||
$: display = $settings.displayOCR ? 'block' : 'none';
|
||||
$: border = $settings.textBoxBorders ? '1px solid red' : 'none';
|
||||
$: contenteditable = $settings.textEditable;
|
||||
</script>
|
||||
|
||||
{#each textBoxes as { fontSize, height, left, lines, top, width, writingMode }, index (`text-box-${index}`)}
|
||||
<div
|
||||
class="text-box"
|
||||
style:width
|
||||
style:height
|
||||
style:left
|
||||
style:top
|
||||
style:font-size={fontSize}
|
||||
style:writing-mode={writingMode}
|
||||
style:font-weight={fontWeight}
|
||||
style:display
|
||||
style:border
|
||||
{contenteditable}
|
||||
>
|
||||
{#each lines as line}
|
||||
<p>{line}</p>
|
||||
{/each}
|
||||
</div>
|
||||
<div
|
||||
class="text-box"
|
||||
style:width
|
||||
style:height
|
||||
style:left
|
||||
style:top
|
||||
style:font-size={fontSize}
|
||||
style:writing-mode={writingMode}
|
||||
style:font-weight={fontWeight}
|
||||
style:display
|
||||
style:border
|
||||
{contenteditable}
|
||||
>
|
||||
{#each lines as line}
|
||||
<p>{line}</p>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
.text-box {
|
||||
color: black;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
line-height: 1.1em;
|
||||
font-size: 16pt;
|
||||
white-space: nowrap;
|
||||
border: 1px solid rgba(0, 0, 0, 0);
|
||||
z-index: 1000;
|
||||
}
|
||||
.text-box {
|
||||
color: black;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
line-height: 1.1em;
|
||||
font-size: 16pt;
|
||||
white-space: nowrap;
|
||||
border: 1px solid rgba(0, 0, 0, 0);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.text-box:focus,
|
||||
.text-box:hover {
|
||||
background: rgb(255, 255, 255);
|
||||
border: 1px solid rgba(0, 0, 0, 0);
|
||||
z-index: 999 !important;
|
||||
}
|
||||
.text-box:focus,
|
||||
.text-box:hover {
|
||||
background: rgb(255, 255, 255);
|
||||
border: 1px solid rgba(0, 0, 0, 0);
|
||||
z-index: 999 !important;
|
||||
}
|
||||
|
||||
.text-box p {
|
||||
display: none;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0.1em;
|
||||
line-height: 1.1em;
|
||||
margin: 0;
|
||||
background-color: rgb(255, 255, 255);
|
||||
font-weight: var(--bold);
|
||||
}
|
||||
.text-box p {
|
||||
display: none;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0.1em;
|
||||
line-height: 1.1em;
|
||||
margin: 0;
|
||||
background-color: rgb(255, 255, 255);
|
||||
font-weight: var(--bold);
|
||||
}
|
||||
|
||||
.text-box:focus p,
|
||||
.text-box:hover p {
|
||||
display: table;
|
||||
}
|
||||
.text-box:focus p,
|
||||
.text-box:hover p {
|
||||
display: table;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,113 +1,113 @@
|
||||
<script lang="ts">
|
||||
import { Drawer, CloseButton, Toggle, Select, Input, Label, Button } from 'flowbite-svelte';
|
||||
import { UserSettingsSolid } from 'flowbite-svelte-icons';
|
||||
import { sineIn } from 'svelte/easing';
|
||||
import { resetSettings, settings, updateSetting } from '$lib/settings';
|
||||
import type { SettingsKey } from '$lib/settings';
|
||||
import { promptConfirmation } from '$lib/util';
|
||||
import { zoomDefault } from '$lib/panzoom';
|
||||
import { Drawer, CloseButton, Toggle, Select, Input, Label, Button } from 'flowbite-svelte';
|
||||
import { UserSettingsSolid } from 'flowbite-svelte-icons';
|
||||
import { sineIn } from 'svelte/easing';
|
||||
import { resetSettings, settings, updateSetting } from '$lib/settings';
|
||||
import type { SettingsKey } from '$lib/settings';
|
||||
import { promptConfirmation } from '$lib/util';
|
||||
import { zoomDefault } from '$lib/panzoom';
|
||||
|
||||
let transitionParams = {
|
||||
x: 320,
|
||||
duration: 200,
|
||||
easing: sineIn
|
||||
};
|
||||
let transitionParams = {
|
||||
x: 320,
|
||||
duration: 200,
|
||||
easing: sineIn
|
||||
};
|
||||
|
||||
export let hidden = true;
|
||||
export let hidden = true;
|
||||
|
||||
$: zoomModeValue = $settings.zoomDefault;
|
||||
$: fontSizeValue = $settings.fontSize;
|
||||
$: zoomModeValue = $settings.zoomDefault;
|
||||
$: fontSizeValue = $settings.fontSize;
|
||||
|
||||
let zoomModes = [
|
||||
{ value: 'zoomFitToScreen', name: 'Fit to screen' },
|
||||
{ value: 'zoomFitToWidth', name: 'Fit to width' },
|
||||
{ value: 'zoomOriginal', name: 'Original size' },
|
||||
{ value: 'keepZoom', name: 'Keep zoom' },
|
||||
{ value: 'keepZoomStart', name: 'Keep zoom, pan to top' }
|
||||
];
|
||||
let zoomModes = [
|
||||
{ value: 'zoomFitToScreen', name: 'Fit to screen' },
|
||||
{ value: 'zoomFitToWidth', name: 'Fit to width' },
|
||||
{ value: 'zoomOriginal', name: 'Original size' },
|
||||
{ value: 'keepZoom', name: 'Keep zoom' },
|
||||
{ value: 'keepZoomStart', name: 'Keep zoom, pan to top' }
|
||||
];
|
||||
|
||||
let fontSizes = [
|
||||
{ value: 'auto', name: 'auto' },
|
||||
{ value: '9', name: '9' },
|
||||
{ value: '10', name: '10' },
|
||||
{ value: '11', name: '11' },
|
||||
{ value: '12', name: '12' },
|
||||
{ value: '14', name: '14' },
|
||||
{ value: '16', name: '16' },
|
||||
{ value: '18', name: '18' },
|
||||
{ value: '20', name: '20' },
|
||||
{ value: '24', name: '24' },
|
||||
{ value: '32', name: '32' },
|
||||
{ value: '40', name: '40' },
|
||||
{ value: '48', name: '48' },
|
||||
{ value: '60', name: '60' }
|
||||
];
|
||||
let fontSizes = [
|
||||
{ value: 'auto', name: 'auto' },
|
||||
{ value: '9', name: '9' },
|
||||
{ value: '10', name: '10' },
|
||||
{ value: '11', name: '11' },
|
||||
{ value: '12', name: '12' },
|
||||
{ value: '14', name: '14' },
|
||||
{ value: '16', name: '16' },
|
||||
{ value: '18', name: '18' },
|
||||
{ value: '20', name: '20' },
|
||||
{ value: '24', name: '24' },
|
||||
{ value: '32', name: '32' },
|
||||
{ value: '40', name: '40' },
|
||||
{ value: '48', name: '48' },
|
||||
{ value: '60', name: '60' }
|
||||
];
|
||||
|
||||
$: toggles = [
|
||||
{ key: 'rightToLeft', text: 'Right to left', value: $settings.rightToLeft },
|
||||
{ key: 'singlePageView', text: 'Single page view', value: $settings.singlePageView },
|
||||
{ key: 'hasCover', text: 'First page is cover', value: $settings.hasCover },
|
||||
{ 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 },
|
||||
{ key: 'boldFont', text: 'Bold font', value: $settings.boldFont },
|
||||
{ key: 'pageNum', text: 'Show page number', value: $settings.pageNum }
|
||||
] as { key: SettingsKey; text: string; value: any }[];
|
||||
$: toggles = [
|
||||
{ key: 'rightToLeft', text: 'Right to left', value: $settings.rightToLeft },
|
||||
{ key: 'singlePageView', text: 'Single page view', value: $settings.singlePageView },
|
||||
{ key: 'hasCover', text: 'First page is cover', value: $settings.hasCover },
|
||||
{ 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 },
|
||||
{ key: 'boldFont', text: 'Bold font', value: $settings.boldFont },
|
||||
{ key: 'pageNum', text: 'Show page number', value: $settings.pageNum }
|
||||
] as { key: SettingsKey; text: string; value: any }[];
|
||||
|
||||
function onBackgroundColor(event: Event) {
|
||||
updateSetting('backgroundColor', (event.target as HTMLInputElement).value);
|
||||
}
|
||||
function onBackgroundColor(event: Event) {
|
||||
updateSetting('backgroundColor', (event.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
function onSelectChange(event: Event, setting: SettingsKey) {
|
||||
updateSetting(setting, (event.target as HTMLInputElement).value);
|
||||
}
|
||||
function onSelectChange(event: Event, setting: SettingsKey) {
|
||||
updateSetting(setting, (event.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
function onReset() {
|
||||
hidden = true;
|
||||
promptConfirmation('Restore default settings?', resetSettings);
|
||||
}
|
||||
function onReset() {
|
||||
hidden = true;
|
||||
promptConfirmation('Restore default settings?', resetSettings);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Drawer
|
||||
placement="right"
|
||||
transitionType="fly"
|
||||
width="lg:w-1/4 md:w-1/2 w-full"
|
||||
{transitionParams}
|
||||
bind:hidden
|
||||
id="settings"
|
||||
placement="right"
|
||||
transitionType="fly"
|
||||
width="lg:w-1/4 md:w-1/2 w-full"
|
||||
{transitionParams}
|
||||
bind:hidden
|
||||
id="settings"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<h5 id="drawer-label" class="inline-flex items-center mb-4 text-base font-semibold">
|
||||
<UserSettingsSolid class="w-4 h-4 mr-2.5" />Settings
|
||||
</h5>
|
||||
<CloseButton on:click={() => (hidden = true)} class="mb-4 dark:text-white" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-5">
|
||||
<div>
|
||||
<Label>On page zoom:</Label>
|
||||
<Select
|
||||
items={zoomModes}
|
||||
bind:value={zoomModeValue}
|
||||
on:change={(e) => onSelectChange(e, 'zoomDefault')}
|
||||
/>
|
||||
</div>
|
||||
{#each toggles as { key, text, value }}
|
||||
<Toggle size="small" checked={value} on:change={() => updateSetting(key, !value)}
|
||||
>{text}</Toggle
|
||||
>
|
||||
{/each}
|
||||
<div>
|
||||
<Label>Fontsize:</Label>
|
||||
<Select
|
||||
items={fontSizes}
|
||||
bind:value={fontSizeValue}
|
||||
on:change={(e) => onSelectChange(e, 'fontSize')}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Background color:</Label>
|
||||
<Input type="color" on:change={onBackgroundColor} value={$settings.backgroundColor} />
|
||||
</div>
|
||||
<Button outline on:click={onReset}>Reset</Button>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<h5 id="drawer-label" class="inline-flex items-center mb-4 text-base font-semibold">
|
||||
<UserSettingsSolid class="w-4 h-4 mr-2.5" />Settings
|
||||
</h5>
|
||||
<CloseButton on:click={() => (hidden = true)} class="mb-4 dark:text-white" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-5">
|
||||
<div>
|
||||
<Label>On page zoom:</Label>
|
||||
<Select
|
||||
items={zoomModes}
|
||||
bind:value={zoomModeValue}
|
||||
on:change={(e) => onSelectChange(e, 'zoomDefault')}
|
||||
/>
|
||||
</div>
|
||||
{#each toggles as { key, text, value }}
|
||||
<Toggle size="small" checked={value} on:change={() => updateSetting(key, !value)}
|
||||
>{text}</Toggle
|
||||
>
|
||||
{/each}
|
||||
<div>
|
||||
<Label>Fontsize:</Label>
|
||||
<Select
|
||||
items={fontSizes}
|
||||
bind:value={fontSizeValue}
|
||||
on:change={(e) => onSelectChange(e, 'fontSize')}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Background color:</Label>
|
||||
<Input type="color" on:change={onBackgroundColor} value={$settings.backgroundColor} />
|
||||
</div>
|
||||
<Button outline on:click={onReset}>Reset</Button>
|
||||
</div>
|
||||
</Drawer>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { snackbarStore } from '$lib/util/snackbar';
|
||||
import { Toast } from 'flowbite-svelte';
|
||||
import { snackbarStore } from '$lib/util/snackbar';
|
||||
import { Toast } from 'flowbite-svelte';
|
||||
</script>
|
||||
|
||||
{#if $snackbarStore?.message && $snackbarStore?.visible}
|
||||
<Toast position="bottom-right">{$snackbarStore?.message}</Toast>
|
||||
<Toast position="bottom-right">{$snackbarStore?.message}</Toast>
|
||||
{/if}
|
||||
|
||||
@@ -1,152 +1,152 @@
|
||||
<script lang="ts">
|
||||
import { Button, Dropzone, Modal, Spinner } from 'flowbite-svelte';
|
||||
import FileUpload from './FileUpload.svelte';
|
||||
import { processFiles } from '$lib/upload';
|
||||
import { onMount } from 'svelte';
|
||||
import { scanFiles } from '$lib/upload';
|
||||
import { formatBytes } from '$lib/util/upload';
|
||||
import { catalog } from '$lib/catalog';
|
||||
import { Button, Dropzone, Modal, Spinner } from 'flowbite-svelte';
|
||||
import FileUpload from './FileUpload.svelte';
|
||||
import { processFiles } from '$lib/upload';
|
||||
import { onMount } from 'svelte';
|
||||
import { scanFiles } from '$lib/upload';
|
||||
import { formatBytes } from '$lib/util/upload';
|
||||
import { catalog } from '$lib/catalog';
|
||||
|
||||
export let open = false;
|
||||
export let open = false;
|
||||
|
||||
let promise: Promise<void>;
|
||||
let files: FileList | undefined = undefined;
|
||||
let promise: Promise<void>;
|
||||
let files: FileList | undefined = undefined;
|
||||
|
||||
async function onUpload() {
|
||||
if (files) {
|
||||
promise = processFiles([...files]).then(() => {
|
||||
open = false;
|
||||
});
|
||||
} else if (draggedFiles) {
|
||||
promise = processFiles(draggedFiles).then(() => {
|
||||
open = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
async function onUpload() {
|
||||
if (files) {
|
||||
promise = processFiles([...files]).then(() => {
|
||||
open = false;
|
||||
});
|
||||
} else if (draggedFiles) {
|
||||
promise = processFiles(draggedFiles).then(() => {
|
||||
open = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
files = undefined;
|
||||
draggedFiles = undefined;
|
||||
}
|
||||
function reset() {
|
||||
files = undefined;
|
||||
draggedFiles = undefined;
|
||||
}
|
||||
|
||||
let storageSpace = 'Loading...';
|
||||
let storageSpace = 'Loading...';
|
||||
|
||||
onMount(() => {
|
||||
navigator.storage.estimate().then(({ usage, quota }) => {
|
||||
if (usage && quota) {
|
||||
storageSpace = `Storage: ${formatBytes(usage)} / ${formatBytes(quota)}`;
|
||||
}
|
||||
});
|
||||
});
|
||||
onMount(() => {
|
||||
navigator.storage.estimate().then(({ usage, quota }) => {
|
||||
if (usage && quota) {
|
||||
storageSpace = `Storage: ${formatBytes(usage)} / ${formatBytes(quota)}`;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let filePromises: Promise<File>[];
|
||||
let draggedFiles: File[] | undefined;
|
||||
let loading = false;
|
||||
$: disabled = loading || (!draggedFiles && !files);
|
||||
let filePromises: Promise<File>[];
|
||||
let draggedFiles: File[] | undefined;
|
||||
let loading = false;
|
||||
$: disabled = loading || (!draggedFiles && !files);
|
||||
|
||||
const dropHandle = async (event: DragEvent) => {
|
||||
loading = true;
|
||||
draggedFiles = [];
|
||||
filePromises = [];
|
||||
event.preventDefault();
|
||||
activeStyle = defaultStyle;
|
||||
const dropHandle = async (event: DragEvent) => {
|
||||
loading = true;
|
||||
draggedFiles = [];
|
||||
filePromises = [];
|
||||
event.preventDefault();
|
||||
activeStyle = defaultStyle;
|
||||
|
||||
if (event?.dataTransfer?.items) {
|
||||
for (const item of [...event.dataTransfer.items]) {
|
||||
const entry = item.webkitGetAsEntry();
|
||||
if (item.kind === 'file' && entry) {
|
||||
if (entry.isDirectory) {
|
||||
await scanFiles(entry, filePromises);
|
||||
} else {
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
draggedFiles.push(file);
|
||||
draggedFiles = draggedFiles;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (event?.dataTransfer?.items) {
|
||||
for (const item of [...event.dataTransfer.items]) {
|
||||
const entry = item.webkitGetAsEntry();
|
||||
if (item.kind === 'file' && entry) {
|
||||
if (entry.isDirectory) {
|
||||
await scanFiles(entry, filePromises);
|
||||
} else {
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
draggedFiles.push(file);
|
||||
draggedFiles = draggedFiles;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filePromises && filePromises.length > 0) {
|
||||
const files = await Promise.all(filePromises);
|
||||
if (files) {
|
||||
draggedFiles = [...draggedFiles, ...files];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (filePromises && filePromises.length > 0) {
|
||||
const files = await Promise.all(filePromises);
|
||||
if (files) {
|
||||
draggedFiles = [...draggedFiles, ...files];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loading = false;
|
||||
};
|
||||
loading = false;
|
||||
};
|
||||
|
||||
let defaultStyle =
|
||||
'flex flex-col justify-center items-center w-full h-64 bg-gray-50 rounded-lg border-2 border-gray-300 border-dashed cursor-pointer dark:hover:bg-bray-800 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600';
|
||||
let highlightStyle =
|
||||
'flex flex-col justify-center items-center w-full h-64 bg-gray-50 rounded-lg border-2 border-gray-300 border-dashed cursor-pointer dark:bg-bray-800 dark:bg-gray-700 bg-gray-100 dark:border-gray-600 dark:border-gray-500 dark:bg-gray-600';
|
||||
let defaultStyle =
|
||||
'flex flex-col justify-center items-center w-full h-64 bg-gray-50 rounded-lg border-2 border-gray-300 border-dashed cursor-pointer dark:hover:bg-bray-800 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600';
|
||||
let highlightStyle =
|
||||
'flex flex-col justify-center items-center w-full h-64 bg-gray-50 rounded-lg border-2 border-gray-300 border-dashed cursor-pointer dark:bg-bray-800 dark:bg-gray-700 bg-gray-100 dark:border-gray-600 dark:border-gray-500 dark:bg-gray-600';
|
||||
|
||||
let activeStyle = defaultStyle;
|
||||
let activeStyle = defaultStyle;
|
||||
</script>
|
||||
|
||||
<Modal title="Upload" bind:open outsideclose on:close={reset}>
|
||||
{#await promise}
|
||||
<h2 class="justify-center flex">Loading...</h2>
|
||||
<div class="text-center"><Spinner /></div>
|
||||
{:then}
|
||||
<Dropzone
|
||||
id="dropzone"
|
||||
on:drop={dropHandle}
|
||||
on:dragover={(event) => {
|
||||
event.preventDefault();
|
||||
activeStyle = highlightStyle;
|
||||
}}
|
||||
on:dragleave={(event) => {
|
||||
event.preventDefault();
|
||||
activeStyle = defaultStyle;
|
||||
}}
|
||||
on:click={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
defaultClass={activeStyle}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="mb-3 w-10 h-10 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||
/></svg
|
||||
>
|
||||
{#if files}
|
||||
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Upload {files.length}
|
||||
{files.length > 1 ? 'files' : 'file'}?
|
||||
</p>
|
||||
{:else if draggedFiles && draggedFiles.length > 0}
|
||||
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Upload {draggedFiles.length} hih
|
||||
{draggedFiles.length > 1 ? 'files' : 'file'}?
|
||||
</p>
|
||||
{:else if loading}
|
||||
<Spinner />
|
||||
{:else}
|
||||
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Drag and drop / <FileUpload bind:files accept=".mokuro,.zip,.cbz" multiple
|
||||
>choose files</FileUpload
|
||||
> /
|
||||
<FileUpload bind:files webkitdirectory>choose directory</FileUpload>
|
||||
</p>
|
||||
{/if}
|
||||
</Dropzone>
|
||||
{#await promise}
|
||||
<h2 class="justify-center flex">Loading...</h2>
|
||||
<div class="text-center"><Spinner /></div>
|
||||
{:then}
|
||||
<Dropzone
|
||||
id="dropzone"
|
||||
on:drop={dropHandle}
|
||||
on:dragover={(event) => {
|
||||
event.preventDefault();
|
||||
activeStyle = highlightStyle;
|
||||
}}
|
||||
on:dragleave={(event) => {
|
||||
event.preventDefault();
|
||||
activeStyle = defaultStyle;
|
||||
}}
|
||||
on:click={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
defaultClass={activeStyle}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="mb-3 w-10 h-10 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||
/></svg
|
||||
>
|
||||
{#if files}
|
||||
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Upload {files.length}
|
||||
{files.length > 1 ? 'files' : 'file'}?
|
||||
</p>
|
||||
{:else if draggedFiles && draggedFiles.length > 0}
|
||||
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Upload {draggedFiles.length} hih
|
||||
{draggedFiles.length > 1 ? 'files' : 'file'}?
|
||||
</p>
|
||||
{:else if loading}
|
||||
<Spinner />
|
||||
{:else}
|
||||
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Drag and drop / <FileUpload bind:files accept=".mokuro,.zip,.cbz" multiple
|
||||
>choose files</FileUpload
|
||||
> /
|
||||
<FileUpload bind:files webkitdirectory>choose directory</FileUpload>
|
||||
</p>
|
||||
{/if}
|
||||
</Dropzone>
|
||||
|
||||
<p class=" text-sm text-gray-500 dark:text-gray-400 text-center">{storageSpace}</p>
|
||||
<div class="flex flex-1 flex-col gap-2">
|
||||
<Button outline on:click={reset} {disabled} color="dark">Reset</Button>
|
||||
<Button outline on:click={onUpload} {disabled}>Upload</Button>
|
||||
</div>
|
||||
{/await}
|
||||
<p class=" text-sm text-gray-500 dark:text-gray-400 text-center">{storageSpace}</p>
|
||||
<div class="flex flex-1 flex-col gap-2">
|
||||
<Button outline on:click={reset} {disabled} color="dark">Reset</Button>
|
||||
<Button outline on:click={onUpload} {disabled}>Upload</Button>
|
||||
</div>
|
||||
{/await}
|
||||
</Modal>
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { currentVolume } from '$lib/catalog';
|
||||
import type { Volume } from '$lib/types';
|
||||
import { page } from '$app/stores';
|
||||
import { currentVolume } from '$lib/catalog';
|
||||
import type { Volume } from '$lib/types';
|
||||
|
||||
export let volume: Volume;
|
||||
const { volumeName, files } = volume;
|
||||
export let volume: Volume;
|
||||
const { volumeName, files } = volume;
|
||||
|
||||
function onClick() {
|
||||
currentVolume.set(volume);
|
||||
}
|
||||
function onClick() {
|
||||
currentVolume.set(volume);
|
||||
}
|
||||
</script>
|
||||
|
||||
<a href={`${$page.params.manga}/${volumeName}`} on:click={onClick}>
|
||||
<div class="flex flex-col gap-[5px] text-center">
|
||||
{volumeName}
|
||||
{#if files}
|
||||
<img
|
||||
src={URL.createObjectURL(Object.values(files)[0])}
|
||||
alt="img"
|
||||
class="object-contain w-[250px] h-[350px] bg-black border-gray-900 border"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-col gap-[5px] text-center">
|
||||
{volumeName}
|
||||
{#if files}
|
||||
<img
|
||||
src={URL.createObjectURL(Object.values(files)[0])}
|
||||
alt="img"
|
||||
class="object-contain w-[250px] h-[350px] bg-black border-gray-900 border"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { initPanzoom } from './util';
|
||||
import { initPanzoom } from './util';
|
||||
</script>
|
||||
|
||||
<div use:initPanzoom>
|
||||
<slot />
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './util'
|
||||
export {default as Panzoom} from './Panzoom.svelte'
|
||||
export * from './util';
|
||||
export { default as Panzoom } from './Panzoom.svelte';
|
||||
|
||||
@@ -37,20 +37,20 @@ export function initPanzoom(node: HTMLElement) {
|
||||
}
|
||||
});
|
||||
|
||||
panzoomStore.set(pz)
|
||||
panzoomStore.set(pz);
|
||||
}
|
||||
|
||||
type PanX = 'left' | 'center' | 'right'
|
||||
type PanY = 'top' | 'center' | 'bottom'
|
||||
type PanX = 'left' | 'center' | 'right';
|
||||
type PanY = 'top' | 'center' | 'bottom';
|
||||
|
||||
export function panAlign(alignX: PanX, alignY: PanY) {
|
||||
if (!pz || !container) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const { scale } = pz.getTransform();
|
||||
const { innerWidth, innerHeight } = window
|
||||
const { offsetWidth, offsetHeight } = container
|
||||
const { innerWidth, innerHeight } = window;
|
||||
const { offsetWidth, offsetHeight } = container;
|
||||
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
@@ -63,7 +63,7 @@ export function panAlign(alignX: PanX, alignY: PanY) {
|
||||
x = (innerWidth - offsetWidth * scale) / 2;
|
||||
break;
|
||||
case 'right':
|
||||
x = (innerWidth - offsetWidth * scale);
|
||||
x = innerWidth - offsetWidth * scale;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -75,11 +75,11 @@ export function panAlign(alignX: PanX, alignY: PanY) {
|
||||
y = (innerHeight - offsetHeight * scale) / 2;
|
||||
break;
|
||||
case 'bottom':
|
||||
y = (innerHeight - offsetHeight * scale);
|
||||
y = innerHeight - offsetHeight * scale;
|
||||
break;
|
||||
}
|
||||
|
||||
pz?.moveTo(x, y)
|
||||
pz?.moveTo(x, y);
|
||||
}
|
||||
|
||||
export function zoomOriginal() {
|
||||
@@ -90,12 +90,11 @@ export function zoomOriginal() {
|
||||
|
||||
export function zoomFitToWidth() {
|
||||
if (!pz || !container) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
const { innerWidth } = window
|
||||
const { innerWidth } = window;
|
||||
|
||||
const scale =
|
||||
(1 / pz.getTransform().scale) * (innerWidth / container.offsetWidth);
|
||||
const scale = (1 / pz.getTransform().scale) * (innerWidth / container.offsetWidth);
|
||||
|
||||
pz.moveTo(0, 0);
|
||||
pz.zoomTo(0, 0, scale);
|
||||
@@ -104,13 +103,12 @@ export function zoomFitToWidth() {
|
||||
|
||||
export function zoomFitToScreen() {
|
||||
if (!pz || !container) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
const { innerWidth, innerHeight } = window
|
||||
const { innerWidth, innerHeight } = window;
|
||||
const scaleX = innerWidth / container.offsetWidth;
|
||||
const scaleY = innerHeight / container.offsetHeight;
|
||||
const scale =
|
||||
(1 / pz.getTransform().scale) * Math.min(scaleX, scaleY);
|
||||
const scale = (1 / pz.getTransform().scale) * Math.min(scaleX, scaleY);
|
||||
pz.moveTo(0, 0);
|
||||
pz.zoomTo(0, 0, scale);
|
||||
panAlign('center', 'center');
|
||||
@@ -121,7 +119,7 @@ export function keepZoomStart() {
|
||||
}
|
||||
|
||||
export function zoomDefault() {
|
||||
const zoomDefault = get(settings).zoomDefault
|
||||
const zoomDefault = get(settings).zoomDefault;
|
||||
switch (zoomDefault) {
|
||||
case 'zoomFitToScreen':
|
||||
zoomFitToScreen();
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
import { browser } from "$app/environment";
|
||||
import { zoomDefault } from "$lib/panzoom";
|
||||
import { writable } from "svelte/store";
|
||||
import { browser } from '$app/environment';
|
||||
import { zoomDefault } from '$lib/panzoom';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export type FontSize = 'auto' |
|
||||
'9' |
|
||||
'10' |
|
||||
'11' |
|
||||
'12' |
|
||||
'14' |
|
||||
'16' |
|
||||
'18' |
|
||||
'20' |
|
||||
'24' |
|
||||
'32' |
|
||||
'40' |
|
||||
'48' |
|
||||
'60'
|
||||
export type FontSize =
|
||||
| 'auto'
|
||||
| '9'
|
||||
| '10'
|
||||
| '11'
|
||||
| '12'
|
||||
| '14'
|
||||
| '16'
|
||||
| '18'
|
||||
| '20'
|
||||
| '24'
|
||||
| '32'
|
||||
| '40'
|
||||
| '48'
|
||||
| '60';
|
||||
|
||||
export type ZoomModes = 'zoomFitToScreen' |
|
||||
'zoomFitToWidth' |
|
||||
'zoomOriginal' |
|
||||
'keepZoom' |
|
||||
'keepZoomStart'
|
||||
export type ZoomModes =
|
||||
| 'zoomFitToScreen'
|
||||
| 'zoomFitToWidth'
|
||||
| 'zoomOriginal'
|
||||
| 'keepZoom'
|
||||
| 'keepZoomStart';
|
||||
|
||||
export type Settings = {
|
||||
rightToLeft: boolean;
|
||||
@@ -37,7 +39,7 @@ export type Settings = {
|
||||
zoomDefault: ZoomModes;
|
||||
};
|
||||
|
||||
export type SettingsKey = keyof Settings
|
||||
export type SettingsKey = keyof Settings;
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
rightToLeft: true,
|
||||
@@ -51,12 +53,12 @@ const defaultSettings: Settings = {
|
||||
backgroundColor: '#0d0d0f',
|
||||
fontSize: 'auto',
|
||||
zoomDefault: 'zoomFitToScreen'
|
||||
}
|
||||
};
|
||||
|
||||
const stored = browser ? window.localStorage.getItem('settings') : undefined
|
||||
const initialSettings: Settings = stored && browser ? JSON.parse(stored) : defaultSettings
|
||||
const stored = browser ? window.localStorage.getItem('settings') : undefined;
|
||||
const initialSettings: Settings = stored && browser ? JSON.parse(stored) : defaultSettings;
|
||||
|
||||
export * from './progress'
|
||||
export * from './progress';
|
||||
|
||||
export const settings = writable<Settings>(initialSettings);
|
||||
|
||||
@@ -76,7 +78,6 @@ export function resetSettings() {
|
||||
|
||||
settings.subscribe((settings) => {
|
||||
if (browser) {
|
||||
window.localStorage.setItem('settings', JSON.stringify(settings))
|
||||
window.localStorage.setItem('settings', JSON.stringify(settings));
|
||||
}
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { browser } from "$app/environment";
|
||||
import { writable } from "svelte/store";
|
||||
import { browser } from '$app/environment';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
type Progress = Record<string, number> | undefined
|
||||
type Progress = Record<string, number> | undefined;
|
||||
|
||||
const stored = browser ? window.localStorage.getItem('progress') : undefined
|
||||
const initial: Progress = stored && browser ? JSON.parse(stored) : undefined
|
||||
const stored = browser ? window.localStorage.getItem('progress') : undefined;
|
||||
const initial: Progress = stored && browser ? JSON.parse(stored) : undefined;
|
||||
|
||||
export const progress = writable<Progress>(initial);
|
||||
|
||||
@@ -19,7 +19,6 @@ export function updateProgress(volume: string, value: number) {
|
||||
|
||||
progress.subscribe((progress) => {
|
||||
if (browser) {
|
||||
window.localStorage.setItem('progress', progress ? JSON.stringify(progress) : '')
|
||||
window.localStorage.setItem('progress', progress ? JSON.stringify(progress) : '');
|
||||
}
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
@@ -20,10 +20,10 @@ export type MokuroData = {
|
||||
volume: string;
|
||||
volume_uuid: string;
|
||||
pages: Page[];
|
||||
}
|
||||
};
|
||||
|
||||
export type Volume = {
|
||||
mokuroData: MokuroData;
|
||||
volumeName: string;
|
||||
files: Record<string, File>;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import { db } from "$lib/catalog/db";
|
||||
import type { Volume } from "$lib/types";
|
||||
import { showSnackbar } from "$lib/util/snackbar";
|
||||
import { requestPersistentStorage } from "$lib/util/upload";
|
||||
import { BlobReader, ZipReader, BlobWriter, getMimeType } from "@zip.js/zip.js";
|
||||
import { db } from '$lib/catalog/db';
|
||||
import type { Volume } from '$lib/types';
|
||||
import { showSnackbar } from '$lib/util/snackbar';
|
||||
import { requestPersistentStorage } from '$lib/util/upload';
|
||||
import { BlobReader, ZipReader, BlobWriter, getMimeType } from '@zip.js/zip.js';
|
||||
|
||||
export async function unzipManga(file: File) {
|
||||
const zipFileReader = new BlobReader(file);
|
||||
const zipReader = new ZipReader(zipFileReader);
|
||||
|
||||
const entries = await zipReader.getEntries()
|
||||
const entries = await zipReader.getEntries();
|
||||
const unzippedFiles: Record<string, File> = {};
|
||||
|
||||
|
||||
for (const entry of entries) {
|
||||
const mime = getMimeType(entry.filename);
|
||||
if (mime === 'image/jpeg' || mime === 'image/png') {
|
||||
const blob = await entry.getData?.(new BlobWriter(mime))
|
||||
const blob = await entry.getData?.(new BlobWriter(mime));
|
||||
if (blob) {
|
||||
const file = new File([blob], entry.filename, { type: mime })
|
||||
unzippedFiles[entry.filename] = file
|
||||
const file = new File([blob], entry.filename, { type: mime });
|
||||
unzippedFiles[entry.filename] = file;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +31,7 @@ function getDetails(file: File) {
|
||||
return {
|
||||
filename,
|
||||
ext
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function getFile(fileEntry: FileSystemFileEntry) {
|
||||
@@ -50,31 +49,31 @@ export async function scanFiles(item: FileSystemEntry, files: Promise<File | und
|
||||
directoryReader.readEntries(async (entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isFile) {
|
||||
files.push(getFile(entry as FileSystemFileEntry))
|
||||
files.push(getFile(entry as FileSystemFileEntry));
|
||||
} else {
|
||||
await scanFiles(entry, files);
|
||||
}
|
||||
}
|
||||
resolve()
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function processFiles(files: File[]) {
|
||||
const zipTypes = ['zip', 'cbz']
|
||||
const zipTypes = ['zip', 'cbz'];
|
||||
const volumes: Record<string, Volume> = {};
|
||||
const mangas: string[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const { ext, filename } = getDetails(file)
|
||||
const { type, webkitRelativePath } = file
|
||||
const { ext, filename } = getDetails(file);
|
||||
const { type, webkitRelativePath } = file;
|
||||
|
||||
if (ext === 'mokuro') {
|
||||
const mokuroData: Volume['mokuroData'] = JSON.parse(await file.text())
|
||||
const mokuroData: Volume['mokuroData'] = JSON.parse(await file.text());
|
||||
|
||||
if (!mangas.includes(mokuroData.title_uuid)) {
|
||||
mangas.push(mokuroData.title_uuid)
|
||||
mangas.push(mokuroData.title_uuid);
|
||||
}
|
||||
|
||||
volumes[filename] = {
|
||||
@@ -85,21 +84,20 @@ export async function processFiles(files: File[]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mimeType = type || getMimeType(file.name)
|
||||
const mimeType = type || getMimeType(file.name);
|
||||
|
||||
if (mimeType === 'image/jpeg' || mimeType === 'image/png') {
|
||||
|
||||
if (webkitRelativePath) {
|
||||
const imageName = webkitRelativePath.split('/').at(-1)
|
||||
const vol = webkitRelativePath.split('/').at(-2)
|
||||
const imageName = webkitRelativePath.split('/').at(-1);
|
||||
const vol = webkitRelativePath.split('/').at(-2);
|
||||
|
||||
if (vol && imageName) {
|
||||
volumes[vol] = {
|
||||
...volumes[vol],
|
||||
files: {
|
||||
...volumes[vol]?.files,
|
||||
[imageName]: file,
|
||||
},
|
||||
[imageName]: file
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -107,12 +105,12 @@ export async function processFiles(files: File[]) {
|
||||
}
|
||||
|
||||
if (zipTypes.includes(ext)) {
|
||||
const unzippedFiles = await unzipManga(file)
|
||||
const unzippedFiles = await unzipManga(file);
|
||||
|
||||
volumes[filename] = {
|
||||
...volumes[filename],
|
||||
files: unzippedFiles
|
||||
}
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
@@ -122,19 +120,19 @@ export async function processFiles(files: File[]) {
|
||||
|
||||
if (vols.length > 0) {
|
||||
const valid = vols.map((vol) => {
|
||||
const { files, mokuroData, volumeName } = vol
|
||||
const { files, mokuroData, volumeName } = vol;
|
||||
if (!mokuroData || !volumeName) {
|
||||
showSnackbar('Missing .mokuro file')
|
||||
showSnackbar('Missing .mokuro file');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!files) {
|
||||
showSnackbar('Missing image files')
|
||||
showSnackbar('Missing image files');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!valid.includes(false)) {
|
||||
await requestPersistentStorage();
|
||||
@@ -143,19 +141,21 @@ export async function processFiles(files: File[]) {
|
||||
const existingCatalog = await db.catalog.get(key);
|
||||
|
||||
const filtered = vols.filter((vol) => {
|
||||
return !existingCatalog?.manga.some(manga => {
|
||||
return manga.mokuroData.volume_uuid === vol.mokuroData.volume_uuid
|
||||
}) && key === vol.mokuroData.title_uuid
|
||||
})
|
||||
return (
|
||||
!existingCatalog?.manga.some((manga) => {
|
||||
return manga.mokuroData.volume_uuid === vol.mokuroData.volume_uuid;
|
||||
}) && key === vol.mokuroData.title_uuid
|
||||
);
|
||||
});
|
||||
|
||||
if (existingCatalog) {
|
||||
await db.catalog.update(key, { manga: [...existingCatalog.manga, ...filtered] })
|
||||
await db.catalog.update(key, { manga: [...existingCatalog.manga, ...filtered] });
|
||||
} else {
|
||||
await db.catalog.add({ id: key, manga: filtered })
|
||||
await db.catalog.add({ id: key, manga: filtered });
|
||||
}
|
||||
}
|
||||
|
||||
showSnackbar('Catalog updated successfully')
|
||||
showSnackbar('Catalog updated successfully');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './snackbar'
|
||||
export * from './upload'
|
||||
export * from './misc'
|
||||
export * from './modals'
|
||||
export * from './snackbar';
|
||||
export * from './upload';
|
||||
export * from './misc';
|
||||
export * from './modals';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { writable } from "svelte/store";
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
type ConfirmationPopup = {
|
||||
open: boolean;
|
||||
@@ -14,4 +14,3 @@ export function promptConfirmation(message: string, onConfirm?: () => void) {
|
||||
onConfirm
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { writable } from "svelte/store";
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
type Snackbar = {
|
||||
visible: boolean;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script lang="ts">
|
||||
import '../app.postcss';
|
||||
import NavBar from '$lib/components/NavBar.svelte';
|
||||
import Snackbar from '$lib/components/Snackbar.svelte';
|
||||
import ConfirmationPopup from '$lib/components/ConfirmationPopup.svelte';
|
||||
import { settings } from '$lib/settings';
|
||||
import '../app.postcss';
|
||||
import NavBar from '$lib/components/NavBar.svelte';
|
||||
import Snackbar from '$lib/components/Snackbar.svelte';
|
||||
import ConfirmationPopup from '$lib/components/ConfirmationPopup.svelte';
|
||||
import { settings } from '$lib/settings';
|
||||
</script>
|
||||
|
||||
<div class=" h-full min-h-[100svh] text-white" style:background-color={$settings.backgroundColor}>
|
||||
<NavBar />
|
||||
<Snackbar />
|
||||
<ConfirmationPopup />
|
||||
<slot />
|
||||
<NavBar />
|
||||
<Snackbar />
|
||||
<ConfirmationPopup />
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Catalog from '$lib/components/Catalog.svelte';
|
||||
import Catalog from '$lib/components/Catalog.svelte';
|
||||
</script>
|
||||
|
||||
<div class="p-2">
|
||||
<Catalog />
|
||||
<Catalog />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
|
||||
<h1>{$page.status}: {$page.error?.message}</h1>
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { currentManga } from '$lib/catalog';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import VolumeItem from '$lib/components/VolumeItem.svelte';
|
||||
import { Button } from 'flowbite-svelte';
|
||||
import { db } from '$lib/catalog/db';
|
||||
import { promptConfirmation } from '$lib/util';
|
||||
import { currentManga } from '$lib/catalog';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import VolumeItem from '$lib/components/VolumeItem.svelte';
|
||||
import { Button } from 'flowbite-svelte';
|
||||
import { db } from '$lib/catalog/db';
|
||||
import { promptConfirmation } from '$lib/util';
|
||||
|
||||
const manga = $currentManga?.sort((a, b) => {
|
||||
if (a.volumeName < b.volumeName) {
|
||||
return -1;
|
||||
}
|
||||
if (a.volumeName > b.volumeName) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
const manga = $currentManga?.sort((a, b) => {
|
||||
if (a.volumeName < b.volumeName) {
|
||||
return -1;
|
||||
}
|
||||
if (a.volumeName > b.volumeName) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
if (!manga) {
|
||||
goto('/');
|
||||
}
|
||||
});
|
||||
onMount(() => {
|
||||
if (!manga) {
|
||||
goto('/');
|
||||
}
|
||||
});
|
||||
|
||||
async function confirmDelete() {
|
||||
const title = manga?.[0].mokuroData.title_uuid;
|
||||
await db.catalog.delete(title);
|
||||
goto('/');
|
||||
}
|
||||
async function confirmDelete() {
|
||||
const title = manga?.[0].mokuroData.title_uuid;
|
||||
await db.catalog.delete(title);
|
||||
goto('/');
|
||||
}
|
||||
|
||||
function onDelete() {
|
||||
promptConfirmation('Are you sure you want to delete this manga?', confirmDelete);
|
||||
}
|
||||
function onDelete() {
|
||||
promptConfirmation('Are you sure you want to delete this manga?', confirmDelete);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-2 flex flex-col gap-5">
|
||||
<div class="sm:block flex-col flex">
|
||||
<Button outline color="red" class="float-right" on:click={onDelete}>Delete manga</Button>
|
||||
</div>
|
||||
<div class="flex flex-row gap-5 flex-wrap">
|
||||
{#if manga}
|
||||
{#each manga as volume}
|
||||
<VolumeItem {volume} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="sm:block flex-col flex">
|
||||
<Button outline color="red" class="float-right" on:click={onDelete}>Delete manga</Button>
|
||||
</div>
|
||||
<div class="flex flex-row gap-5 flex-wrap">
|
||||
{#if manga}
|
||||
{#each manga as volume}
|
||||
<VolumeItem {volume} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { currentVolume } from '$lib/catalog';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { currentVolume } from '$lib/catalog';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
const volume = $currentVolume;
|
||||
const volume = $currentVolume;
|
||||
|
||||
onMount(() => {
|
||||
if (!volume) {
|
||||
goto('/');
|
||||
}
|
||||
});
|
||||
onMount(() => {
|
||||
if (!volume) {
|
||||
goto('/');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
|
||||
<style>
|
||||
:global(body.reader) {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
:global(body.reader) {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Reader from '$lib/components/Reader/Reader.svelte';
|
||||
import Reader from '$lib/components/Reader/Reader.svelte';
|
||||
</script>
|
||||
|
||||
<Reader />
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import Catalog from '$lib/components/Catalog.svelte';
|
||||
import FileUpload from '$lib/components/FileUpload.svelte';
|
||||
import { processFiles } from '$lib/upload';
|
||||
import Catalog from '$lib/components/Catalog.svelte';
|
||||
import FileUpload from '$lib/components/FileUpload.svelte';
|
||||
import { processFiles } from '$lib/upload';
|
||||
|
||||
let promise: Promise<void>;
|
||||
let promise: Promise<void>;
|
||||
|
||||
async function onUpload(files: FileList) {
|
||||
promise = processFiles([...files]);
|
||||
}
|
||||
async function onUpload(files: FileList) {
|
||||
promise = processFiles([...files]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Note: webkitdirectory is not fully supported and does not work on mobile -->
|
||||
@@ -15,5 +15,5 @@
|
||||
<FileUpload {onUpload} accept=".mokuro,.zip,.cbz" multiple>Upload files</FileUpload>
|
||||
|
||||
{#await promise}
|
||||
<h2>Loading...</h2>
|
||||
<h2>Loading...</h2>
|
||||
{/await}
|
||||
|
||||
@@ -3,20 +3,20 @@ import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
},
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
},
|
||||
|
||||
vitePlugin: {
|
||||
inspector: true
|
||||
}
|
||||
vitePlugin: {
|
||||
inspector: true
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
/** @type {import('tailwindcss').Config}*/
|
||||
const config = {
|
||||
content: ['./src/**/*.{html,js,svelte,ts}', './node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}', './node_modules/flowbite-svelte-icons/**/*.{html,js,svelte,ts}'],
|
||||
content: [
|
||||
'./src/**/*.{html,js,svelte,ts}',
|
||||
'./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}',
|
||||
'./node_modules/flowbite-svelte-icons/**/*.{html,js,svelte,ts}'
|
||||
],
|
||||
|
||||
theme: {
|
||||
theme: {
|
||||
fontFamily: {
|
||||
'sans': 'Verdana, Geneva, Tahoma, sans-serif'
|
||||
sans: 'Verdana, Geneva, Tahoma, sans-serif'
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#FFF5F2',
|
||||
100: '#FFF1EE',
|
||||
@@ -21,12 +25,12 @@ const config = {
|
||||
900: '#A5371B'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
plugins: [require('flowbite/plugin')],
|
||||
plugins: [require('flowbite/plugin')],
|
||||
|
||||
darkMode: 'class',
|
||||
darkMode: 'class'
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
server: {
|
||||
host: true
|
||||
}
|
||||
plugins: [sveltekit()],
|
||||
server: {
|
||||
host: true
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user