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",
|
||||
"@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"
|
||||
|
||||
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>
|
||||
<html lang="en">
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<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 {
|
||||
border: none;
|
||||
font-family: $font-family;
|
||||
background-color: transparent;
|
||||
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,7 +3,8 @@
|
||||
import CatalogItem from './CatalogItem.svelte';
|
||||
</script>
|
||||
|
||||
{#if $catalog && $catalog.length > 0}
|
||||
{#if $catalog}
|
||||
{#if $catalog.length > 0}
|
||||
<div class="container">
|
||||
{#each $catalog as { manga }}
|
||||
<CatalogItem {manga} />
|
||||
@@ -12,9 +13,11 @@
|
||||
{:else}
|
||||
<div class="empty-state">
|
||||
<p>Your catalog is currently empty.</p>
|
||||
<a href="upload">Add manga</a>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<p>Loading...</p>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<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;
|
||||
|
||||
let input: HTMLInputElement;
|
||||
@@ -13,38 +15,15 @@
|
||||
function onClick() {
|
||||
input.click();
|
||||
}
|
||||
|
||||
function onDrop(event: DragEvent) {
|
||||
const items = event.dataTransfer?.items;
|
||||
// TODO
|
||||
}
|
||||
</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
|
||||
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>
|
||||
<A on:click={onClick}><slot>Upload</slot></A>
|
||||
|
||||
@@ -1,72 +1,43 @@
|
||||
<script lang="ts" context="module">
|
||||
export let navbarTitle = writable<string | undefined>(undefined);
|
||||
</script>
|
||||
|
||||
<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 { currentManga, currentVolume } from '$lib/catalog';
|
||||
import SettingsIcon from '$lib/assets/svgs/settings-svgrepo-com.svg';
|
||||
|
||||
import { writable } from 'svelte/store';
|
||||
let title: string | undefined = 'Mokuro';
|
||||
let back: string | undefined = undefined;
|
||||
let settingsHidden = true;
|
||||
let uploadModalOpen = false;
|
||||
let isReader = false;
|
||||
|
||||
afterNavigate(() => {
|
||||
window.document.body.classList.remove('reader');
|
||||
isReader = $page.route.id === '/[manga]/[volume]';
|
||||
|
||||
switch ($page?.route.id) {
|
||||
case '/[manga]':
|
||||
title = $currentManga?.[0].mokuroData.title;
|
||||
back = '/';
|
||||
break;
|
||||
case '/[manga]/[volume]':
|
||||
if (isReader) {
|
||||
window.document.body.classList.add('reader');
|
||||
title = $currentVolume?.volumeName;
|
||||
back = '/manga';
|
||||
break;
|
||||
case '/upload':
|
||||
title = 'Upload';
|
||||
back = '/';
|
||||
break;
|
||||
default:
|
||||
title = 'Mokuro';
|
||||
back = undefined;
|
||||
break;
|
||||
} else {
|
||||
window.document.body.classList.remove('reader');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
<div>
|
||||
{#if back}
|
||||
<a href={back}><h2>Back</h2></a>
|
||||
<h2>{title}</h2>
|
||||
{:else}
|
||||
<a href="/"><h2>{title}</h2></a>
|
||||
{/if}
|
||||
<img src={SettingsIcon} alt="settings" />
|
||||
<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={() => (settingsHidden = false)} />
|
||||
<UploadSolid class="hover:text-primary-700" on:click={() => (uploadModalOpen = true)} />
|
||||
</div>
|
||||
</Navbar>
|
||||
{#if isReader}
|
||||
<UserSettingsSolid
|
||||
class="hover:text-primary-700 absolute right-5 top-5 opacity-10 hover:opacity-100"
|
||||
on:click={() => (settingsHidden = false)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<style lang="scss">
|
||||
img {
|
||||
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>
|
||||
<Settings bind:hidden={settingsHidden} />
|
||||
<UploadModal bind:open={uploadModalOpen} />
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import type { Page } from '$lib/types';
|
||||
|
||||
export let page: Page;
|
||||
export let left: () => void;
|
||||
export let right: () => void;
|
||||
|
||||
let bold = false;
|
||||
|
||||
@@ -38,14 +40,10 @@
|
||||
return textBox;
|
||||
});
|
||||
|
||||
export let src;
|
||||
export let src: Blob;
|
||||
</script>
|
||||
|
||||
<div style:--bold={fontWeight}>
|
||||
<div class="bold">
|
||||
<label>Bold</label>
|
||||
<input bind:checked={bold} type="checkbox" placeholder="????" />
|
||||
</div>
|
||||
<div>
|
||||
<img draggable="false" src={URL.createObjectURL(src)} alt="img" />
|
||||
{#each textBoxes as { left, top, width, height, lines, fontSize, writingMode }}
|
||||
<div
|
||||
@@ -63,6 +61,16 @@
|
||||
</div>
|
||||
<div />
|
||||
{/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>
|
||||
|
||||
<style>
|
||||
@@ -71,14 +79,6 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.bold {
|
||||
position: absolute;
|
||||
color: #000;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.text-box {
|
||||
color: black;
|
||||
padding: 0;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { currentManga, currentVolume } from '$lib/catalog';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import { Panzoom, zoomOriginal } from '$lib/panzoom';
|
||||
import { currentVolume } from '$lib/catalog';
|
||||
import { Panzoom } from '$lib/panzoom';
|
||||
import MangaPage from './MangaPage.svelte';
|
||||
|
||||
const volume = $currentVolume;
|
||||
@@ -19,22 +18,8 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if volume}
|
||||
<div>
|
||||
<Button on:click={zoomOriginal}>Reset Zoom</Button>
|
||||
<Button on:click={left}>{'<'}</Button>
|
||||
<Button on:click={right}>{'>'}</Button>
|
||||
</div>
|
||||
{#if volume && pages}
|
||||
<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>
|
||||
{/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">
|
||||
import { snackbarStore } from '$lib/util/snackbar';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
import { Toast } from 'flowbite-svelte';
|
||||
</script>
|
||||
|
||||
{#if $snackbarStore?.message && $snackbarStore?.visible}
|
||||
<div in:fly={{ y: 200, duration: 500 }} out:fade={{ duration: 500 }}>
|
||||
{$snackbarStore?.message}
|
||||
</div>
|
||||
<Toast position="bottom-right">{$snackbarStore?.message}</Toast>
|
||||
{/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[]) {
|
||||
// for (const { mokuroData, volumeName, archiveFile, files } of volumes) {
|
||||
// const { title_uuid } = mokuroData
|
||||
// }
|
||||
export async function scanFiles(item: FileSystemEntry, files: Promise<File | undefined>[]) {
|
||||
if (item.isDirectory) {
|
||||
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(fileList: FileList) {
|
||||
const files = [...fileList]
|
||||
export async function processFiles(files: File[]) {
|
||||
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
|
||||
|
||||
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],
|
||||
@@ -64,7 +85,10 @@ export async function processFiles(fileList: FileList) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type === 'image/jpeg' || type === 'image/png') {
|
||||
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)
|
||||
@@ -115,18 +139,20 @@ export async function processFiles(fileList: FileList) {
|
||||
if (!valid.includes(false)) {
|
||||
await requestPersistentStorage();
|
||||
|
||||
const key = vols[0].mokuroData.title_uuid;
|
||||
for (const key of mangas) {
|
||||
const existingCatalog = await db.catalog.get(key);
|
||||
|
||||
if (existingCatalog) {
|
||||
const filtered = vols.filter((vol) => {
|
||||
return !existingCatalog.manga.some(manga => {
|
||||
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] })
|
||||
} else {
|
||||
await db.catalog.put({ id: key, manga: vols })
|
||||
await db.catalog.put({ id: key, manga: filtered })
|
||||
}
|
||||
}
|
||||
|
||||
showSnackbar('Catalog updated successfully')
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
export async function requestPersistentStorage() {
|
||||
if (navigator.storage && navigator.storage.persist) {
|
||||
const isPersisted = await navigator.storage.persist();
|
||||
console.log(`Persisted storage granted: ${isPersisted}`);
|
||||
await navigator.storage.persist();
|
||||
}
|
||||
}
|
||||
|
||||
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">
|
||||
import '../app.postcss';
|
||||
import NavBar from '$lib/components/NavBar.svelte';
|
||||
import Snackbar from '$lib/components/Snackbar.svelte';
|
||||
import '../app.scss';
|
||||
|
||||
@@ -3,17 +3,41 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from '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(() => {
|
||||
if (!manga) {
|
||||
goto('/');
|
||||
}
|
||||
});
|
||||
|
||||
function onDelete() {
|
||||
popupModal = true;
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
const title = manga?.[0].mokuroData.title_uuid;
|
||||
await db.catalog.delete(title);
|
||||
goto('/');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="float-right"><Button outline color="red" on:click={onDelete}>Delete manga</Button></div>
|
||||
<div class="volumes">
|
||||
{#if manga}
|
||||
{#each manga as volume}
|
||||
<VolumeItem {volume} />
|
||||
@@ -21,8 +45,19 @@
|
||||
{/if}
|
||||
</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>
|
||||
div {
|
||||
.volumes {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
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 />
|
||||
|
||||
<style>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
let promise: Promise<void>;
|
||||
|
||||
async function onUpload(files: FileList) {
|
||||
promise = processFiles(files);
|
||||
promise = processFiles([...files]);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
import preprocess from 'svelte-preprocess';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
preprocess: preprocess({
|
||||
preprocess: [
|
||||
preprocess({
|
||||
scss: {
|
||||
prependData: `@use './src/variables' as *;`
|
||||
}
|
||||
}),
|
||||
vitePreprocess({})
|
||||
],
|
||||
|
||||
kit: {
|
||||
// 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