1321
package-lock.json
generated
1321
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,14 +16,20 @@
|
|||||||
"@sveltejs/kit": "^1.20.4",
|
"@sveltejs/kit": "^1.20.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "^5.45.0",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.28.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-svelte": "^2.30.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": "^2.8.0",
|
||||||
"prettier-plugin-svelte": "^2.10.1",
|
"prettier-plugin-svelte": "^2.10.1",
|
||||||
"sass": "^1.64.2",
|
"sass": "^1.64.2",
|
||||||
"svelte": "^4.0.5",
|
"svelte": "^4.0.5",
|
||||||
"svelte-check": "^3.4.3",
|
"svelte-check": "^3.4.3",
|
||||||
|
"tailwindcss": "^3.3.2",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"vite": "^4.4.2"
|
"vite": "^4.4.2"
|
||||||
|
|||||||
13
postcss.config.cjs
Normal file
13
postcss.config.cjs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" class="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
|||||||
4
src/app.postcss
Normal file
4
src/app.postcss
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/* Write your global styles here, in PostCSS syntax */
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
@@ -9,7 +9,6 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border: none;
|
|
||||||
font-family: $font-family;
|
font-family: $font-family;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: white;
|
color: white;
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
|
||||||
<svg width="64px" height="64px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
|
|
||||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
|
||||||
|
|
||||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -1,50 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
type ButtonType = 'primary' | 'secondary' | 'tertiary' | 'danger';
|
|
||||||
|
|
||||||
export let variant: ButtonType = 'primary';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button class={variant} {...$$restProps} on:click>
|
|
||||||
<slot />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@mixin button(
|
|
||||||
$bg-color: $primary-color,
|
|
||||||
$color: $secondary-color,
|
|
||||||
$active-color: $primary-accent-color
|
|
||||||
) {
|
|
||||||
border-style: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 2px 10px 2px 10px;
|
|
||||||
font-family: $font-family;
|
|
||||||
height: 40px;
|
|
||||||
max-height: 40px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: $color;
|
|
||||||
border: 1px solid;
|
|
||||||
background-color: $bg-color;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background-color: $active-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.primary {
|
|
||||||
@include button();
|
|
||||||
}
|
|
||||||
.secondary {
|
|
||||||
@include button($secondary-color, $primary-color, $secondary-accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tertiary {
|
|
||||||
@include button(transparent, $secondary-color, $primary-accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.danger {
|
|
||||||
@include button($danger-accent-color, $danger-color, $danger-active-color);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -3,17 +3,20 @@
|
|||||||
import CatalogItem from './CatalogItem.svelte';
|
import CatalogItem from './CatalogItem.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $catalog && $catalog.length > 0}
|
{#if $catalog}
|
||||||
<div class="container">
|
{#if $catalog.length > 0}
|
||||||
{#each $catalog as { manga }}
|
<div class="container">
|
||||||
<CatalogItem {manga} />
|
{#each $catalog as { manga }}
|
||||||
{/each}
|
<CatalogItem {manga} />
|
||||||
</div>
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>Your catalog is currently empty.</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="empty-state">
|
<p>Loading...</p>
|
||||||
<p>Your catalog is currently empty.</p>
|
|
||||||
<a href="upload">Add manga</a>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let files: FileList | null | undefined = undefined;
|
import { A, Fileupload, Label } from 'flowbite-svelte';
|
||||||
|
|
||||||
|
export let files: FileList | undefined = undefined;
|
||||||
export let onUpload: ((files: FileList) => void) | undefined = undefined;
|
export let onUpload: ((files: FileList) => void) | undefined = undefined;
|
||||||
|
|
||||||
let input: HTMLInputElement;
|
let input: HTMLInputElement;
|
||||||
@@ -13,38 +15,15 @@
|
|||||||
function onClick() {
|
function onClick() {
|
||||||
input.click();
|
input.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDrop(event: DragEvent) {
|
|
||||||
const items = event.dataTransfer?.items;
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<input type="file" bind:files bind:this={input} on:change={handleChange} {...$$restProps} />
|
<input
|
||||||
|
type="file"
|
||||||
|
bind:files
|
||||||
|
bind:this={input}
|
||||||
|
on:change={handleChange}
|
||||||
|
{...$$restProps}
|
||||||
|
class="hidden"
|
||||||
|
/>
|
||||||
|
|
||||||
<button
|
<A on:click={onClick}><slot>Upload</slot></A>
|
||||||
on:click={onClick}
|
|
||||||
on:dragover|preventDefault
|
|
||||||
on:drop|preventDefault|stopPropagation={onDrop}
|
|
||||||
>
|
|
||||||
<p><slot>Upload</slot></p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
width: 500px;
|
|
||||||
height: 100px;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 2px dashed $secondary-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 16px;
|
|
||||||
color: $secondary-color;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,72 +1,43 @@
|
|||||||
<script lang="ts" context="module">
|
|
||||||
export let navbarTitle = writable<string | undefined>(undefined);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { Navbar, NavBrand } from 'flowbite-svelte';
|
||||||
|
import { UserSettingsSolid, UploadSolid } from 'flowbite-svelte-icons';
|
||||||
import { afterNavigate } from '$app/navigation';
|
import { afterNavigate } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
import Settings from './Settings.svelte';
|
||||||
|
import UploadModal from './UploadModal.svelte';
|
||||||
|
|
||||||
import { currentManga, currentVolume } from '$lib/catalog';
|
let settingsHidden = true;
|
||||||
import SettingsIcon from '$lib/assets/svgs/settings-svgrepo-com.svg';
|
let uploadModalOpen = false;
|
||||||
|
let isReader = false;
|
||||||
import { writable } from 'svelte/store';
|
|
||||||
let title: string | undefined = 'Mokuro';
|
|
||||||
let back: string | undefined = undefined;
|
|
||||||
|
|
||||||
afterNavigate(() => {
|
afterNavigate(() => {
|
||||||
window.document.body.classList.remove('reader');
|
isReader = $page.route.id === '/[manga]/[volume]';
|
||||||
|
|
||||||
switch ($page?.route.id) {
|
if (isReader) {
|
||||||
case '/[manga]':
|
window.document.body.classList.add('reader');
|
||||||
title = $currentManga?.[0].mokuroData.title;
|
} else {
|
||||||
back = '/';
|
window.document.body.classList.remove('reader');
|
||||||
break;
|
|
||||||
case '/[manga]/[volume]':
|
|
||||||
window.document.body.classList.add('reader');
|
|
||||||
title = $currentVolume?.volumeName;
|
|
||||||
back = '/manga';
|
|
||||||
break;
|
|
||||||
case '/upload':
|
|
||||||
title = 'Upload';
|
|
||||||
back = '/';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
title = 'Mokuro';
|
|
||||||
back = undefined;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav>
|
<div class="relative z-10">
|
||||||
<div>
|
<Navbar hidden={isReader}>
|
||||||
{#if back}
|
<NavBrand href="/">
|
||||||
<a href={back}><h2>Back</h2></a>
|
<span class="text-xl font-semibold dark:text-white">Mokuro</span>
|
||||||
<h2>{title}</h2>
|
</NavBrand>
|
||||||
{:else}
|
<div class="flex md:order-2 gap-5">
|
||||||
<a href="/"><h2>{title}</h2></a>
|
<UserSettingsSolid class="hover:text-primary-700" on:click={() => (settingsHidden = false)} />
|
||||||
{/if}
|
<UploadSolid class="hover:text-primary-700" on:click={() => (uploadModalOpen = true)} />
|
||||||
<img src={SettingsIcon} alt="settings" />
|
</div>
|
||||||
</div>
|
</Navbar>
|
||||||
</nav>
|
{#if isReader}
|
||||||
|
<UserSettingsSolid
|
||||||
|
class="hover:text-primary-700 absolute right-5 top-5 opacity-10 hover:opacity-100"
|
||||||
|
on:click={() => (settingsHidden = false)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<Settings bind:hidden={settingsHidden} />
|
||||||
img {
|
<UploadModal bind:open={uploadModalOpen} />
|
||||||
width: 32px;
|
|
||||||
fill: #000;
|
|
||||||
}
|
|
||||||
nav {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
background-color: $primary-color;
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 10px 0 10px;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
import type { Page } from '$lib/types';
|
import type { Page } from '$lib/types';
|
||||||
|
|
||||||
export let page: Page;
|
export let page: Page;
|
||||||
|
export let left: () => void;
|
||||||
|
export let right: () => void;
|
||||||
|
|
||||||
let bold = false;
|
let bold = false;
|
||||||
|
|
||||||
@@ -38,14 +40,10 @@
|
|||||||
return textBox;
|
return textBox;
|
||||||
});
|
});
|
||||||
|
|
||||||
export let src;
|
export let src: Blob;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div style:--bold={fontWeight}>
|
<div>
|
||||||
<div class="bold">
|
|
||||||
<label>Bold</label>
|
|
||||||
<input bind:checked={bold} type="checkbox" placeholder="????" />
|
|
||||||
</div>
|
|
||||||
<img draggable="false" src={URL.createObjectURL(src)} alt="img" />
|
<img draggable="false" src={URL.createObjectURL(src)} alt="img" />
|
||||||
{#each textBoxes as { left, top, width, height, lines, fontSize, writingMode }}
|
{#each textBoxes as { left, top, width, height, lines, fontSize, writingMode }}
|
||||||
<div
|
<div
|
||||||
@@ -63,6 +61,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div />
|
<div />
|
||||||
{/each}
|
{/each}
|
||||||
|
<button
|
||||||
|
on:click={left}
|
||||||
|
class={`left-0 top-0 absolute h-full w-[200%]`}
|
||||||
|
style:margin-left={page.img_width * -2 + 'px'}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
on:click={right}
|
||||||
|
class={`right-0 top-0 absolute h-full w-[200%]`}
|
||||||
|
style:margin-right={page.img_width * -2 + 'px'}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -71,14 +79,6 @@
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bold {
|
|
||||||
position: absolute;
|
|
||||||
color: #000;
|
|
||||||
right: 10px;
|
|
||||||
bottom: 10px;
|
|
||||||
z-index: 99;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-box {
|
.text-box {
|
||||||
color: black;
|
color: black;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { currentManga, currentVolume } from '$lib/catalog';
|
import { currentVolume } from '$lib/catalog';
|
||||||
import Button from '$lib/components/Button.svelte';
|
import { Panzoom } from '$lib/panzoom';
|
||||||
import { Panzoom, zoomOriginal } from '$lib/panzoom';
|
|
||||||
import MangaPage from './MangaPage.svelte';
|
import MangaPage from './MangaPage.svelte';
|
||||||
|
|
||||||
const volume = $currentVolume;
|
const volume = $currentVolume;
|
||||||
@@ -19,22 +18,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if volume}
|
{#if volume && pages}
|
||||||
<div>
|
|
||||||
<Button on:click={zoomOriginal}>Reset Zoom</Button>
|
|
||||||
<Button on:click={left}>{'<'}</Button>
|
|
||||||
<Button on:click={right}>{'>'}</Button>
|
|
||||||
</div>
|
|
||||||
<Panzoom>
|
<Panzoom>
|
||||||
<MangaPage page={pages[page - 1]} src={Object.values(volume?.files)[page - 1]} />
|
<MangaPage page={pages[page - 1]} src={Object.values(volume?.files)[page - 1]} {left} {right} />
|
||||||
</Panzoom>
|
</Panzoom>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
|
||||||
div {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 10px;
|
|
||||||
left: 10px;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
61
src/lib/components/Settings.svelte
Normal file
61
src/lib/components/Settings.svelte
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Drawer, CloseButton, Toggle, Select, Input, Label } from 'flowbite-svelte';
|
||||||
|
import { UserSettingsSolid } from 'flowbite-svelte-icons';
|
||||||
|
import { sineIn } from 'svelte/easing';
|
||||||
|
import { settingsStore, updateSetting } from '$lib/settings';
|
||||||
|
|
||||||
|
let transitionParams = {
|
||||||
|
x: 320,
|
||||||
|
duration: 200,
|
||||||
|
easing: sineIn
|
||||||
|
};
|
||||||
|
|
||||||
|
export let hidden = true;
|
||||||
|
|
||||||
|
let selected = 'us';
|
||||||
|
|
||||||
|
let countries = [
|
||||||
|
{ value: 'us', name: 'auto' },
|
||||||
|
{ value: 'ca', name: 'Canada' },
|
||||||
|
{ value: 'fr', name: 'France' }
|
||||||
|
];
|
||||||
|
|
||||||
|
$: toggles = [
|
||||||
|
{ key: 'rightToLeft', text: 'Right to left', value: $settingsStore.rightToLeft },
|
||||||
|
{ key: 'singlePageView', text: 'Single page view', value: $settingsStore.singlePageView },
|
||||||
|
{ key: 'textEditable', text: 'Editable text', value: $settingsStore.textEditable },
|
||||||
|
{ key: 'textBoxBorders', text: 'Text box borders', value: $settingsStore.textBoxBorders },
|
||||||
|
{ key: 'displayOCR', text: 'OCR enabled', value: $settingsStore.displayOCR }
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
placement="right"
|
||||||
|
transitionType="fly"
|
||||||
|
width="lg:w-1/4 md:w-1/2 w-full"
|
||||||
|
{transitionParams}
|
||||||
|
bind:hidden
|
||||||
|
id="sidebar1"
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
{#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={countries} bind:value={selected} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Background color:</Label>
|
||||||
|
<Input type="color" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
@@ -1,25 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { snackbarStore } from '$lib/util/snackbar';
|
import { snackbarStore } from '$lib/util/snackbar';
|
||||||
import { fade, fly } from 'svelte/transition';
|
import { Toast } from 'flowbite-svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $snackbarStore?.message && $snackbarStore?.visible}
|
{#if $snackbarStore?.message && $snackbarStore?.visible}
|
||||||
<div in:fly={{ y: 200, duration: 500 }} out:fade={{ duration: 500 }}>
|
<Toast position="bottom-right">{$snackbarStore?.message}</Toast>
|
||||||
{$snackbarStore?.message}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
div {
|
|
||||||
background-color: rgba($primary-accent-color, 0.9);
|
|
||||||
position: fixed;
|
|
||||||
right: 10px;
|
|
||||||
bottom: 10px;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid $secondary-color;
|
|
||||||
min-width: 250px;
|
|
||||||
display: flex;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
145
src/lib/components/UploadModal.svelte
Normal file
145
src/lib/components/UploadModal.svelte
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<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';
|
||||||
|
|
||||||
|
export let open = false;
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
files = undefined;
|
||||||
|
draggedFiles = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let storageSpace = 'Loading...';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
navigator.storage.estimate().then(({ usage, quota }) => {
|
||||||
|
if (usage && quota) {
|
||||||
|
storageSpace = `Storage: ${formatBytes(usage)} / ${formatBytes(quota)}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let filePromises: Promise<File>[];
|
||||||
|
let draggedFiles: File[] | undefined;
|
||||||
|
|
||||||
|
const dropHandle = async (event: DragEvent) => {
|
||||||
|
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 (filePromises && filePromises.length > 0) {
|
||||||
|
const files = await Promise.all(filePromises);
|
||||||
|
if (files) {
|
||||||
|
draggedFiles = [...draggedFiles, ...files];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
</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}
|
||||||
|
<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={!files && !draggedFiles} color="dark">Reset</Button
|
||||||
|
>
|
||||||
|
<Button outline on:click={onUpload} disabled={!files && !draggedFiles}>Upload</Button>
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
</Modal>
|
||||||
45
src/lib/settings/index.ts
Normal file
45
src/lib/settings/index.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { browser } from "$app/environment";
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
|
type Settings = {
|
||||||
|
zoomMode: 'keep' | 'something'
|
||||||
|
rightToLeft: boolean;
|
||||||
|
singlePageView: boolean;
|
||||||
|
textEditable: boolean;
|
||||||
|
textBoxBorders: boolean;
|
||||||
|
displayOCR: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultSettings: Settings = {
|
||||||
|
zoomMode: 'keep',
|
||||||
|
rightToLeft: true,
|
||||||
|
singlePageView: false,
|
||||||
|
displayOCR: true,
|
||||||
|
textEditable: false,
|
||||||
|
textBoxBorders: false
|
||||||
|
}
|
||||||
|
|
||||||
|
const stored = browser ? window.localStorage.getItem('settings') : undefined
|
||||||
|
const initialSettings: Settings = stored && browser ? JSON.parse(stored) : defaultSettings
|
||||||
|
|
||||||
|
export const settingsStore = writable<Settings>(initialSettings);
|
||||||
|
|
||||||
|
export function updateSetting(key: string, value: any) {
|
||||||
|
settingsStore.update((settings) => {
|
||||||
|
return {
|
||||||
|
...settings,
|
||||||
|
[key]: value
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetSettings() {
|
||||||
|
settingsStore.set(defaultSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsStore.subscribe((settings) => {
|
||||||
|
if (browser) {
|
||||||
|
window.localStorage.setItem('settings', JSON.stringify(settings))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
@@ -35,26 +35,47 @@ function getDetails(file: File) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getFile(fileEntry: FileSystemFileEntry) {
|
||||||
|
try {
|
||||||
|
return new Promise<File>((resolve, reject) => fileEntry.file(resolve, reject));
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// export async function processVolumes(volumes: Volume[]) {
|
export async function scanFiles(item: FileSystemEntry, files: Promise<File | undefined>[]) {
|
||||||
// for (const { mokuroData, volumeName, archiveFile, files } of volumes) {
|
if (item.isDirectory) {
|
||||||
// const { title_uuid } = mokuroData
|
const directoryReader = (item as FileSystemDirectoryEntry).createReader();
|
||||||
// }
|
await new Promise<void>((resolve) => {
|
||||||
|
directoryReader.readEntries(async (entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isFile) {
|
||||||
|
files.push(getFile(entry as FileSystemFileEntry))
|
||||||
|
} else {
|
||||||
|
await scanFiles(entry, files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processFiles(files: File[]) {
|
||||||
// }
|
|
||||||
|
|
||||||
export async function processFiles(fileList: FileList) {
|
|
||||||
const files = [...fileList]
|
|
||||||
const zipTypes = ['zip', 'cbz']
|
const zipTypes = ['zip', 'cbz']
|
||||||
const volumes: Record<string, Volume> = {};
|
const volumes: Record<string, Volume> = {};
|
||||||
|
const mangas: string[] = [];
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const { ext, filename } = getDetails(file)
|
const { ext, filename } = getDetails(file)
|
||||||
const { type, webkitRelativePath } = file
|
const { type, webkitRelativePath } = file
|
||||||
|
|
||||||
if (ext === 'mokuro') {
|
if (ext === 'mokuro') {
|
||||||
const 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)
|
||||||
|
}
|
||||||
|
|
||||||
volumes[filename] = {
|
volumes[filename] = {
|
||||||
...volumes[filename],
|
...volumes[filename],
|
||||||
@@ -64,7 +85,10 @@ export async function processFiles(fileList: FileList) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'image/jpeg' || type === 'image/png') {
|
const mimeType = type || getMimeType(file.name)
|
||||||
|
|
||||||
|
if (mimeType === 'image/jpeg' || mimeType === 'image/png') {
|
||||||
|
|
||||||
if (webkitRelativePath) {
|
if (webkitRelativePath) {
|
||||||
const imageName = webkitRelativePath.split('/').at(-1)
|
const imageName = webkitRelativePath.split('/').at(-1)
|
||||||
const vol = webkitRelativePath.split('/').at(-2)
|
const vol = webkitRelativePath.split('/').at(-2)
|
||||||
@@ -115,18 +139,20 @@ export async function processFiles(fileList: FileList) {
|
|||||||
if (!valid.includes(false)) {
|
if (!valid.includes(false)) {
|
||||||
await requestPersistentStorage();
|
await requestPersistentStorage();
|
||||||
|
|
||||||
const key = vols[0].mokuroData.title_uuid;
|
for (const key of mangas) {
|
||||||
const existingCatalog = await db.catalog.get(key);
|
const existingCatalog = await db.catalog.get(key);
|
||||||
|
|
||||||
if (existingCatalog) {
|
|
||||||
const filtered = vols.filter((vol) => {
|
const filtered = vols.filter((vol) => {
|
||||||
return !existingCatalog.manga.some(manga => {
|
return !existingCatalog?.manga.some(manga => {
|
||||||
return manga.mokuroData.volume_uuid === vol.mokuroData.volume_uuid
|
return manga.mokuroData.volume_uuid === vol.mokuroData.volume_uuid
|
||||||
})
|
}) && key === vol.mokuroData.title_uuid
|
||||||
})
|
})
|
||||||
await db.catalog.update(key, { manga: [...existingCatalog.manga, ...filtered] })
|
|
||||||
} else {
|
if (existingCatalog) {
|
||||||
await db.catalog.put({ id: key, manga: vols })
|
await db.catalog.update(key, { manga: [...existingCatalog.manga, ...filtered] })
|
||||||
|
} else {
|
||||||
|
await db.catalog.put({ id: key, manga: filtered })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showSnackbar('Catalog updated successfully')
|
showSnackbar('Catalog updated successfully')
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
export async function requestPersistentStorage() {
|
export async function requestPersistentStorage() {
|
||||||
if (navigator.storage && navigator.storage.persist) {
|
if (navigator.storage && navigator.storage.persist) {
|
||||||
const isPersisted = await navigator.storage.persist();
|
await navigator.storage.persist();
|
||||||
console.log(`Persisted storage granted: ${isPersisted}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatBytes(bytes: number, decimals = 2) {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
|
||||||
|
const k = 1024;
|
||||||
|
const dm = decimals < 0 ? 0 : decimals;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
|
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
|
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import '../app.postcss';
|
||||||
import NavBar from '$lib/components/NavBar.svelte';
|
import NavBar from '$lib/components/NavBar.svelte';
|
||||||
import Snackbar from '$lib/components/Snackbar.svelte';
|
import Snackbar from '$lib/components/Snackbar.svelte';
|
||||||
import '../app.scss';
|
import '../app.scss';
|
||||||
|
|||||||
@@ -3,17 +3,41 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import VolumeItem from '$lib/components/VolumeItem.svelte';
|
import VolumeItem from '$lib/components/VolumeItem.svelte';
|
||||||
|
import { Button, Modal } from 'flowbite-svelte';
|
||||||
|
import { ExclamationCircleOutline } from 'flowbite-svelte-icons';
|
||||||
|
import { db } from '$lib/catalog/db';
|
||||||
|
|
||||||
const manga = $currentManga;
|
const manga = $currentManga?.sort((a, b) => {
|
||||||
|
if (a.volumeName < b.volumeName) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.volumeName > b.volumeName) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
let popupModal = false;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!manga) {
|
if (!manga) {
|
||||||
goto('/');
|
goto('/');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function onDelete() {
|
||||||
|
popupModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDelete() {
|
||||||
|
const title = manga?.[0].mokuroData.title_uuid;
|
||||||
|
await db.catalog.delete(title);
|
||||||
|
goto('/');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div class="float-right"><Button outline color="red" on:click={onDelete}>Delete manga</Button></div>
|
||||||
|
<div class="volumes">
|
||||||
{#if manga}
|
{#if manga}
|
||||||
{#each manga as volume}
|
{#each manga as volume}
|
||||||
<VolumeItem {volume} />
|
<VolumeItem {volume} />
|
||||||
@@ -21,8 +45,19 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Modal bind:open={popupModal} size="xs" autoclose>
|
||||||
|
<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">
|
||||||
|
Are you sure you want to delete this manga?
|
||||||
|
</h3>
|
||||||
|
<Button color="red" class="mr-2" on:click={confirmDelete}>Yes</Button>
|
||||||
|
<Button color="alternative">No</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
.volumes {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|||||||
@@ -1,19 +1,3 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { currentVolume } from '$lib/catalog';
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
|
|
||||||
const volume = $currentVolume;
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (!volume) {
|
|
||||||
goto('/');
|
|
||||||
} else {
|
|
||||||
window.document.body.classList.add('reader');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
let promise: Promise<void>;
|
let promise: Promise<void>;
|
||||||
|
|
||||||
async function onUpload(files: FileList) {
|
async function onUpload(files: FileList) {
|
||||||
promise = processFiles(files);
|
promise = processFiles([...files]);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
|
import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||||
import adapter from '@sveltejs/adapter-auto';
|
import adapter from '@sveltejs/adapter-auto';
|
||||||
import preprocess from 'svelte-preprocess';
|
import preprocess from 'svelte-preprocess';
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
preprocess: preprocess({
|
preprocess: [
|
||||||
scss: {
|
preprocess({
|
||||||
prependData: `@use './src/variables' as *;`
|
scss: {
|
||||||
}
|
prependData: `@use './src/variables' as *;`
|
||||||
}),
|
}
|
||||||
|
}),
|
||||||
|
vitePreprocess({})
|
||||||
|
],
|
||||||
|
|
||||||
kit: {
|
kit: {
|
||||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||||
|
|||||||
30
tailwind.config.cjs
Normal file
30
tailwind.config.cjs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/** @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}'],
|
||||||
|
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#FFF5F2',
|
||||||
|
100: '#FFF1EE',
|
||||||
|
200: '#FFE4DE',
|
||||||
|
300: '#FFD5CC',
|
||||||
|
400: '#FFBCAD',
|
||||||
|
500: '#FE795D',
|
||||||
|
600: '#EF562F',
|
||||||
|
700: '#EB4F27',
|
||||||
|
800: '#CC4522',
|
||||||
|
900: '#A5371B'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [require('flowbite/plugin')],
|
||||||
|
|
||||||
|
darkMode: 'class',
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
Reference in New Issue
Block a user