Merge pull request #1 from ZXY101/flowbite

Add Flowbite
This commit is contained in:
Shaun Tenner
2023-09-13 20:47:44 +02:00
committed by GitHub
25 changed files with 1794 additions and 277 deletions

1321
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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;

View File

@@ -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
View File

@@ -0,0 +1,4 @@
/* Write your global styles here, in PostCSS syntax */
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -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;

View File

@@ -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

View File

@@ -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>

View File

@@ -3,17 +3,20 @@
import CatalogItem from './CatalogItem.svelte'; import CatalogItem from './CatalogItem.svelte';
</script> </script>
{#if $catalog && $catalog.length > 0} {#if $catalog}
{#if $catalog.length > 0}
<div class="container"> <div class="container">
{#each $catalog as { manga }} {#each $catalog as { manga }}
<CatalogItem {manga} /> <CatalogItem {manga} />
{/each} {/each}
</div> </div>
{:else} {:else}
<div class="empty-state"> <div class="empty-state">
<p>Your catalog is currently empty.</p> <p>Your catalog is currently empty.</p>
<a href="upload">Add manga</a>
</div> </div>
{/if}
{:else}
<p>Loading...</p>
{/if} {/if}
<style lang="scss"> <style lang="scss">

View File

@@ -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>

View File

@@ -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]':
title = $currentManga?.[0].mokuroData.title;
back = '/';
break;
case '/[manga]/[volume]':
window.document.body.classList.add('reader'); window.document.body.classList.add('reader');
title = $currentVolume?.volumeName; } else {
back = '/manga'; window.document.body.classList.remove('reader');
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>
</nav> </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>
<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>

View File

@@ -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;

View File

@@ -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>

View 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>

View File

@@ -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>

View 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
View 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))
}
})

View File

@@ -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
}) })
})
if (existingCatalog) {
await db.catalog.update(key, { manga: [...existingCatalog.manga, ...filtered] }) await db.catalog.update(key, { manga: [...existingCatalog.manga, ...filtered] })
} else { } else {
await db.catalog.put({ id: key, manga: vols }) await db.catalog.put({ id: key, manga: filtered })
}
} }
showSnackbar('Catalog updated successfully') showSnackbar('Catalog updated successfully')

View File

@@ -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]}`;
}

View File

@@ -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';

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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: [
preprocess({
scss: { scss: {
prependData: `@use './src/variables' as *;` 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
View 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;