second day
This commit is contained in:
18
view/src/routes/editor.svelte
Normal file
18
view/src/routes/editor.svelte
Normal file
@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { bufToImageUrl } from "helpers";
|
||||
import { route as currentRoute, images as imageStore } from "stores";
|
||||
import Editor from "components/Editor/Editor.svelte";
|
||||
|
||||
const imageId = parseInt($currentRoute.split("/")[1]);
|
||||
|
||||
const imagePromise = imageStore.get(imageId);
|
||||
</script>
|
||||
|
||||
{#await imagePromise}
|
||||
Loading image
|
||||
{:then image}
|
||||
<Editor {image} />
|
||||
{:catch err}
|
||||
<p>Error loading image</p>
|
||||
{JSON.stringify(err)}
|
||||
{/await}
|
2
view/src/routes/error.svelte
Normal file
2
view/src/routes/error.svelte
Normal file
@ -0,0 +1,2 @@
|
||||
<script>
|
||||
</script>
|
@ -1,24 +1,133 @@
|
||||
<script lang="ts">
|
||||
import { store as imageStore } from "../stores/images";
|
||||
import { Cross } from "../icons";
|
||||
import { fly, fade } from "svelte/transition";
|
||||
import { images as imageData, route } from "stores";
|
||||
import { bufToImageUrl } from "helpers";
|
||||
import type { Writable } from "svelte/store";
|
||||
|
||||
let images = [];
|
||||
const urlCreator = window.URL || window.webkitURL;
|
||||
import Toast from "../components/Toast";
|
||||
import Analyzer from "../components/Analyzer.svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
$: if ($imageStore.length) {
|
||||
images = $imageStore.map((img) => {
|
||||
const blob = new Blob([img.data], { type: img.type });
|
||||
const imageUrl = urlCreator.createObjectURL(blob);
|
||||
return {
|
||||
...img,
|
||||
imageUrl,
|
||||
};
|
||||
});
|
||||
const imageStore: Writable<Image[]> = imageData.store;
|
||||
|
||||
let showAnalyzerIndex;
|
||||
|
||||
function handleFileChange(ev, img: Image) {
|
||||
if (ev.target.innerText.includes("\n")) {
|
||||
const value = ev.target.innerText.replace("\n", "");
|
||||
ev.target.innerText = value;
|
||||
ev.target.blur();
|
||||
img.name = value;
|
||||
imageData.updateImage(img);
|
||||
}
|
||||
}
|
||||
|
||||
let startCheckingEmpty = false;
|
||||
setTimeout(() => {
|
||||
startCheckingEmpty = true;
|
||||
}, 1000);
|
||||
|
||||
onMount(() =>
|
||||
imageStore.subscribe((img) => {
|
||||
if (img.length === 0 && startCheckingEmpty) {
|
||||
route.set("main");
|
||||
}
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
{#each images as img}
|
||||
<p>{img.filename}</p>
|
||||
<img src={img.imageUrl} />
|
||||
{/each}
|
||||
<main>
|
||||
<h3>List</h3>
|
||||
|
||||
<h3>List</h3>
|
||||
{#each $imageStore as img, i}
|
||||
<div class="list-item" in:fly={{ duration: 500, x: -50, delay: i * 200 }}>
|
||||
<img
|
||||
alt={img.filename}
|
||||
on:click={() => route.set("editor/" + img.id)}
|
||||
src={bufToImageUrl(img.data, img.type)}
|
||||
/>
|
||||
<div class="content">
|
||||
<h4 contenteditable on:input={(ev) => handleFileChange(ev, img)}>
|
||||
{img.name}
|
||||
</h4>
|
||||
|
||||
<button
|
||||
on:click={() => {
|
||||
if (img.overlayData && img.overlayData.byteLength) {
|
||||
if (showAnalyzerIndex === i) {
|
||||
showAnalyzerIndex = undefined;
|
||||
} else {
|
||||
showAnalyzerIndex = i;
|
||||
}
|
||||
} else {
|
||||
showAnalyzerIndex = undefined;
|
||||
Toast.warn("Image has no data; Paint some regions to analyze");
|
||||
}
|
||||
}}>analyze</button
|
||||
>
|
||||
<button on:click={() => route.set("editor/" + img.id)}>edit</button>
|
||||
<button
|
||||
on:click={() => {
|
||||
imageData.deleteImage(img);
|
||||
}}>delete</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if i === showAnalyzerIndex}
|
||||
<div transition:fly={{ x: -50 }}>
|
||||
<Analyzer {img} />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<div id="cross-wrapper" on:click={() => route.set("main")}>
|
||||
<Cross />
|
||||
<p>Add more images</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
#cross-wrapper {
|
||||
margin-top: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
#cross-wrapper > p {
|
||||
margin-left: 10px;
|
||||
}
|
||||
main {
|
||||
height: 100%;
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
main {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
.list-item {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 80px auto;
|
||||
column-gap: 10px;
|
||||
|
||||
margin-top: 10px;
|
||||
outline: solid thin black;
|
||||
}
|
||||
|
||||
.list-item > img {
|
||||
width: 80px;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
cursor: pointer;
|
||||
background-color: black;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.list-item > img:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
|
@ -3,10 +3,13 @@
|
||||
import AnimatedNumber from "../components/AnimatedNumber.svelte";
|
||||
import Cross from "../icons/Cross.svelte";
|
||||
import Toast from "../components/Toast";
|
||||
import { route as currentRoute, images } from "../stores";
|
||||
import { route as currentRoute, images, route } from "../stores";
|
||||
import { fileToImage } from "../helpers";
|
||||
let acceptedFiles: File[] = [];
|
||||
let hovering = false;
|
||||
|
||||
const imageStore = images.store;
|
||||
|
||||
async function handleFilesSelect(e) {
|
||||
if (e.detail.acceptedFiles) {
|
||||
const addedFiles: File[] = e.detail.acceptedFiles;
|
||||
@ -14,13 +17,15 @@
|
||||
const newFiles: File[] = [];
|
||||
const dupes: File[] = [];
|
||||
|
||||
const allImages = [...$imageStore, ...acceptedFiles];
|
||||
|
||||
addedFiles.forEach((f) => {
|
||||
const isNew = !acceptedFiles.find((_f) => {
|
||||
const isNew = !allImages.find((_f: Image) => {
|
||||
return (
|
||||
_f.lastModified === f.lastModified &&
|
||||
_f.name === f.name &&
|
||||
_f.type === f.type &&
|
||||
_f.size === f.size
|
||||
_f.data.byteLength === f.size
|
||||
);
|
||||
});
|
||||
|
||||
@ -35,9 +40,9 @@
|
||||
|
||||
if (dupes.length) {
|
||||
const loadDupes = await Toast.confirm(
|
||||
`Add <b> ${dupes.length}</b> duplicate file${
|
||||
dupes.length > 1 ? "s" : ""
|
||||
}?`
|
||||
`Some images are already loaded. Add <b> ${
|
||||
dupes.length
|
||||
}</b> duplicate${dupes.length > 1 ? "s" : ""}?`
|
||||
);
|
||||
|
||||
if (loadDupes) {
|
||||
@ -53,17 +58,7 @@
|
||||
e.stopImmediatePropagation();
|
||||
e.stopPropagation();
|
||||
|
||||
const img = await Promise.all(
|
||||
acceptedFiles.map(async (f) => {
|
||||
return {
|
||||
name: f.name,
|
||||
filename: f.name,
|
||||
type: f.type,
|
||||
lastModified: f.lastModified,
|
||||
data: await f.arrayBuffer(),
|
||||
};
|
||||
})
|
||||
);
|
||||
const img = await Promise.all(acceptedFiles.map(fileToImage));
|
||||
|
||||
images.add(img);
|
||||
|
||||
@ -78,7 +73,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<div id="wrapper">
|
||||
<DropZone
|
||||
accept="image/*"
|
||||
on:drop={handleFilesSelect}
|
||||
@ -86,6 +81,12 @@
|
||||
on:dragleave={() => (hovering = false)}
|
||||
on:dragenter={() => (hovering = false)}
|
||||
>
|
||||
{#if $imageStore.length}
|
||||
<button on:click|preventDefault|stopPropagation={() => route.set("list")}
|
||||
>Exit</button
|
||||
>
|
||||
{/if}
|
||||
|
||||
<div class="inner-wrapper">
|
||||
<div class="icon-wrapper" class:hovering>
|
||||
<Cross />
|
||||
@ -103,7 +104,8 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
#wrapper {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
height: 100%;
|
||||
@ -114,6 +116,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
outline: solid thin black;
|
||||
}
|
||||
|
||||
p {
|
||||
|
Reference in New Issue
Block a user