init
This commit is contained in:
commit
7fc8feb0cc
11
Makefile
Executable file
11
Makefile
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
MAKEFLAGS += -j2
|
||||||
|
|
||||||
|
export PORT = 8080
|
||||||
|
|
||||||
|
dev: dev-server dev-view
|
||||||
|
|
||||||
|
dev-server:
|
||||||
|
cd server && gin run main.go
|
||||||
|
dev-view:
|
||||||
|
cd view && npm run dev
|
||||||
|
|
1
server/.gitignore
vendored
Normal file
1
server/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
gin-bin
|
5
server/go.mod
Executable file
5
server/go.mod
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
module git.jim-fx.com/max/karl-server
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require github.com/gin-gonic/gin v1.6.3
|
42
server/go.sum
Executable file
42
server/go.sum
Executable file
@ -0,0 +1,42 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||||
|
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||||
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
|
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||||
|
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||||
|
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
|
||||||
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||||
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||||
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||||
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
|
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||||
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
33
server/main.go
Executable file
33
server/main.go
Executable file
@ -0,0 +1,33 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
var db = make(map[string]string)
|
||||||
|
|
||||||
|
func setupRouter() *gin.Engine {
|
||||||
|
// Disable Console Color
|
||||||
|
// gin.DisableConsoleColor()
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
r.Static("/", "../view/public")
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := setupRouter()
|
||||||
|
|
||||||
|
port := os.Getenv("PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = "8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("PORT: " + port)
|
||||||
|
|
||||||
|
r.Run(":" + port)
|
||||||
|
}
|
4
view/.gitignore
vendored
Executable file
4
view/.gitignore
vendored
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
/node_modules/
|
||||||
|
/public/build/
|
||||||
|
|
||||||
|
.DS_Store
|
2342
view/package-lock.json
generated
Executable file
2342
view/package-lock.json
generated
Executable file
File diff suppressed because it is too large
Load Diff
32
view/package.json
Executable file
32
view/package.json
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "svelte-app",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "rollup -c",
|
||||||
|
"dev": "rollup -c -w",
|
||||||
|
"start": "sirv public",
|
||||||
|
"validate": "svelte-check"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-commonjs": "^17.0.0",
|
||||||
|
"@rollup/plugin-node-resolve": "^11.0.0",
|
||||||
|
"@rollup/plugin-typescript": "^8.0.0",
|
||||||
|
"@tsconfig/svelte": "^1.0.10",
|
||||||
|
"idb": "^6.0.0",
|
||||||
|
"rollup": "^2.3.4",
|
||||||
|
"rollup-plugin-css-only": "^3.1.0",
|
||||||
|
"rollup-plugin-livereload": "^2.0.0",
|
||||||
|
"rollup-plugin-svelte": "^7.0.0",
|
||||||
|
"rollup-plugin-terser": "^7.0.0",
|
||||||
|
"svelte": "^3.0.0",
|
||||||
|
"svelte-check": "^1.0.0",
|
||||||
|
"svelte-preprocess": "^4.0.0",
|
||||||
|
"tslib": "^2.0.0",
|
||||||
|
"typescript": "^4.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"sirv-cli": "^1.0.0",
|
||||||
|
"svelte-file-dropzone": "^0.0.15"
|
||||||
|
}
|
||||||
|
}
|
BIN
view/public/favicon.png
Executable file
BIN
view/public/favicon.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
62
view/public/global.css
Executable file
62
view/public/global.css
Executable file
@ -0,0 +1,62 @@
|
|||||||
|
html, body {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgb(0,100,200);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:visited {
|
||||||
|
color: rgb(0,80,160);
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, button, select, textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
-webkit-padding: 0.4em 0;
|
||||||
|
padding: 0.4em;
|
||||||
|
margin: 0 0 0.5em 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:disabled {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:not(:disabled):active {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus {
|
||||||
|
border-color: #666;
|
||||||
|
}
|
23
view/public/index.html
Executable file
23
view/public/index.html
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8'>
|
||||||
|
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||||
|
|
||||||
|
<title>Karls Analyzer</title>
|
||||||
|
|
||||||
|
<link rel='icon' type='image/png' href='/favicon.png'>
|
||||||
|
<link rel='stylesheet' href='/global.css'>
|
||||||
|
<link rel='stylesheet' href='/build/bundle.css'>
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;900&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<script defer src='/build/bundle.js'></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
56
view/rollup.config.js
Executable file
56
view/rollup.config.js
Executable file
@ -0,0 +1,56 @@
|
|||||||
|
import svelte from 'rollup-plugin-svelte';
|
||||||
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
|
import resolve from '@rollup/plugin-node-resolve';
|
||||||
|
import { terser } from 'rollup-plugin-terser';
|
||||||
|
import sveltePreprocess from 'svelte-preprocess';
|
||||||
|
import typescript from '@rollup/plugin-typescript';
|
||||||
|
import css from 'rollup-plugin-css-only';
|
||||||
|
|
||||||
|
const production = !process.env.ROLLUP_WATCH;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: 'src/main.ts',
|
||||||
|
output: {
|
||||||
|
sourcemap: true,
|
||||||
|
format: 'iife',
|
||||||
|
name: 'app',
|
||||||
|
file: 'public/build/bundle.js'
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
svelte({
|
||||||
|
preprocess: sveltePreprocess({ sourceMap: !production }),
|
||||||
|
compilerOptions: {
|
||||||
|
// enable run-time checks when not in production
|
||||||
|
dev: !production
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
// we'll extract any component CSS out into
|
||||||
|
// a separate file - better for performance
|
||||||
|
css({ output: 'bundle.css' }),
|
||||||
|
|
||||||
|
// If you have external dependencies installed from
|
||||||
|
// npm, you'll most likely need these plugins. In
|
||||||
|
// some cases you'll need additional configuration -
|
||||||
|
// consult the documentation for details:
|
||||||
|
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
||||||
|
resolve({
|
||||||
|
browser: true,
|
||||||
|
dedupe: ['svelte']
|
||||||
|
}),
|
||||||
|
commonjs(),
|
||||||
|
typescript({
|
||||||
|
sourceMap: !production,
|
||||||
|
inlineSources: !production
|
||||||
|
}),
|
||||||
|
|
||||||
|
// If we're building for production (npm run build
|
||||||
|
// instead of npm run dev), minify
|
||||||
|
production && terser()
|
||||||
|
],
|
||||||
|
watch: {
|
||||||
|
clearScreen: false,
|
||||||
|
chokidar: {
|
||||||
|
followSymlinks: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
25
view/src/App.svelte
Executable file
25
view/src/App.svelte
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as routes from "./routes";
|
||||||
|
import ToastWrapper from "./components/Toast/ToastWrapper.svelte";
|
||||||
|
import { route as currentRoute } from "./stores";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
{#if $currentRoute in routes}
|
||||||
|
<svelte:component this={routes[$currentRoute]} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<ToastWrapper />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
main {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
main {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
80
view/src/components/AnimatedNumber.svelte
Normal file
80
view/src/components/AnimatedNumber.svelte
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<script>
|
||||||
|
export let num;
|
||||||
|
let prevNumber = num;
|
||||||
|
let dirUp;
|
||||||
|
|
||||||
|
let isAnimating = false;
|
||||||
|
let startAnimate = false;
|
||||||
|
|
||||||
|
const duration = 500;
|
||||||
|
|
||||||
|
$: if (num !== undefined) {
|
||||||
|
if (num !== prevNumber) {
|
||||||
|
isAnimating = true;
|
||||||
|
dirUp = prevNumber < num;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
startAnimate = true;
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
isAnimating = false;
|
||||||
|
startAnimate = false;
|
||||||
|
prevNumber = num;
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper" class:isAnimating>
|
||||||
|
<span>
|
||||||
|
{num}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{#if isAnimating}
|
||||||
|
<span
|
||||||
|
style={`transition: transform ${duration}ms ease, opacity ${duration}ms ease;`}
|
||||||
|
class:down={startAnimate && dirUp}
|
||||||
|
class:up={startAnimate && !dirUp}
|
||||||
|
>
|
||||||
|
{prevNumber}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
style={`transition: transform ${duration}ms ease, opacity ${duration}ms ease;`}
|
||||||
|
class:down={!startAnimate && !dirUp}
|
||||||
|
class:up={!startAnimate && dirUp}
|
||||||
|
>
|
||||||
|
{num}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper.isAnimating > span:nth-child(1) {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.up {
|
||||||
|
transform: translateY(-10px) !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.down {
|
||||||
|
transform: translateY(10px) !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper > span:nth-child(2),
|
||||||
|
.wrapper > span:nth-child(3) {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 1;
|
||||||
|
left: 0px;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
</style>
|
350
view/src/components/DropZone/index.svelte
Executable file
350
view/src/components/DropZone/index.svelte
Executable file
@ -0,0 +1,350 @@
|
|||||||
|
<script>
|
||||||
|
import { fromEvent } from "file-selector";
|
||||||
|
import {
|
||||||
|
allFilesAccepted,
|
||||||
|
composeEventHandlers,
|
||||||
|
fileAccepted,
|
||||||
|
fileMatchSize,
|
||||||
|
isEvtWithFiles,
|
||||||
|
isIeOrEdge,
|
||||||
|
isPropagationStopped,
|
||||||
|
onDocumentDragOver,
|
||||||
|
TOO_MANY_FILES_REJECTION,
|
||||||
|
} from "./utils";
|
||||||
|
import { onMount, onDestroy, createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
//props
|
||||||
|
/**
|
||||||
|
* Set accepted file types.
|
||||||
|
* See https://github.com/okonet/attr-accept for more information.
|
||||||
|
*/
|
||||||
|
export let accept; // string or string[]
|
||||||
|
export let disabled = false;
|
||||||
|
export let getFilesFromEvent = fromEvent;
|
||||||
|
export let maxSize = Infinity;
|
||||||
|
export let minSize = 0;
|
||||||
|
export let multiple = true;
|
||||||
|
export let preventDropOnDocument = true;
|
||||||
|
export let noClick = false;
|
||||||
|
export let noKeyboard = false;
|
||||||
|
export let noDrag = false;
|
||||||
|
export let noDragEventsBubbling = false;
|
||||||
|
export let containerClasses = "";
|
||||||
|
export let containerStyles = "";
|
||||||
|
export let disableDefaultStyles = false;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
//state
|
||||||
|
|
||||||
|
let state = {
|
||||||
|
isFocused: false,
|
||||||
|
isFileDialogActive: false,
|
||||||
|
isDragActive: false,
|
||||||
|
isDragAccept: false,
|
||||||
|
isDragReject: false,
|
||||||
|
draggedFiles: [],
|
||||||
|
acceptedFiles: [],
|
||||||
|
fileRejections: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
let rootRef;
|
||||||
|
let inputRef;
|
||||||
|
|
||||||
|
function resetState() {
|
||||||
|
state.isFileDialogActive = false;
|
||||||
|
state.isDragActive = false;
|
||||||
|
state.draggedFiles = [];
|
||||||
|
state.acceptedFiles = [];
|
||||||
|
state.fileRejections = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fn for opening the file dialog programmatically
|
||||||
|
function openFileDialog() {
|
||||||
|
if (inputRef) {
|
||||||
|
inputRef.value = null; // TODO check if null needs to be set
|
||||||
|
state.isFileDialogActive = true;
|
||||||
|
inputRef.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cb to open the file dialog when SPACE/ENTER occurs on the dropzone
|
||||||
|
function onKeyDownCb(event) {
|
||||||
|
// Ignore keyboard events bubbling up the DOM tree
|
||||||
|
if (!rootRef || !rootRef.isEqualNode(event.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.keyCode === 32 || event.keyCode === 13) {
|
||||||
|
event.preventDefault();
|
||||||
|
openFileDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update focus state for the dropzone
|
||||||
|
function onFocusCb() {
|
||||||
|
state.isFocused = true;
|
||||||
|
}
|
||||||
|
function onBlurCb() {
|
||||||
|
state.isFocused = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cb to open the file dialog when click occurs on the dropzone
|
||||||
|
function onClickCb() {
|
||||||
|
if (noClick) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In IE11/Edge the file-browser dialog is blocking, therefore, use setTimeout()
|
||||||
|
// to ensure React can handle state changes
|
||||||
|
// See: https://github.com/react-dropzone/react-dropzone/issues/450
|
||||||
|
if (isIeOrEdge()) {
|
||||||
|
setTimeout(openFileDialog, 0);
|
||||||
|
} else {
|
||||||
|
openFileDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragEnterCb(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
stopPropagation(event);
|
||||||
|
|
||||||
|
dragTargetsRef = [...dragTargetsRef, event.target];
|
||||||
|
|
||||||
|
if (isEvtWithFiles(event)) {
|
||||||
|
Promise.resolve(getFilesFromEvent(event)).then((draggedFiles) => {
|
||||||
|
if (isPropagationStopped(event) && !noDragEventsBubbling) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.draggedFiles = draggedFiles;
|
||||||
|
state.isDragActive = true;
|
||||||
|
|
||||||
|
dispatch("dragenter", {
|
||||||
|
dragEvent: event,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragOverCb(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
stopPropagation(event);
|
||||||
|
|
||||||
|
if (event.dataTransfer) {
|
||||||
|
try {
|
||||||
|
event.dataTransfer.dropEffect = "copy";
|
||||||
|
} catch {} /* eslint-disable-line no-empty */
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEvtWithFiles(event)) {
|
||||||
|
dispatch("dragover", {
|
||||||
|
dragEvent: event,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragLeaveCb(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
stopPropagation(event);
|
||||||
|
|
||||||
|
// Only deactivate once the dropzone and all children have been left
|
||||||
|
const targets = dragTargetsRef.filter(
|
||||||
|
(target) => rootRef && rootRef.contains(target)
|
||||||
|
);
|
||||||
|
// Make sure to remove a target present multiple times only once
|
||||||
|
// (Firefox may fire dragenter/dragleave multiple times on the same element)
|
||||||
|
const targetIdx = targets.indexOf(event.target);
|
||||||
|
if (targetIdx !== -1) {
|
||||||
|
targets.splice(targetIdx, 1);
|
||||||
|
}
|
||||||
|
dragTargetsRef = targets;
|
||||||
|
if (targets.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.isDragActive = false;
|
||||||
|
state.draggedFiles = [];
|
||||||
|
|
||||||
|
if (isEvtWithFiles(event)) {
|
||||||
|
dispatch("dragleave", {
|
||||||
|
dragEvent: event,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDropCb(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
stopPropagation(event);
|
||||||
|
|
||||||
|
dragTargetsRef = [];
|
||||||
|
|
||||||
|
if (isEvtWithFiles(event)) {
|
||||||
|
Promise.resolve(getFilesFromEvent(event)).then((files) => {
|
||||||
|
if (isPropagationStopped(event) && !noDragEventsBubbling) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const acceptedFiles = [];
|
||||||
|
const fileRejections = [];
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
const [accepted, acceptError] = fileAccepted(file, accept);
|
||||||
|
const [sizeMatch, sizeError] = fileMatchSize(file, minSize, maxSize);
|
||||||
|
if (accepted && sizeMatch) {
|
||||||
|
acceptedFiles.push(file);
|
||||||
|
} else {
|
||||||
|
const errors = [acceptError, sizeError].filter((e) => e);
|
||||||
|
fileRejections.push({ file, errors });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!multiple && acceptedFiles.length > 1) {
|
||||||
|
// Reject everything and empty accepted files
|
||||||
|
acceptedFiles.forEach((file) => {
|
||||||
|
fileRejections.push({ file, errors: [TOO_MANY_FILES_REJECTION] });
|
||||||
|
});
|
||||||
|
acceptedFiles.splice(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.acceptedFiles = acceptedFiles;
|
||||||
|
state.fileRejections = fileRejections;
|
||||||
|
|
||||||
|
dispatch("drop", {
|
||||||
|
acceptedFiles,
|
||||||
|
fileRejections,
|
||||||
|
event,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fileRejections.length > 0) {
|
||||||
|
dispatch("droprejected", {
|
||||||
|
fileRejections,
|
||||||
|
event,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (acceptedFiles.length > 0) {
|
||||||
|
dispatch("dropaccepted", {
|
||||||
|
acceptedFiles,
|
||||||
|
event,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
resetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function composeHandler(fn) {
|
||||||
|
return disabled ? null : fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
function composeKeyboardHandler(fn) {
|
||||||
|
return noKeyboard ? null : composeHandler(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
function composeDragHandler(fn) {
|
||||||
|
return noDrag ? null : composeHandler(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPropagation(event) {
|
||||||
|
if (noDragEventsBubbling) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let dragTargetsRef = [];
|
||||||
|
function onDocumentDrop(event) {
|
||||||
|
if (rootRef && rootRef.contains(event.target)) {
|
||||||
|
// If we intercepted an event for our instance, let it propagate down to the instance's onDrop handler
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
dragTargetsRef = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update file dialog active state when the window is focused on
|
||||||
|
function onWindowFocus() {
|
||||||
|
// Execute the timeout only if the file dialog is opened in the browser
|
||||||
|
if (state.isFileDialogActive) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (inputRef) {
|
||||||
|
const { files } = inputRef;
|
||||||
|
|
||||||
|
if (!files.length) {
|
||||||
|
state.isFileDialogActive = false;
|
||||||
|
dispatch("filedialogcancel");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
window.addEventListener("focus", onWindowFocus, false);
|
||||||
|
if (preventDropOnDocument) {
|
||||||
|
document.addEventListener("dragover", onDocumentDragOver, false);
|
||||||
|
document.addEventListener("drop", onDocumentDrop, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
window.removeEventListener("focus", onWindowFocus, false);
|
||||||
|
if (preventDropOnDocument) {
|
||||||
|
document.removeEventListener("dragover", onDocumentDragOver);
|
||||||
|
document.removeEventListener("drop", onDocumentDrop);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function onInputElementClick(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={rootRef}
|
||||||
|
tabindex="0"
|
||||||
|
class="{disableDefaultStyles ? '' : 'dropzone'}
|
||||||
|
{containerClasses}"
|
||||||
|
style={containerStyles}
|
||||||
|
on:keydown={composeKeyboardHandler(onKeyDownCb)}
|
||||||
|
on:focus={composeKeyboardHandler(onFocusCb)}
|
||||||
|
on:blur={composeKeyboardHandler(onBlurCb)}
|
||||||
|
on:click={composeHandler(onClickCb)}
|
||||||
|
on:dragenter={composeDragHandler(onDragEnterCb)}
|
||||||
|
on:dragover={composeDragHandler(onDragOverCb)}
|
||||||
|
on:dragleave={composeDragHandler(onDragLeaveCb)}
|
||||||
|
on:drop={composeDragHandler(onDropCb)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
{accept}
|
||||||
|
{multiple}
|
||||||
|
type="file"
|
||||||
|
autocomplete="off"
|
||||||
|
tabindex="-1"
|
||||||
|
on:change={onDropCb}
|
||||||
|
on:click={onInputElementClick}
|
||||||
|
bind:this={inputRef}
|
||||||
|
style="display: none;"
|
||||||
|
/>
|
||||||
|
<slot>
|
||||||
|
<p>Drag 'n' drop some files here, or click to select files</p>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dropzone {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
border-width: 2px;
|
||||||
|
outline: solid thin black;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
.dropzone:focus {
|
||||||
|
border-color: #2196f3;
|
||||||
|
}
|
||||||
|
</style>
|
33
view/src/components/DropZone/utils/attr-accept.ts
Normal file
33
view/src/components/DropZone/utils/attr-accept.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Check if the provided file type should be accepted by the input with accept attribute.
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#attr-accept
|
||||||
|
*
|
||||||
|
* Inspired by https://github.com/enyo/dropzone
|
||||||
|
*
|
||||||
|
* @param file {File} https://developer.mozilla.org/en-US/docs/Web/API/File
|
||||||
|
* @param acceptedFiles {string}
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function(file, acceptedFiles) {
|
||||||
|
if (file && acceptedFiles) {
|
||||||
|
const acceptedFilesArray = Array.isArray(acceptedFiles)
|
||||||
|
? acceptedFiles
|
||||||
|
: acceptedFiles.split(",");
|
||||||
|
const fileName = file.name || "";
|
||||||
|
const mimeType = (file.type || "").toLowerCase();
|
||||||
|
const baseMimeType = mimeType.replace(/\/.*$/, "");
|
||||||
|
|
||||||
|
return acceptedFilesArray.some((type) => {
|
||||||
|
const validType = type.trim().toLowerCase();
|
||||||
|
if (validType.charAt(0) === ".") {
|
||||||
|
return fileName.toLowerCase().endsWith(validType);
|
||||||
|
} else if (validType.endsWith("/*")) {
|
||||||
|
// This is something like a image/* mime type
|
||||||
|
return baseMimeType === validType.replace(/\/.*$/, "");
|
||||||
|
}
|
||||||
|
return mimeType === validType;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
151
view/src/components/DropZone/utils/index.ts
Normal file
151
view/src/components/DropZone/utils/index.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import accepts from "./attr-accept";
|
||||||
|
|
||||||
|
// Error codes
|
||||||
|
export const FILE_INVALID_TYPE = "file-invalid-type";
|
||||||
|
export const FILE_TOO_LARGE = "file-too-large";
|
||||||
|
export const FILE_TOO_SMALL = "file-too-small";
|
||||||
|
export const TOO_MANY_FILES = "too-many-files";
|
||||||
|
|
||||||
|
// File Errors
|
||||||
|
export const getInvalidTypeRejectionErr = (accept) => {
|
||||||
|
accept = Array.isArray(accept) && accept.length === 1 ? accept[0] : accept;
|
||||||
|
const messageSuffix = Array.isArray(accept)
|
||||||
|
? `one of ${accept.join(", ")}`
|
||||||
|
: accept;
|
||||||
|
return {
|
||||||
|
code: FILE_INVALID_TYPE,
|
||||||
|
message: `File type must be ${messageSuffix}`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTooLargeRejectionErr = (maxSize) => {
|
||||||
|
return {
|
||||||
|
code: FILE_TOO_LARGE,
|
||||||
|
message: `File is larger than ${maxSize} bytes`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTooSmallRejectionErr = (minSize) => {
|
||||||
|
return {
|
||||||
|
code: FILE_TOO_SMALL,
|
||||||
|
message: `File is smaller than ${minSize} bytes`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TOO_MANY_FILES_REJECTION = {
|
||||||
|
code: TOO_MANY_FILES,
|
||||||
|
message: "Too many files",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Firefox versions prior to 53 return a bogus MIME type for every file drag, so dragovers with
|
||||||
|
// that MIME type will always be accepted
|
||||||
|
export function fileAccepted(file, accept) {
|
||||||
|
const isAcceptable =
|
||||||
|
file.type === "application/x-moz-file" || accepts(file, accept);
|
||||||
|
return [
|
||||||
|
isAcceptable,
|
||||||
|
isAcceptable ? null : getInvalidTypeRejectionErr(accept),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fileMatchSize(file, minSize, maxSize) {
|
||||||
|
if (isDefined(file.size)) {
|
||||||
|
if (isDefined(minSize) && isDefined(maxSize)) {
|
||||||
|
if (file.size > maxSize) return [false, getTooLargeRejectionErr(maxSize)];
|
||||||
|
if (file.size < minSize) return [false, getTooSmallRejectionErr(minSize)];
|
||||||
|
} else if (isDefined(minSize) && file.size < minSize)
|
||||||
|
return [false, getTooSmallRejectionErr(minSize)];
|
||||||
|
else if (isDefined(maxSize) && file.size > maxSize)
|
||||||
|
return [false, getTooLargeRejectionErr(maxSize)];
|
||||||
|
}
|
||||||
|
return [true, null];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDefined(value) {
|
||||||
|
return value !== undefined && value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function allFilesAccepted({
|
||||||
|
files,
|
||||||
|
accept,
|
||||||
|
minSize,
|
||||||
|
maxSize,
|
||||||
|
multiple,
|
||||||
|
}) {
|
||||||
|
if (!multiple && files.length > 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return files.every((file) => {
|
||||||
|
const [accepted] = fileAccepted(file, accept);
|
||||||
|
const [sizeMatch] = fileMatchSize(file, minSize, maxSize);
|
||||||
|
return accepted && sizeMatch;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// React's synthetic events has event.isPropagationStopped,
|
||||||
|
// but to remain compatibility with other libs (Preact) fall back
|
||||||
|
// to check event.cancelBubble
|
||||||
|
export function isPropagationStopped(event) {
|
||||||
|
if (typeof event.isPropagationStopped === "function") {
|
||||||
|
return event.isPropagationStopped();
|
||||||
|
} else if (typeof event.cancelBubble !== "undefined") {
|
||||||
|
return event.cancelBubble;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEvtWithFiles(event) {
|
||||||
|
if (!event.dataTransfer) {
|
||||||
|
return !!event.target && !!event.target.files;
|
||||||
|
}
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/types
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#file
|
||||||
|
return Array.prototype.some.call(
|
||||||
|
event.dataTransfer.types,
|
||||||
|
(type) => type === "Files" || type === "application/x-moz-file"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isKindFile(item) {
|
||||||
|
return typeof item === "object" && item !== null && item.kind === "file";
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow the entire document to be a drag target
|
||||||
|
export function onDocumentDragOver(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIe(userAgent) {
|
||||||
|
return (
|
||||||
|
userAgent.indexOf("MSIE") !== -1 || userAgent.indexOf("Trident/") !== -1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEdge(userAgent) {
|
||||||
|
return userAgent.indexOf("Edge/") !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isIeOrEdge(userAgent = window.navigator.userAgent) {
|
||||||
|
return isIe(userAgent) || isEdge(userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is intended to be used to compose event handlers
|
||||||
|
* They are executed in order until one of them calls `event.isPropagationStopped()`.
|
||||||
|
* Note that the check is done on the first invoke too,
|
||||||
|
* meaning that if propagation was stopped before invoking the fns,
|
||||||
|
* no handlers will be executed.
|
||||||
|
*
|
||||||
|
* @param {Function} fns the event hanlder functions
|
||||||
|
* @return {Function} the event handler to add to an element
|
||||||
|
*/
|
||||||
|
export function composeEventHandlers(...fns) {
|
||||||
|
return (event, ...args) =>
|
||||||
|
fns.some((fn) => {
|
||||||
|
if (!isPropagationStopped(event) && fn) {
|
||||||
|
fn(event, ...args);
|
||||||
|
}
|
||||||
|
return isPropagationStopped(event);
|
||||||
|
});
|
||||||
|
}
|
8
view/src/components/Toast/Toast.d.ts
vendored
Normal file
8
view/src/components/Toast/Toast.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
interface Toast {
|
||||||
|
type: string;
|
||||||
|
msg: string;
|
||||||
|
time: number;
|
||||||
|
|
||||||
|
res?: () => void;
|
||||||
|
rej?: () => void;
|
||||||
|
}
|
65
view/src/components/Toast/Toast.svelte
Normal file
65
view/src/components/Toast/Toast.svelte
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { fly } from "svelte/transition";
|
||||||
|
export let msg;
|
||||||
|
export let time;
|
||||||
|
export let type;
|
||||||
|
export let res;
|
||||||
|
export let rej;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div transition:fly={{ duration: 500, x: -50 }} class={`wrapper type-${type}`}>
|
||||||
|
<p>{@html msg}</p>
|
||||||
|
|
||||||
|
{#if type === "confirm"}
|
||||||
|
<div class="button-wrapper">
|
||||||
|
<button on:click={res}>okay</button>
|
||||||
|
<button on:click={rej}>nope</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
outline: solid thin black;
|
||||||
|
padding-top: 2.5px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper.type-warn {
|
||||||
|
background-color: rgb(255, 255, 132);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper.type-warn > p::before,
|
||||||
|
.wrapper.type-warn > p::after {
|
||||||
|
content: " ! ";
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-wrapper > button {
|
||||||
|
border-radius: 0px;
|
||||||
|
background: none;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0px;
|
||||||
|
border: none;
|
||||||
|
color: black;
|
||||||
|
outline: solid thin black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-wrapper > button:first-child {
|
||||||
|
background-color: rgb(131, 255, 131);
|
||||||
|
}
|
||||||
|
.button-wrapper > button:last-child {
|
||||||
|
background-color: rgb(255, 126, 126);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
19
view/src/components/Toast/ToastWrapper.svelte
Normal file
19
view/src/components/Toast/ToastWrapper.svelte
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Toast from "./Toast.svelte";
|
||||||
|
import toasts from "./store";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="toast-wrapper">
|
||||||
|
{#each $toasts as toast}
|
||||||
|
<Toast {...toast} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.toast-wrapper {
|
||||||
|
padding: 1em;
|
||||||
|
position: fixed;
|
||||||
|
left: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
}
|
||||||
|
</style>
|
44
view/src/components/Toast/index.ts
Normal file
44
view/src/components/Toast/index.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import store from "./store";
|
||||||
|
|
||||||
|
function add(type, msg, duration) {
|
||||||
|
|
||||||
|
let prom: Promise<boolean>;
|
||||||
|
|
||||||
|
const toast: Toast = {
|
||||||
|
type,
|
||||||
|
msg,
|
||||||
|
time: duration
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "confirm") {
|
||||||
|
prom = new Promise((res, rej) => {
|
||||||
|
toast.res = () => {
|
||||||
|
store.update(toasts => toasts.filter(t => t !== toast));
|
||||||
|
res(true);
|
||||||
|
};
|
||||||
|
toast.rej = () => {
|
||||||
|
store.update(toasts => toasts.filter(t => t !== toast));
|
||||||
|
res(false);
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
store.update(toasts => [...toasts, toast])
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
store.update(toasts => toasts.filter(t => t !== toast));
|
||||||
|
if (type === "confirm") {
|
||||||
|
toast.rej();
|
||||||
|
}
|
||||||
|
}, duration);
|
||||||
|
|
||||||
|
return prom;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
info: msg => add("info", msg, 3000),
|
||||||
|
warn: msg => add("warn", msg, 3000),
|
||||||
|
error: msg => add("error", msg, 3000),
|
||||||
|
confirm: msg => add("confirm", msg, 10000),
|
||||||
|
}
|
3
view/src/components/Toast/store.ts
Normal file
3
view/src/components/Toast/store.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
|
export default writable<Toast[]>([]);
|
13
view/src/icons/Cross.svelte
Normal file
13
view/src/icons/Cross.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<svg
|
||||||
|
width="82"
|
||||||
|
height="82"
|
||||||
|
viewBox="0 0 82 82"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M41 0V81.5M81.75 40.75L0.25 40.75"
|
||||||
|
stroke="black"
|
||||||
|
stroke-width="0.75px"
|
||||||
|
/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 214 B |
7
view/src/main.ts
Executable file
7
view/src/main.ts
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
import App from './App.svelte';
|
||||||
|
|
||||||
|
const app = new App({
|
||||||
|
target: document.body
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
2
view/src/routes/index.ts
Executable file
2
view/src/routes/index.ts
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
export { default as main } from "./main.svelte"
|
||||||
|
export { default as list } from "./list.svelte"
|
24
view/src/routes/list.svelte
Normal file
24
view/src/routes/list.svelte
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { store as imageStore } from "../stores/images";
|
||||||
|
|
||||||
|
let images = [];
|
||||||
|
const urlCreator = window.URL || window.webkitURL;
|
||||||
|
|
||||||
|
$: if ($imageStore.length) {
|
||||||
|
images = $imageStore.map((img) => {
|
||||||
|
const blob = new Blob([img.data], { type: img.type });
|
||||||
|
const imageUrl = urlCreator.createObjectURL(blob);
|
||||||
|
return {
|
||||||
|
...img,
|
||||||
|
imageUrl,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each images as img}
|
||||||
|
<p>{img.filename}</p>
|
||||||
|
<img src={img.imageUrl} />
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<h3>List</h3>
|
168
view/src/routes/main.svelte
Executable file
168
view/src/routes/main.svelte
Executable file
@ -0,0 +1,168 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import DropZone from "../components/DropZone/index.svelte";
|
||||||
|
import AnimatedNumber from "../components/AnimatedNumber.svelte";
|
||||||
|
import Cross from "../icons/Cross.svelte";
|
||||||
|
import Toast from "../components/Toast";
|
||||||
|
import { route as currentRoute, images } from "../stores";
|
||||||
|
let acceptedFiles: File[] = [];
|
||||||
|
let hovering = false;
|
||||||
|
|
||||||
|
async function handleFilesSelect(e) {
|
||||||
|
if (e.detail.acceptedFiles) {
|
||||||
|
const addedFiles: File[] = e.detail.acceptedFiles;
|
||||||
|
|
||||||
|
const newFiles: File[] = [];
|
||||||
|
const dupes: File[] = [];
|
||||||
|
|
||||||
|
addedFiles.forEach((f) => {
|
||||||
|
const isNew = !acceptedFiles.find((_f) => {
|
||||||
|
return (
|
||||||
|
_f.lastModified === f.lastModified &&
|
||||||
|
_f.name === f.name &&
|
||||||
|
_f.type === f.type &&
|
||||||
|
_f.size === f.size
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isNew) {
|
||||||
|
newFiles.push(f);
|
||||||
|
} else {
|
||||||
|
dupes.push(f);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
acceptedFiles = [...acceptedFiles, ...newFiles];
|
||||||
|
|
||||||
|
if (dupes.length) {
|
||||||
|
const loadDupes = await Toast.confirm(
|
||||||
|
`Add <b> ${dupes.length}</b> duplicate file${
|
||||||
|
dupes.length > 1 ? "s" : ""
|
||||||
|
}?`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loadDupes) {
|
||||||
|
acceptedFiles = [...acceptedFiles, ...dupes];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hovering = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLoadingImages(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
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(),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
images.add(img);
|
||||||
|
|
||||||
|
await currentRoute.set("list");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClearingImages(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
e.stopPropagation();
|
||||||
|
acceptedFiles = [];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper">
|
||||||
|
<DropZone
|
||||||
|
accept="image/*"
|
||||||
|
on:drop={handleFilesSelect}
|
||||||
|
on:dragenter={() => (hovering = true)}
|
||||||
|
on:dragleave={() => (hovering = false)}
|
||||||
|
on:dragenter={() => (hovering = false)}
|
||||||
|
>
|
||||||
|
<div class="inner-wrapper">
|
||||||
|
<div class="icon-wrapper" class:hovering>
|
||||||
|
<Cross />
|
||||||
|
</div>
|
||||||
|
<p>Drop an image here to get started</p>
|
||||||
|
|
||||||
|
<div class="button-wrapper" class:visible={acceptedFiles.length > 0}>
|
||||||
|
<button on:click={handleLoadingImages}>
|
||||||
|
load <b><AnimatedNumber num={acceptedFiles.length} /></b>
|
||||||
|
</button>
|
||||||
|
<button on:click={handleClearingImages}> clear </button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropZone>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-family: Roboto;
|
||||||
|
margin-left: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-wrapper > button {
|
||||||
|
border-radius: 0px;
|
||||||
|
background: none;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
color: black;
|
||||||
|
outline: solid thin black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-wrapper > button:first-child {
|
||||||
|
background-color: rgb(131, 255, 131);
|
||||||
|
}
|
||||||
|
.button-wrapper > button:last-child {
|
||||||
|
background-color: rgb(255, 126, 126);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -1px;
|
||||||
|
left: 0px;
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
transform: translateY(100%);
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s cubic-bezier(0.57, 0.21, 0.69, 1.25);
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wrapper {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wrapper.hovering {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-wrapper.visible {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
</style>
|
7
view/src/stores/activeImage.ts
Normal file
7
view/src/stores/activeImage.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
|
const r = "route" in localStorage ? localStorage.getItem("route"):"main";
|
||||||
|
|
||||||
|
const store = writable<string>(r);
|
||||||
|
|
||||||
|
export default store;
|
63
view/src/stores/images.ts
Normal file
63
view/src/stores/images.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
import { openDB, DBSchema } from 'idb';
|
||||||
|
|
||||||
|
interface Image {
|
||||||
|
name: string,
|
||||||
|
filename: string
|
||||||
|
lastModified: number
|
||||||
|
type: string
|
||||||
|
data: ArrayBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImageDB extends DBSchema {
|
||||||
|
images: {
|
||||||
|
value: Image;
|
||||||
|
key: string;
|
||||||
|
indexes: { 'filename': string };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const db = openDB<ImageDB>('Images', 1, {
|
||||||
|
upgrade(db) {
|
||||||
|
// Create a store of objects
|
||||||
|
const store = db.createObjectStore('images', {
|
||||||
|
// The 'id' property of the object will be the key.
|
||||||
|
keyPath: 'id',
|
||||||
|
// If it isn't explicitly set, create a value by auto incrementing.
|
||||||
|
autoIncrement: true,
|
||||||
|
});
|
||||||
|
// Create an index on the 'date' property of the objects.
|
||||||
|
store.createIndex('filename', 'filename');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const store = writable<Image[]>([]);
|
||||||
|
|
||||||
|
async function add(img: Image | Image[]) {
|
||||||
|
|
||||||
|
const images = Array.isArray(img) ? [...img] : [img];
|
||||||
|
|
||||||
|
const tx = (await db).transaction('images', 'readwrite');
|
||||||
|
|
||||||
|
await Promise.all(images.map(img => tx.store.add(img)));
|
||||||
|
|
||||||
|
await tx.done;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(name: string): Promise<Image> {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
res({} as Image);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAll(): Promise<Image[]> {
|
||||||
|
return (await db).getAll("images");
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => store.set(await getAll()))()
|
||||||
|
|
||||||
|
export { store, add, get, getAll };
|
3
view/src/stores/index.ts
Normal file
3
view/src/stores/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import * as images from "./images";
|
||||||
|
export { images }
|
||||||
|
export { default as route } from "./route";
|
18
view/src/stores/route.ts
Normal file
18
view/src/stores/route.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
|
let r = "main";
|
||||||
|
|
||||||
|
if (window.location.hash.length) {
|
||||||
|
r = window.location.hash.replace("#", "");
|
||||||
|
} else {
|
||||||
|
r = "main";
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = writable<string>(r);
|
||||||
|
|
||||||
|
store.subscribe(s => {
|
||||||
|
if (s === "main") s = ""
|
||||||
|
window.location.hash = s;
|
||||||
|
})
|
||||||
|
|
||||||
|
export default store;
|
6
view/tsconfig.json
Executable file
6
view/tsconfig.json
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||||
|
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user