Compare commits
No commits in common. "main" and "refactor_pes" have entirely different histories.
main
...
refactor_p
|
@ -1,40 +0,0 @@
|
||||||
name: Deploy
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: docker
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Use Node.js 19
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 19
|
|
||||||
|
|
||||||
- name: Install rsync
|
|
||||||
run: |
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y rsync
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm install
|
|
||||||
|
|
||||||
- name: Build app
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: Add Deploy Key to SSH
|
|
||||||
run: |
|
|
||||||
mkdir ~/.ssh
|
|
||||||
echo "${{ secrets.SSH_KEY }}" >> ~/.ssh/id_ed25519_embroideryviewer
|
|
||||||
chmod 400 ~/.ssh/id_ed25519_embroideryviewer
|
|
||||||
echo -e "Host embroideryviewer\n\tUser embroideryviewer\n\tHostname 45.76.5.44\n\tIdentityFile ~/.ssh/id_ed25519_embroideryviewer\n\tStrictHostKeyChecking No" >> ~/.ssh/config
|
|
||||||
|
|
||||||
- name: Upload changes to server
|
|
||||||
run: rsync -avz --progress dist/ embroideryviewer:web/prod
|
|
27
.github/workflows/deploy.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
SSH_KEY: ${{secrets.SSH_KEY}}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Use Node.js 16
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
cache: "npm"
|
||||||
|
- run: npm install
|
||||||
|
- run: npm run build
|
||||||
|
- run: mkdir ~/.ssh
|
||||||
|
- run: echo "$SSH_KEY" >> ~/.ssh/id_rsa_embroideryviewer
|
||||||
|
- run: chmod 400 ~/.ssh/id_rsa_embroideryviewer
|
||||||
|
- run: echo -e "Host embroideryviewer\n\tUser embroideryviewer\n\tHostname 45.76.5.44\n\tIdentityFile ~/.ssh/id_rsa_embroideryviewer\n\tStrictHostKeyChecking No" >> ~/.ssh/config
|
||||||
|
- run: rsync -avz --progress dist/ embroideryviewer:web/prod
|
BIN
demo.gif
Before Width: | Height: | Size: 4.9 MiB After Width: | Height: | Size: 13 MiB |
31
index.html
|
@ -5,18 +5,33 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="author" content="Leonardo Murça" />
|
<meta name="author" content="Leonardo Murça" />
|
||||||
|
<meta name="description" content="Free online embroidery viewer.">
|
||||||
|
<meta name="keywords"
|
||||||
|
content="Free Emrbroidery Viewer, embroidery design, sewing machine, preview .pes files, preview embroider designs, brother machine.">
|
||||||
|
|
||||||
|
<script async defer data-website-id="e9089f5e-32ea-45f1-a000-b16af1dba58a"
|
||||||
|
src="https://umami.leomurca.xyz/umami.js"></script>
|
||||||
|
|
||||||
|
<link rel="apple-touch-icon" sizes="57x57" href="/apple-icon-57x57.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="60x60" href="/apple-icon-60x60.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="72x72" href="/apple-icon-72x72.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="76x76" href="/apple-icon-76x76.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="114x114" href="/apple-icon-114x114.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="120x120" href="/apple-icon-120x120.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="144x144" href="/apple-icon-144x144.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="152x152" href="/apple-icon-152x152.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="192x192" href="/android-icon-192x192.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
<meta name="msapplication-TileColor" content="#ffffff">
|
||||||
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
|
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
|
||||||
<script defer src="https://umami.leomurca.xyz/script.js" data-website-id="bd4c0533-36e6-402d-ac04-577993aaf43a"></script>
|
<title>Embroidery Viewer</title>
|
||||||
|
|
||||||
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
||||||
<link rel="shortcut icon" href="/favicon.ico" />
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
|
||||||
<meta name="apple-mobile-web-app-title" content="Embroidery Viewer" />
|
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
|
||||||
<link rel="canonical" href="https://embroideryviewer.xyz/">
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
1334
package-lock.json
generated
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "embroidery-viewer",
|
"name": "embroidery-viewer",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.0.0",
|
"version": "1.2.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
@ -10,8 +10,8 @@
|
||||||
"postbuild": "npx svelte-sitemap --domain https://embroideryviewer.xyz -o dist"
|
"postbuild": "npx svelte-sitemap --domain https://embroideryviewer.xyz -o dist"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
"@sveltejs/vite-plugin-svelte": "^2.4.1",
|
||||||
"svelte": "^5.23.3",
|
"svelte": "^3.59.1",
|
||||||
"vite": "^6.2.3"
|
"vite": "^4.3.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
public/android-icon-144x144.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
public/android-icon-192x192.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
public/android-icon-36x36.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
public/android-icon-48x48.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
public/android-icon-72x72.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
public/android-icon-96x96.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
public/apple-icon-114x114.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
public/apple-icon-120x120.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
public/apple-icon-144x144.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
public/apple-icon-152x152.png
Normal file
After Width: | Height: | Size: 9.3 KiB |
BIN
public/apple-icon-180x180.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
public/apple-icon-57x57.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
public/apple-icon-60x60.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
public/apple-icon-72x72.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
public/apple-icon-76x76.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
public/apple-icon-precomposed.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
public/apple-icon.png
Normal file
After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 18 KiB |
2
public/browserconfig.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
|
BIN
public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 61 KiB |
41
public/manifest.json
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"name": "App",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "\/android-icon-36x36.png",
|
||||||
|
"sizes": "36x36",
|
||||||
|
"type": "image\/png",
|
||||||
|
"density": "0.75"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "\/android-icon-48x48.png",
|
||||||
|
"sizes": "48x48",
|
||||||
|
"type": "image\/png",
|
||||||
|
"density": "1.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "\/android-icon-72x72.png",
|
||||||
|
"sizes": "72x72",
|
||||||
|
"type": "image\/png",
|
||||||
|
"density": "1.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "\/android-icon-96x96.png",
|
||||||
|
"sizes": "96x96",
|
||||||
|
"type": "image\/png",
|
||||||
|
"density": "2.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "\/android-icon-144x144.png",
|
||||||
|
"sizes": "144x144",
|
||||||
|
"type": "image\/png",
|
||||||
|
"density": "3.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "\/android-icon-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image\/png",
|
||||||
|
"density": "4.0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
BIN
public/ms-icon-144x144.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
public/ms-icon-150x150.png
Normal file
After Width: | Height: | Size: 9.1 KiB |
BIN
public/ms-icon-310x310.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
public/ms-icon-70x70.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
|
@ -1,21 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Embroidery Viewer",
|
|
||||||
"short_name": "EmbViewer",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "/web-app-manifest-192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/web-app-manifest-512x512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"theme_color": "#06345f",
|
|
||||||
"background_color": "#06345f",
|
|
||||||
"display": "standalone"
|
|
||||||
}
|
|
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 71 KiB |
|
@ -1,11 +1,22 @@
|
||||||
<script>
|
<script>
|
||||||
import Head from "./lib/sections/Head.svelte";
|
import Header from "./lib/Header.svelte";
|
||||||
import Header from "./lib/sections/Header.svelte";
|
import FileViewer from "./lib/FileViewer.svelte";
|
||||||
import Footer from "./lib/sections/Footer.svelte";
|
import Footer from "./lib/Footer.svelte";
|
||||||
import Main from "./lib/sections/Main.svelte";
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Head/>
|
|
||||||
<Header />
|
<Header />
|
||||||
<Main />
|
<main>
|
||||||
|
<FileViewer />
|
||||||
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
28
src/app.css
|
@ -17,10 +17,9 @@
|
||||||
body {
|
body {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-direction: column;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
|
@ -48,27 +47,4 @@ input[type="submit"]:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
body a {
|
|
||||||
text-decoration: none;
|
|
||||||
color: #06345F;
|
|
||||||
border-bottom: 3px solid #06345F;
|
|
||||||
}
|
|
||||||
|
|
||||||
body a:hover {
|
|
||||||
background-color: #06345F;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(h1, h2, h3, h4, h5, h6) {
|
|
||||||
color: #06345F;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
|
||||||
color: #06345F;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul li::marker {
|
|
||||||
color: #06345F;
|
|
||||||
}
|
|
Before Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 155 KiB |
Before Width: | Height: | Size: 130 KiB |
|
@ -1,18 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="68.965652mm"
|
|
||||||
height="68.948975mm"
|
|
||||||
viewBox="0 0 68.965652 68.948975"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
xml:space="preserve"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
|
||||||
id="defs1" /><g
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(-78.581248,-114.14203)"><path
|
|
||||||
d="m 101.71922,133.815 7.89657,-7.93103 v 33.06897 c 0,4.59771 6.89657,4.59771 6.89657,0 v -33.06897 l 7.89657,7.93103 c 1.34889,1.35999 3.54767,1.35999 4.89656,0 1.36001,-1.34889 1.36001,-3.5477 0,-4.89659 l -13.79313,-13.79308 c -0.32789,-0.31411 -0.71459,-0.56036 -1.1379,-0.72463 -0.83953,-0.34489 -1.78117,-0.34489 -2.6207,0 -0.42331,0.16427 -0.81001,0.41052 -1.1379,0.72463 l -13.793128,13.79308 c -3.265094,3.26437 1.631464,8.16096 4.896558,4.89659 z m 42.3794,14.7931 c -1.90447,0 -3.44833,1.5439 -3.44828,3.44837 v 20.68969 c -2e-5,1.90442 -1.54386,3.44824 -3.44828,3.44824 H 88.926098 c -1.904415,0 -3.448254,-1.54382 -3.448278,-3.44824 v -20.68969 c 0,-4.59771 -6.89657,-4.59771 -6.89657,0 v 20.68969 c -10e-7,5.7133 4.631545,10.34485 10.344848,10.34485 h 48.275962 c 5.7133,0 10.34485,-4.63155 10.34485,-10.34485 v -20.68969 c 5e-5,-1.90447 -1.54382,-3.44837 -3.44829,-3.44837 z"
|
|
||||||
id="path1"
|
|
||||||
style="opacity:1;mix-blend-mode:normal;fill:#06345f;fill-opacity:1;stroke-width:3.448;stroke-dasharray:none" /></g></svg>
|
|
Before Width: | Height: | Size: 1.4 KiB |
|
@ -2,7 +2,7 @@ import { jDataView } from "./jdataview";
|
||||||
import { supportedFormats } from "../format-readers";
|
import { supportedFormats } from "../format-readers";
|
||||||
import { Pattern } from "./pattern";
|
import { Pattern } from "./pattern";
|
||||||
|
|
||||||
function renderFile(filename, evt, canvas, colorView, stitchesView, sizeView, localizedStrings) {
|
function renderFile(filename, evt, canvas, colorView, stitchesView, sizeView) {
|
||||||
const fileExtension = filename.toLowerCase().split(".").pop();
|
const fileExtension = filename.toLowerCase().split(".").pop();
|
||||||
const view = jDataView(evt.target.result, 0, evt.size);
|
const view = jDataView(evt.target.result, 0, evt.size);
|
||||||
const pattern = new Pattern();
|
const pattern = new Pattern();
|
||||||
|
@ -12,8 +12,8 @@ function renderFile(filename, evt, canvas, colorView, stitchesView, sizeView, lo
|
||||||
pattern.moveToPositive();
|
pattern.moveToPositive();
|
||||||
pattern.drawShapeTo(canvas);
|
pattern.drawShapeTo(canvas);
|
||||||
pattern.drawColorsTo(colorView);
|
pattern.drawColorsTo(colorView);
|
||||||
pattern.drawStitchesCountTo(stitchesView, localizedStrings.stitches);
|
pattern.drawStitchesCountTo(stitchesView);
|
||||||
pattern.drawSizeValuesTo(stitchesView, localizedStrings.dimensions);
|
pattern.drawSizeValuesTo(stitchesView);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAbortMessage(errorMessageRef) {
|
function renderAbortMessage(errorMessageRef) {
|
||||||
|
@ -56,13 +56,12 @@ export default function renderFileToCanvas(
|
||||||
errorMessageRef,
|
errorMessageRef,
|
||||||
colorView,
|
colorView,
|
||||||
stitchesView,
|
stitchesView,
|
||||||
sizeView,
|
sizeView
|
||||||
localizedStrings
|
|
||||||
) {
|
) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onloadend = (evt) =>
|
reader.onloadend = (evt) =>
|
||||||
renderFile(fileObject.name, evt, canvas, colorView, stitchesView, sizeView, localizedStrings);
|
renderFile(fileObject.name, evt, canvas, colorView, stitchesView, sizeView);
|
||||||
reader.abort = (/** @type {any} */ _) => renderAbortMessage(errorMessageRef);
|
reader.abort = (/** @type {any} */ _) => renderAbortMessage(errorMessageRef);
|
||||||
reader.onerror = (evt) =>
|
reader.onerror = (evt) =>
|
||||||
renderErrorMessage(evt.target.error.name, errorMessageRef);
|
renderErrorMessage(evt.target.error.name, errorMessageRef);
|
||||||
|
|
|
@ -208,16 +208,16 @@ Pattern.prototype.drawShapeTo = function (canvas) {
|
||||||
|
|
||||||
Pattern.prototype.drawColorsTo = function (colorContainer) {
|
Pattern.prototype.drawColorsTo = function (colorContainer) {
|
||||||
this.colors.forEach((color) => {
|
this.colors.forEach((color) => {
|
||||||
colorContainer.innerHTML += `<div style='background-color: rgb(${color.r}, ${color.g}, ${color.b}); height: 25px; width: 25px; border: 1px solid #000000; border-radius: 16px;'></div>`;
|
colorContainer.innerHTML += `<div style='background-color: rgb(${color.r}, ${color.g}, ${color.b}); height: 25px; width: 25px; border: 1px solid #000000;'></div>`;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Pattern.prototype.drawStitchesCountTo = function (stitchesContainer, stitchesString) {
|
Pattern.prototype.drawStitchesCountTo = function (stitchesContainer) {
|
||||||
stitchesContainer.innerHTML += `<div><strong>${stitchesString}:</strong> ${this.stitches.length} </div>`;
|
stitchesContainer.innerHTML += `<div><strong>Stitches:</strong> ${this.stitches.length} </div>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
Pattern.prototype.drawSizeValuesTo = function (sizeContainer, dimensionsString) {
|
Pattern.prototype.drawSizeValuesTo = function (sizeContainer) {
|
||||||
sizeContainer.innerHTML += `<div><strong>${dimensionsString}:</strong> ${Math.round(
|
sizeContainer.innerHTML += `<div><strong>Size (x, y):</strong> ${Math.round(
|
||||||
this.right / 10
|
this.right / 10
|
||||||
)}mm x ${Math.round(this.bottom / 10)}mm </div>`;
|
)}mm x ${Math.round(this.bottom / 10)}mm </div>`;
|
||||||
};
|
};
|
||||||
|
|
|
@ -82,52 +82,54 @@ const colors = [
|
||||||
new Color(227, 172, 129, "Bamboo"),
|
new Color(227, 172, 129, "Bamboo"),
|
||||||
];
|
];
|
||||||
|
|
||||||
const jefDecode = (byte) => (byte >= 0x80 ? -(~byte & 0xff) - 1 : byte);
|
function jefDecode(inputByte) {
|
||||||
const isSpecialStitch = (byte) => byte === 0x80;
|
return inputByte >= 0x80 ? -(~inputByte & 0xff) - 1 : inputByte;
|
||||||
const isStopOrTrim = (byte) => (byte & 0x01) !== 0 || byte === 0x02 || byte === 0x04;
|
|
||||||
const isEndOfPattern = (byte) => byte === 0x10;
|
|
||||||
const isStop = (byte) => byte & 0x01;
|
|
||||||
const readStitchData = (file) => ({ byte1: file.getUint8(), byte2: file.getUint8() });
|
|
||||||
|
|
||||||
const addColorsToPattern = (file, pattern, colorCount) => {
|
|
||||||
for (let i = 0; i < colorCount; i++) {
|
|
||||||
pattern.addColor(colors[file.getUint32(file.tell(), true) % colors.length]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const determineStitchType = (file, byte1, byte2) => {
|
|
||||||
if (isSpecialStitch(byte1)) {
|
|
||||||
if (isStopOrTrim(byte2)) {
|
|
||||||
return { type: isStop(byte2) ? stitchTypes.stop : stitchTypes.trim, byte1: file.getUint8(), byte2: file.getUint8() };
|
|
||||||
} else if (isEndOfPattern(byte2)) {
|
|
||||||
return { type: stitchTypes.end, byte1: 0, byte2: 0, end: true };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { type: stitchTypes.normal, byte1, byte2 };
|
|
||||||
}
|
|
||||||
|
|
||||||
const processStitches = (file, pattern, stitchCount) => {
|
|
||||||
let stitchesProcessed = 0;
|
|
||||||
while (stitchesProcessed < stitchCount + 100) {
|
|
||||||
let { byte1, byte2 } = readStitchData(file);
|
|
||||||
let { type, byte1: decodedByte1, byte2: decodedByte2, end } = determineStitchType(file, byte1, byte2);
|
|
||||||
pattern.addStitchRel(jefDecode(decodedByte1), jefDecode(decodedByte2), type, true);
|
|
||||||
if (end) break;
|
|
||||||
stitchesProcessed++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function jefRead(file, pattern) {
|
export function jefRead(file, pattern) {
|
||||||
file.seek(24);
|
file.seek(24);
|
||||||
const colorCount = file.getInt32(file.tell(), true);
|
const numberOfColors = file.getInt32(file.tell(), true);
|
||||||
const stitchCount = file.getInt32(file.tell(), true);
|
const numberOfStitches = file.getInt32(file.tell(), true);
|
||||||
file.seek(file.tell() + 84);
|
file.seek(file.tell() + 84);
|
||||||
|
|
||||||
addColorsToPattern(file, pattern, colorCount);
|
for (let i = 0; i < numberOfColors; i += 1) {
|
||||||
file.seek(file.tell() + (6 - colorCount) * 4);
|
pattern.addColor(colors[file.getUint32(file.tell(), true) % 78]);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 6 - numberOfColors; i += 1) {
|
||||||
|
file.getUint32();
|
||||||
|
}
|
||||||
|
|
||||||
processStitches(file, pattern, stitchCount);
|
let flags,
|
||||||
|
b0,
|
||||||
|
b1,
|
||||||
|
dx,
|
||||||
|
dy,
|
||||||
|
stitchCount = 0;
|
||||||
|
while (stitchCount < numberOfStitches + 100) {
|
||||||
|
flags = stitchTypes.normal;
|
||||||
|
b0 = file.getUint8();
|
||||||
|
b1 = file.getUint8();
|
||||||
|
|
||||||
|
if (b0 === 0x80) {
|
||||||
|
if (b1 & 0x01) {
|
||||||
|
b0 = file.getUint8();
|
||||||
|
b1 = file.getUint8();
|
||||||
|
flags = stitchTypes.stop;
|
||||||
|
} else if (b1 === 0x02 || b1 === 0x04) {
|
||||||
|
b0 = file.getUint8();
|
||||||
|
b1 = file.getUint8();
|
||||||
|
flags = stitchTypes.trim;
|
||||||
|
} else if (b1 === 0x10) {
|
||||||
|
pattern.addStitchRel(0, 0, stitchTypes.end, true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dx = jefDecode(b0);
|
||||||
|
dy = jefDecode(b1);
|
||||||
|
pattern.addStitchRel(dx, dy, flags, true);
|
||||||
|
stitchCount += 1;
|
||||||
|
}
|
||||||
pattern.invertPatternVertical();
|
pattern.invertPatternVertical();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const jefColors = colors;
|
export const jefColors = colors;
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
import { derived, writable } from "svelte/store";
|
|
||||||
import translations from "./translations";
|
|
||||||
|
|
||||||
const storedLocale = localStorage.getItem("locale");
|
|
||||||
const browserLocale = navigator.language || "en";
|
|
||||||
const [baseLang] = browserLocale.split("-");
|
|
||||||
|
|
||||||
export const DEFAULT_LOCALE =
|
|
||||||
storedLocale && translations[storedLocale] ? storedLocale :
|
|
||||||
translations[browserLocale] ? browserLocale :
|
|
||||||
translations[baseLang] ? baseLang :
|
|
||||||
"en";
|
|
||||||
|
|
||||||
export const locale = writable(DEFAULT_LOCALE);
|
|
||||||
|
|
||||||
locale.subscribe((value) => {
|
|
||||||
if (value) localStorage.setItem("locale", value);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const locales = Object.entries(translations).map(([key, lang]) => [key, lang.name]);
|
|
||||||
|
|
||||||
function translate(locale, key, vars = {}) {
|
|
||||||
if (!key) throw new Error("Translation key is required.");
|
|
||||||
|
|
||||||
const fallbackLocale = "en";
|
|
||||||
const validLocale = translations[locale]
|
|
||||||
? locale
|
|
||||||
: translations[baseLang]
|
|
||||||
? baseLang
|
|
||||||
: fallbackLocale;
|
|
||||||
|
|
||||||
let text = translations[validLocale][key] || translations[fallbackLocale][key];
|
|
||||||
|
|
||||||
if (!text) {
|
|
||||||
console.error(`Missing translation for key "${key}" in locale "${validLocale}".`);
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.entries(vars).reduce(
|
|
||||||
(str, [varKey, value]) => str.replaceAll(`{{${varKey}}}`, value),
|
|
||||||
text
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const t = derived(locale, ($locale) => (key, vars = {}) =>
|
|
||||||
translate($locale, key, vars)
|
|
||||||
);
|
|
|
@ -1,104 +0,0 @@
|
||||||
export default {
|
|
||||||
en: {
|
|
||||||
"head.title": "Free Online Embroidery File Viewer – Open PES, DST, EXP & More",
|
|
||||||
"head.description": "View multiple embroidery files online for free! Open PES, DST, EXP, JEF & more without software. Upload and preview multiple files in a card list format. Try now!",
|
|
||||||
"head.keywords": "free embroidery file viewer, open PES files online, view DST files, embroidery file preview, EXP file viewer, multiple embroidery files",
|
|
||||||
"head.ogtitle": "Free Online Embroidery File Viewer – Open PES, DST & More",
|
|
||||||
"head.ogdescription": "Upload and preview multiple embroidery files like PES, DST, and EXP online for free. No software needed!",
|
|
||||||
"nav.home": "🏠 Home",
|
|
||||||
"nav.viewer": "🧵 Viewer",
|
|
||||||
"nav.donate": "💖 Donate",
|
|
||||||
"nav.about": "ℹ️ About",
|
|
||||||
"main.title": "Upload files",
|
|
||||||
"home.main.title": "🧵 Free Online Embroidery File Viewer",
|
|
||||||
"home.main.description": "<p>✨Upload and preview your embroidery designs instantly – no software needed.</p> <p><strong>Embroidery Viewer</strong> is a free, browser-based tool that supports multiple embroidery file formats. View your designs quickly and securely, right in your browser.</p>",
|
|
||||||
"home.features.title": "🚀 Features",
|
|
||||||
"home.features.list": "<ul><li>📂 <strong>Supports Multiple Formats:</strong> DST, PES, JEF, EXP, VP3, and more</li><li>⚡ <strong>Quick Previews:</strong> See your embroidery files rendered as images</li><li>🧷 <strong>Multiple Files at Once:</strong> Upload several designs and view them side-by-side</li><li>🔒 <strong>No Upload to Server:</strong> Your files stay private – all processing happens locally</li><li>⬇️ <strong>Download as Image:</strong> Save each embroidery design preview as a PNG</li><li>💸 <strong>Fast & Free:</strong> No installations, no sign-ups – just open and use</li></ul>",
|
|
||||||
"home.howtouse.title": "📘 How to Use",
|
|
||||||
"home.howtouse.list": "<ol><li>📁 <strong>Click</strong> the upload button <em>or</em> <strong>drag and drop</strong> your embroidery files into the drop area</li><li>🧵 Select one or more embroidery files</li><li>▶️ Click the <strong>“Render files”</strong> button to preview your designs</li><li>👀 Instantly view your designs right in your browser – it’s that simple</li></ol>",
|
|
||||||
"home.testimonials.title": "❤️ Loved by Hobbyists and Professionals",
|
|
||||||
"home.testimonials.description": "<p>Whether you're a hobbyist working on your next DIY project or a professional digitizer reviewing client files, <strong>Embroidery Viewer</strong> gives you a no-fuss, instant way to visualize your work.</p>",
|
|
||||||
"home.donation.title": "💖 Help Keep It Free",
|
|
||||||
"home.donation.description": "<p><strong>Embroidery Viewer is completely free</strong> for everyone to use, with no ads.</p><p>If you find it useful and want to support ongoing development and hosting costs, please consider making a small donation.</p>",
|
|
||||||
"home.donation.cta": "🙌 Donate Now",
|
|
||||||
"home.donation.cta.description": "every little bit helps!",
|
|
||||||
"home.cta.title": "🚀 Try It Now",
|
|
||||||
"home.cta.cta": "🧵 Open Viewer",
|
|
||||||
"home.cta.cta.description": "the fastest <strong>Free Online Embroidery File Viewer</strong>.",
|
|
||||||
"donate.title": "💖 Donate",
|
|
||||||
"donate.subtitle": "Help support Embroidery Viewer and its development!",
|
|
||||||
"donate.description": "⭐️ <strong>Embroidery Viewer</strong> is free to use, with no ads. If you find this tool helpful, please consider making a donation to keep it running and fund future improvements.",
|
|
||||||
"donate.ways": "💸 Ways to Donate",
|
|
||||||
"donate.bitcoin.description": "Scan or copy the address",
|
|
||||||
"donate.copy": "Copy Address",
|
|
||||||
"donate.copied": "Copied to Clipboard!",
|
|
||||||
"donate.copy.failed": "Copy Failed!",
|
|
||||||
"donate.monero.description": "Private and secure donation option.",
|
|
||||||
"donate.paypal.description": "Want to show support in a friendly way?",
|
|
||||||
"donate.paypal.link": "Open Donation link",
|
|
||||||
"main.languageSwitch": "🇧🇷",
|
|
||||||
"main.fileSize": "Max file size is <strong>{{fileSize}}MB</strong>.",
|
|
||||||
"main.supportedFormats": "Accepted formats: <strong>{{supportedFormats}}</strong>.",
|
|
||||||
"main.render": "Render files",
|
|
||||||
"main.dropzone": "<strong>Choose files</strong><br /><span>or drag and drop them here</span>",
|
|
||||||
"main.browse": "Browse",
|
|
||||||
"main.selected": "Selected files",
|
|
||||||
"main.rejected": "Rejected files",
|
|
||||||
"main.stitches": "Stitches",
|
|
||||||
"main.dimensions": "Dimensions (x, y)",
|
|
||||||
"main.download": "Download image",
|
|
||||||
"main.copyright": "Copyright © {{year}} <a href=\"{{website}}\" target=\"_blank\" rel=\"noreferrer\">Leonardo Murça</a>. <br/> All rights reserved.",
|
|
||||||
"main.version": "🧵 Version: {{version}}"
|
|
||||||
},
|
|
||||||
pt: {
|
|
||||||
"head.title": "Visualizador de arquivos de bordado online gratuito – Abra PES, DST, EXP e mais",
|
|
||||||
"head.description": "Visualize vários arquivos de bordado online gratuitamente! Abra PES, DST, EXP, JEF e mais sem software. Carregue e visualize vários arquivos em um formato de lista de cartões. Experimente agora!",
|
|
||||||
"head.keywords": "visualizador de arquivos de bordado grátis, abra arquivos PES online, visualize arquivos DST, pré-visualização de arquivos de bordado, visualizador de arquivos EXP, vários arquivos de bordado",
|
|
||||||
"head.ogtitle": "Visualizador de arquivos de bordado online gratuito – Abra PES, DST e mais",
|
|
||||||
"head.ogdescription": "Carregue e visualize vários arquivos de bordado como PES, DST e EXP online gratuitamente. Não precisa de software!",
|
|
||||||
"nav.home": "🏠 Página Inicial",
|
|
||||||
"nav.viewer": "🧵 Visualizador",
|
|
||||||
"nav.donate": "💖 Doe",
|
|
||||||
"nav.about": "ℹ️ Sobre",
|
|
||||||
"home.main.title": "🧵 Visualizador de arquivos de bordado online gratuito",
|
|
||||||
"home.main.description": "<p>✨Carregue e visualize seus desenhos de bordado instantaneamente – sem necessidade de software</p> <p><strong>Embroidery Viewer</strong> é uma ferramenta gratuita para navegador que suporta diversos formatos de arquivo de bordado. Visualize seus designs de forma rápida e segura, diretamente no seu navegador.</p>",
|
|
||||||
"home.features.title": "🚀 Funcionalidades",
|
|
||||||
"home.features.list": "<ul><li>📂 <strong>Suporta vários formatos:</strong> DST, PES, JEF, EXP, VP3 e mais</li><li>⚡ <strong>Visualizações rápidas:</strong> Veja seus arquivos de bordado renderizados como imagens</li><li>🧷 <strong>Vários arquivos de uma só vez:</strong> Carregue vários designs e visualize-os lado a lado</li><li>🔒 <strong>Sem upload para o servidor:</strong> Seus arquivos permanecem privados – todo o processamento acontece localmente</li><li>⬇️ <strong>Baixar como imagem:</strong> Salve cada pré-visualização do desenho do bordado como um PNG</li><li>💸 <strong>Rápido e gratuito:</strong> Sem instalações, sem cadastros – basta abrir e usar</li></ul>",
|
|
||||||
"home.howtouse.title": "📘 Como usar",
|
|
||||||
"home.howtouse.list": "<ol><li>📁 <strong>Clique</strong> no botão de upload <em>ou</em> <strong>arraste e solte</strong> seus arquivos de bordado na área de soltar</li><li>🧵 Selecione um ou mais arquivos de bordado</li><li>▶️ Clique no botão <strong>“Renderizar arquivos”</strong> para visualizar seus designs</li><li>👀 Visualize seus designs instantaneamente no seu navegador – é simples assim</li></ol>",
|
|
||||||
"home.testimonials.title": "❤️ Amado por Hobbyistas e Profissionais",
|
|
||||||
"home.testimonials.description": "<p>Seja você um amador trabalhando em seu próximo projeto \"faça você mesmo\" ou um digitalizador profissional revisando arquivos de clientes, o <strong>Embroidery Viewer</strong> oferece uma maneira fácil e instantânea de visualizar seu trabalho.</p>",
|
|
||||||
"home.donation.title": "💖 Ajude a mantê-lo gratuito",
|
|
||||||
"home.donation.description": "<p><strong>O Embroidery Viewer é totalmente gratuito</strong> para todos usarem, sem anúncios.</p><p>Se você o achar útil e quiser apoiar o desenvolvimento contínuo e os custos de hospedagem, considere fazer uma pequena doação.</p>",
|
|
||||||
"home.donation.cta": "🙌 Doe agora",
|
|
||||||
"home.donation.cta.description": "cada pequena ajuda é bem-vinda!",
|
|
||||||
"home.cta.title": "🚀 Experimente agora",
|
|
||||||
"home.cta.cta": "🧵 Abrir visualizador",
|
|
||||||
"home.cta.cta.description": "o <strong>visualizador de arquivos de bordado online gratuito</strong> mais rápido.",
|
|
||||||
"donate.title": "💖 Doe",
|
|
||||||
"donate.subtitle": "Ajude a apoiar o Embroidery Viewer e seu desenvolvimento!",
|
|
||||||
"donate.description": "⭐️ O <strong>Embroidery Viewer</strong> é gratuito e sem anúncios. Se você achar esta ferramenta útil, considere fazer uma doação para mantê-la funcionando e financiar melhorias futuras.",
|
|
||||||
"donate.ways": "💸 Formas de doar",
|
|
||||||
"donate.bitcoin.description": "Escaneie ou copie o endereço",
|
|
||||||
"donate.copy": "Copiar Endereço",
|
|
||||||
"donate.copied": "Copiado para a área de transferência!",
|
|
||||||
"donate.copy.failed": "Falha na Cópia!",
|
|
||||||
"donate.monero.description": "Opção de doação privada e segura.",
|
|
||||||
"donate.paypal.description": "Quer demonstrar apoio de uma forma amigável?",
|
|
||||||
"donate.paypal.link": "Abrir Link de Doação",
|
|
||||||
"main.title": "Carregar arquivos",
|
|
||||||
"main.languageSwitch": "🇺🇸",
|
|
||||||
"main.fileSize": "O tamanho máximo de cada arquivo é <strong>{{fileSize}}MB</strong>.",
|
|
||||||
"main.supportedFormats": "Formatos aceitos: <strong>{{supportedFormats}}</strong>.",
|
|
||||||
"main.render": "Renderizar arquivos",
|
|
||||||
"main.dropzone": "<strong>Selecione arquivos</strong><br /><span>ou arraste e solte-os aqui</span>",
|
|
||||||
"main.browse": "Selecionar arquivos",
|
|
||||||
"main.selected": "Arquivos selecionados",
|
|
||||||
"main.rejected": "Arquivos recusados",
|
|
||||||
"main.stitches": "Pontos",
|
|
||||||
"main.dimensions": "Dimensões (x, y)",
|
|
||||||
"main.download": "Baixar imagem",
|
|
||||||
"main.copyright": "Copyright © {{year}} <a href=\"{{website}}/pt-br\" target=\"_blank\" rel=\"noreferrer\">Leonardo Murça</a>. <br/> Todos os direitos reservados.",
|
|
||||||
"main.version": "🧵 Versão: {{version}}"
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,6 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { t } from "../../i18n"
|
import renderFileToCanvas from "../file-renderer";
|
||||||
import renderFileToCanvas from "../../file-renderer";
|
|
||||||
|
|
||||||
export let files = [];
|
export let files = [];
|
||||||
let canvasRefs = [];
|
let canvasRefs = [];
|
||||||
|
@ -8,10 +7,6 @@
|
||||||
let stitchesRefs = [];
|
let stitchesRefs = [];
|
||||||
let sizeRefs = [];
|
let sizeRefs = [];
|
||||||
let errorMessageRef;
|
let errorMessageRef;
|
||||||
let localizedStrings = {
|
|
||||||
stitches: $t("main.stitches"),
|
|
||||||
dimensions: $t("main.dimensions"),
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadCanvasAsImage = (canvas, filename) => {
|
const downloadCanvasAsImage = (canvas, filename) => {
|
||||||
const image = canvas
|
const image = canvas
|
||||||
|
@ -35,19 +30,18 @@
|
||||||
<div id="container" style="width: 100%; heigth: 100vh;">
|
<div id="container" style="width: 100%; heigth: 100vh;">
|
||||||
{#each Array.from(files) as file, i}
|
{#each Array.from(files) as file, i}
|
||||||
<div class="canvas-container">
|
<div class="canvas-container">
|
||||||
<canvas bind:this={canvasRefs[i]} class="canvas"></canvas>
|
<canvas bind:this={canvasRefs[i]} class="canvas" />
|
||||||
<p><strong>{file.name}</strong></p>
|
<p><strong>{file.name}</strong></p>
|
||||||
<div class="stitches-container" bind:this={stitchesRefs[i]}></div>
|
<div class="stitches-container" bind:this={stitchesRefs[i]} />
|
||||||
<div class="size-container" bind:this={sizeRefs[i]}></div>
|
<div class="size-container" bind:this={sizeRefs[i]} />
|
||||||
<div class="colors-container" bind:this={colorRefs[i]}></div>
|
<div class="colors-container" bind:this={colorRefs[i]} />
|
||||||
<div
|
<div
|
||||||
id="download-button"
|
id="download-button"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex=0
|
|
||||||
on:keydown={onKeydown}
|
on:keydown={onKeydown}
|
||||||
on:click={() => downloadCanvasAsImage(canvasRefs[i], file.name)}
|
on:click={() => downloadCanvasAsImage(canvasRefs[i], file.name)}
|
||||||
>
|
>
|
||||||
{$t("main.download")}
|
Download
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{canvasRefs[i] &&
|
{canvasRefs[i] &&
|
||||||
|
@ -57,12 +51,11 @@
|
||||||
errorMessageRef,
|
errorMessageRef,
|
||||||
colorRefs[i],
|
colorRefs[i],
|
||||||
stitchesRefs[i],
|
stitchesRefs[i],
|
||||||
sizeRefs[i],
|
sizeRefs[i]
|
||||||
localizedStrings
|
|
||||||
)}
|
)}
|
||||||
{/each}
|
{/each}
|
||||||
<!-- svelte-ignore a11y-missing-content -->
|
<!-- svelte-ignore a11y-missing-content -->
|
||||||
<h1 bind:this={errorMessageRef}></h1>
|
<h1 bind:this={errorMessageRef} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -84,9 +77,7 @@
|
||||||
max-height: 1000px;
|
max-height: 1000px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
/* border: 2px solid black;*/
|
border: 2px solid black;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
||||||
border-radius: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas {
|
.canvas {
|
||||||
|
@ -110,13 +101,10 @@
|
||||||
|
|
||||||
div[role="button"] {
|
div[role="button"] {
|
||||||
background-color: #05345f;
|
background-color: #05345f;
|
||||||
font-weight: bold;
|
font-weight: 500;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 10px;
|
border-radius: 0;
|
||||||
padding: 10px;
|
|
||||||
width: 50%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div[role="button"]:hover {
|
div[role="button"]:hover {
|
||||||
|
@ -134,10 +122,5 @@
|
||||||
#container {
|
#container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
div[role="button"] {
|
|
||||||
width: 100%;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -1,7 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import { t } from "../../i18n"
|
|
||||||
import upload from "../../assets/upload.svg"
|
|
||||||
|
|
||||||
export let files;
|
export let files;
|
||||||
export let supportedFormats;
|
export let supportedFormats;
|
||||||
export let onKeydown;
|
export let onKeydown;
|
||||||
|
@ -14,13 +11,14 @@
|
||||||
<div
|
<div
|
||||||
id="dropzone"
|
id="dropzone"
|
||||||
tabindex={0}
|
tabindex={0}
|
||||||
role="region"
|
|
||||||
on:keydown={onKeydown}
|
on:keydown={onKeydown}
|
||||||
|
on:click={onClick}
|
||||||
on:dragover|preventDefault|stopPropagation
|
on:dragover|preventDefault|stopPropagation
|
||||||
on:drop|preventDefault|stopPropagation={onDrop}
|
on:drop|preventDefault|stopPropagation={onDrop}
|
||||||
>
|
>
|
||||||
<img src={upload} width="40" height="40" alt="Upload icon" />
|
<label id="file-label" for="file-input"
|
||||||
<label id="file-label" for="file-input">{@html $t("main.dropzone")}</label>
|
>Drag and drop files here or click to upload.</label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
id="file-input"
|
id="file-input"
|
||||||
type="file"
|
type="file"
|
||||||
|
@ -28,58 +26,38 @@
|
||||||
accept={supportedFormats.join(",")}
|
accept={supportedFormats.join(",")}
|
||||||
multiple
|
multiple
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
bind:this={files}
|
bind:files
|
||||||
/>
|
/>
|
||||||
<button on:click|preventDefault={onClick}>{$t("main.browse")}</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#dropzone {
|
#dropzone {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
height: 100px;
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
border: 1px solid #d3dce6;
|
|
||||||
border-radius: 12px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
border: 5px dotted black;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
#file-label {
|
#file-label {
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
margin-top: 10px;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
#file-input {
|
#file-input {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
#dropzone:hover {
|
||||||
margin-top: 20px;
|
|
||||||
padding: 12px 24px;
|
|
||||||
background-color: #06345F;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 1rem;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 60%;
|
border: 5px dotted #05345f;
|
||||||
font-weight: bold;
|
color: #05345f;
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
color: #fff;
|
|
||||||
background-color: #000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-device-width: 812px) {
|
@media only screen and (max-device-width: 812px) {
|
||||||
#dropzone {
|
#dropzone {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -7,39 +7,24 @@
|
||||||
{#if files.length !== 0}
|
{#if files.length !== 0}
|
||||||
<div id="selected-files-container">
|
<div id="selected-files-container">
|
||||||
<h2>{title}:</h2>
|
<h2>{title}:</h2>
|
||||||
<div id="files-list">
|
|
||||||
{#each Array.from(files) as file}
|
{#each Array.from(files) as file}
|
||||||
<div id={isError ? "selected-file-card-error" : "selected-file-card"}>
|
<div id={isError ? "selected-file-card-error" : "selected-file-card"}>
|
||||||
<span>{file.name}</span>
|
<p>{file.name} ({file.size / 1000} kb)</p>
|
||||||
<span>{Math.round(file.size / 1000)} KB</span>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#files-list{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#selected-file-card {
|
#selected-file-card {
|
||||||
display: flex;
|
border: 1px solid #000;
|
||||||
justify-content: space-between;
|
|
||||||
color: #06345F;
|
|
||||||
font-weight: bolder;
|
|
||||||
width: 500px;
|
width: 500px;
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#selected-file-card-error {
|
#selected-file-card-error {
|
||||||
display: flex;
|
border: 1px solid red;
|
||||||
justify-content: space-between;
|
|
||||||
color: #06345F;
|
|
||||||
font-weight: bolder;
|
|
||||||
width: 500px;
|
width: 500px;
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
|
@ -3,16 +3,15 @@
|
||||||
import Dropzone from "./Dropzone.svelte";
|
import Dropzone from "./Dropzone.svelte";
|
||||||
import FileList from "./FileList.svelte";
|
import FileList from "./FileList.svelte";
|
||||||
|
|
||||||
import { filterFiles } from "../../utils/filterFiles";
|
import { filterFiles } from "../utils/filterFiles";
|
||||||
import { supportedFormats } from "../../format-readers";
|
import { supportedFormats } from "../format-readers";
|
||||||
import { t } from "../../i18n"
|
|
||||||
|
|
||||||
let acceptedFiles;
|
let acceptedFiles;
|
||||||
let rejectedFiles;
|
let rejectedFiles;
|
||||||
let areAcceptedFilesRendered = false;
|
let areAcceptedFilesRendered = false;
|
||||||
const fileRequirements = {
|
const fileRequirements = {
|
||||||
supportedFormats: Object.values(supportedFormats).map((f) => f.ext),
|
supportedFormats: Object.values(supportedFormats).map((f) => f.ext),
|
||||||
maxSize: 1000000,
|
maxSize: 700000,
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
|
@ -45,7 +44,6 @@
|
||||||
document.getElementById("file-input").click();
|
document.getElementById("file-input").click();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
|
@ -53,12 +51,11 @@
|
||||||
enctype="multipart/form-data"
|
enctype="multipart/form-data"
|
||||||
on:submit|preventDefault|stopPropagation={onSubmit}
|
on:submit|preventDefault|stopPropagation={onSubmit}
|
||||||
>
|
>
|
||||||
<div class="title-container">
|
<h2>Upload files</h2>
|
||||||
<h2>{$t("main.title")}</h2>
|
|
||||||
</div>
|
|
||||||
<p>
|
<p>
|
||||||
{@html $t("main.fileSize", { fileSize: fileRequirements.maxSize / 1000000 })}
|
Max file size is <strong>{fileRequirements.maxSize / 1000}kb</strong>.
|
||||||
{@html $t("main.supportedFormats", { supportedFormats: fileRequirements.supportedFormats.join(", ") })}
|
Accepted formats:
|
||||||
|
<strong>{fileRequirements.supportedFormats.join(", ")}</strong>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Dropzone
|
<Dropzone
|
||||||
|
@ -70,34 +67,18 @@
|
||||||
{onChange}
|
{onChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input id="submit" type="submit" value={$t("main.render")} />
|
<input type="submit" value="Render files" />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{#if areAcceptedFilesRendered}
|
{#if areAcceptedFilesRendered}
|
||||||
<CardList files={acceptedFiles} />
|
<CardList files={acceptedFiles} />
|
||||||
{:else}
|
{:else}
|
||||||
<FileList title={$t("main.selected")} files={acceptedFiles} />
|
<FileList title="Rejected Files" files={rejectedFiles} isError />
|
||||||
<FileList title={$t("main.rejected")} files={rejectedFiles} isError />
|
<FileList title="Selected Files" files={acceptedFiles} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
form {
|
@media only screen and (max-device-width: 812px) {
|
||||||
width: fit-content;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.title-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#submit {
|
|
||||||
border: none;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 15px
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-device-width: 768px) {
|
|
||||||
#form {
|
#form {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
24
src/lib/Footer.svelte
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<script>
|
||||||
|
import { appVersion } from "../utils/env";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>
|
||||||
|
Copyright © {new Date().getFullYear()}
|
||||||
|
<a href="https://leomurca.xyz" target="_blank" rel="noreferrer"
|
||||||
|
>Leonardo Murça</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
version: {appVersion()}
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
footer {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
42
src/lib/Header.svelte
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<script>
|
||||||
|
import MediaQuery from "../lib/MediaQuery.svelte";
|
||||||
|
import logo from "../assets/embroidery-viewer-logo.webp";
|
||||||
|
import logoMobile from "../assets/embroidery-viewer-logo-mobile.webp";
|
||||||
|
|
||||||
|
const configsFor = (matches) => {
|
||||||
|
return matches
|
||||||
|
? { src: logoMobile, width: 350, height: 96 }
|
||||||
|
: { src: logo, width: 460, height: 200 };
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<a href="/">
|
||||||
|
<MediaQuery query="(min-width: 481px) and (max-width: 812px)" let:matches>
|
||||||
|
{@const configs = configsFor(matches)}
|
||||||
|
<img
|
||||||
|
class="logo"
|
||||||
|
alt="Embroidery viewer logo."
|
||||||
|
src={configs.src}
|
||||||
|
width={configs.width}
|
||||||
|
height={configs.height}
|
||||||
|
/>
|
||||||
|
</MediaQuery>
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
header {
|
||||||
|
margin-top: 100px;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
background-image: logo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-device-width: 812px) {
|
||||||
|
.logo {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,24 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { routes, fallback } from '../../utils/routes.js';
|
|
||||||
import { path } from '../../utils/stores.js';
|
|
||||||
|
|
||||||
const navigate = (to) => {
|
|
||||||
history.pushState({}, '', to);
|
|
||||||
path.set(to);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('popstate', () => {
|
|
||||||
path.set(window.location.pathname);
|
|
||||||
});
|
|
||||||
|
|
||||||
let component;
|
|
||||||
const unsubscribe = path.subscribe(current => {
|
|
||||||
component = routes[current] !== undefined ? routes[current].component : fallback;
|
|
||||||
});
|
|
||||||
|
|
||||||
onMount(() => () => unsubscribe());
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:component this={component} />
|
|
|
@ -1,195 +0,0 @@
|
||||||
|
|
||||||
<script>
|
|
||||||
import { t } from "../../i18n"
|
|
||||||
import bitcoin from "../../assets/bitcoin.svg"
|
|
||||||
import monero from "../../assets/monero.svg"
|
|
||||||
import paypal from "../../assets/paypal.svg"
|
|
||||||
|
|
||||||
let bitcoinCopyStatus = '';
|
|
||||||
let moneroCopyStatus= '';
|
|
||||||
|
|
||||||
const onCopyMonero = async (text) => {
|
|
||||||
try {
|
|
||||||
const textarea = document.createElement('textarea');
|
|
||||||
textarea.value = text;
|
|
||||||
textarea.setAttribute('readonly', '');
|
|
||||||
textarea.style.position = 'absolute';
|
|
||||||
textarea.style.left = '-9999px';
|
|
||||||
document.body.appendChild(textarea);
|
|
||||||
|
|
||||||
textarea.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
document.body.removeChild(textarea);
|
|
||||||
|
|
||||||
moneroCopyStatus = 'donate.copied';
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Copy failed:', err);
|
|
||||||
moneroCopyStatus = 'donate.copy.failed';
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => moneroCopyStatus = '', 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCopyBitcoin = async (text) => {
|
|
||||||
try {
|
|
||||||
const textarea = document.createElement('textarea');
|
|
||||||
textarea.value = text;
|
|
||||||
textarea.setAttribute('readonly', '');
|
|
||||||
textarea.style.position = 'absolute';
|
|
||||||
textarea.style.left = '-9999px';
|
|
||||||
document.body.appendChild(textarea);
|
|
||||||
|
|
||||||
textarea.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
document.body.removeChild(textarea);
|
|
||||||
|
|
||||||
bitcoinCopyStatus = 'donate.copied';
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Copy failed:', err);
|
|
||||||
bitcoinCopyStatus = 'donate.copy.failed';
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => bitcoinCopyStatus = '', 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
</script>
|
|
||||||
<section aria-labelledby="donate-title">
|
|
||||||
<h1 id="donate-title">{$t("donate.title")}</h1>
|
|
||||||
<p class="donate-subtitle">{$t("donate.subtitle")}</p>
|
|
||||||
<p>
|
|
||||||
{@html $t("donate.description")}
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
<section id="ways" aria-labelledby="ways-title">
|
|
||||||
<h2>{$t("donate.ways")}</h2>
|
|
||||||
<div class="donation-options">
|
|
||||||
<article class="donation-method" aria-labelledby="btc-label">
|
|
||||||
<img src={bitcoin} width="200" height="200" alt="Bitcoin QR code" />
|
|
||||||
<h3 id="btc-label">Bitcoin</h3>
|
|
||||||
<p>{$t("donate.bitcoin.description")}</p>
|
|
||||||
<button id="copy-btc" aria-label="Copy Bitcoin address" on:click={() => onCopyBitcoin("bc1qpc4lpyr6stxrrg3u0k4clp4crlt6z4j6q845rq")}>
|
|
||||||
{#if bitcoinCopyStatus}
|
|
||||||
{$t(bitcoinCopyStatus)}
|
|
||||||
{:else}
|
|
||||||
{$t("donate.copy")}
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
|
|
||||||
<article class="donation-method" aria-labelledby="xmr-label">
|
|
||||||
<img src={monero} alt="Monero QR code" width="200" height="200" />
|
|
||||||
<h3 id="xmr-label">Monero</h3>
|
|
||||||
<p>{$t("donate.monero.description")}</p>
|
|
||||||
<button id="copy-monero" aria-label="Copy Monero address" on:click={() => onCopyMonero("8A9iyTskiBh6f6GDUwnUJaYhAW13gNjDYaZYJBftX434D3XLrcGBko4a8kC4pLSfiuJAoSJ7e8rwP8W4StsVypftCp6FGwm")}>
|
|
||||||
{#if moneroCopyStatus}
|
|
||||||
{$t(moneroCopyStatus)}
|
|
||||||
{:else}
|
|
||||||
{$t("donate.copy")}
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="donation-method" aria-labelledby="bmc-label">
|
|
||||||
<img src={paypal} alt="PayPal" width="200" height="200" />
|
|
||||||
<h3 id="bmc-label">PayPal</h3>
|
|
||||||
<p>{$t("donate.paypal.description")}</p>
|
|
||||||
<a id="paypal-donation-link" aria-label="Paypal donation link" target="_blank" href="https://www.paypal.com/donate/?business=leo@leomurca.xyz¤cy_code=USD">{$t("donate.paypal.link")}</a>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
h1 {
|
|
||||||
padding: 0;
|
|
||||||
margin-bottom: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.donate-subtitle {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #06345F;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.donation-options {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.donation-method {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 33.33%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.donation-method p {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
font-size: 14px;
|
|
||||||
background-color: #05345f;
|
|
||||||
font-weight: bold;
|
|
||||||
color: white;
|
|
||||||
padding: 10px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 10px;
|
|
||||||
width: 200px;
|
|
||||||
height: 45px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: black;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#paypal-donation-link {
|
|
||||||
font-size: 14px;
|
|
||||||
background-color: #05345f;
|
|
||||||
font-weight: bold;
|
|
||||||
color: white;
|
|
||||||
padding: 10px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 10px;
|
|
||||||
width: 200px;
|
|
||||||
height: 45px;
|
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#paypal-donation-link:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: black;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
|
|
||||||
button {
|
|
||||||
font-size: 1em;
|
|
||||||
width: 100%;
|
|
||||||
height: 55px;
|
|
||||||
}
|
|
||||||
#paypal-donation-link {
|
|
||||||
font-size: 1em;
|
|
||||||
width: 100%;
|
|
||||||
height: 55px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.donation-options{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 50px;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.donation-method {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,60 +0,0 @@
|
||||||
<script>
|
|
||||||
import { t } from "../../i18n"
|
|
||||||
import { path } from '../../utils/stores.js';
|
|
||||||
|
|
||||||
const onNavigateTo = (e, route) => {
|
|
||||||
e.preventDefault()
|
|
||||||
history.pushState({}, '', route);
|
|
||||||
path.set(route);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="home-container">
|
|
||||||
|
|
||||||
<section aria-labelledby="main-title">
|
|
||||||
<h1 id="main-title">{$t("home.main.title")}</h1>
|
|
||||||
{@html $t("home.main.description")}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section aria-labelledby="features-title">
|
|
||||||
<h2 id="features-title">{$t("home.features.title")}</h2>
|
|
||||||
{@html $t("home.features.list")}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section aria-labelledby="how-to-use-title">
|
|
||||||
<h2 id="how-to-use-title">{$t("home.howtouse.title")}</h2>
|
|
||||||
{@html $t("home.howtouse.list")}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section aria-labelledby="testimonials-title">
|
|
||||||
<h2 id="testimonials-title">{$t("home.testimonials.title")}</h2>
|
|
||||||
{@html $t("home.testimonials.description")}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section aria-labelledby="donation-title">
|
|
||||||
<h2 id="donation-title">{$t("home.donation.title")}</h2>
|
|
||||||
{@html $t("home.donation.description")}
|
|
||||||
<p><a href="#" on:click={(e) => onNavigateTo(e, "/donate")} class="button">{$t("home.donation.cta")}</a> – {$t("home.donation.cta.description")}</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!--TODO: add video preview-->
|
|
||||||
<section aria-labelledby="cta-title">
|
|
||||||
<h2 id="cta-title">{$t("home.cta.title")}</h2>
|
|
||||||
<p><a href="#" on:click={(e) => onNavigateTo(e, "/viewer")} class="button">{$t("home.cta.cta")}</a> – {@html $t("home.cta.cta.description")}</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.home-container {
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 70%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.home-container {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
<h1>404 - Not Found</h1>
|
|
||||||
<p>Oops! That route does not exist.</p>
|
|
|
@ -1,4 +0,0 @@
|
||||||
<script>
|
|
||||||
import FileViewer from "../components/FileViewer.svelte"
|
|
||||||
</script>
|
|
||||||
<FileViewer/>
|
|
|
@ -1,49 +0,0 @@
|
||||||
<script>
|
|
||||||
import { t } from "../../i18n"
|
|
||||||
import { appVersion } from "../../utils/env";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<div class="footer-content">
|
|
||||||
<p>{@html $t("main.copyright", { year: new Date().getFullYear(), website: "https://leomurca.xyz" })}</p>
|
|
||||||
<p>{@html $t("main.version", { version: appVersion() })}</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
footer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
width: 100%;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-content {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer p {
|
|
||||||
margin: 5px 0;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer p:first-child {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
footer {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer p {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<script>
|
|
||||||
import { t, locale } from "../../i18n";
|
|
||||||
import thumbnail from "../../assets/thumbnail.webp";
|
|
||||||
|
|
||||||
$: document.documentElement.lang = $locale;
|
|
||||||
</script>
|
|
||||||
<svelte:head>
|
|
||||||
<title>{$t("head.title")}</title>
|
|
||||||
<meta name="description" content="{$t('head.description')}" />
|
|
||||||
<meta name="keywords" content="{$t('head.keywords')}">
|
|
||||||
<meta property="og:title" content="{$t('head.ogtitle')}">
|
|
||||||
<meta property="og:description" content="{$t('head.ogdescription')}">
|
|
||||||
<meta property="og:url" content="https://embroideryviewer.xyz/">
|
|
||||||
<meta property="og:type" content="website">
|
|
||||||
<meta property="og:image" content="{thumbnail}">
|
|
||||||
</svelte:head>
|
|
|
@ -1,195 +0,0 @@
|
||||||
<script>
|
|
||||||
import MediaQuery from "../MediaQuery.svelte";
|
|
||||||
import logo from "../../assets/logo.webp";
|
|
||||||
import { t, locale, locales } from "../../i18n"
|
|
||||||
import { path } from '../../utils/stores.js';
|
|
||||||
import { routes } from '../../utils/routes.js';
|
|
||||||
|
|
||||||
const configsFor = (matches) => {
|
|
||||||
return matches
|
|
||||||
? { src: logo, width: 150, height: 70} // mobile
|
|
||||||
: { src: logo, width: 150, height: 100 }; // desktop
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSwitchToOppositeLang = () => {
|
|
||||||
const oppositeLang = locales.find(item => item[0] !== $locale);
|
|
||||||
locale.set(oppositeLang[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const onNavigateTo = (e, route) => {
|
|
||||||
e.preventDefault()
|
|
||||||
history.pushState({}, '', route);
|
|
||||||
path.set(route);
|
|
||||||
if (isMenuOpen) {
|
|
||||||
isMenuOpen = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let isMenuOpen = false;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<header>
|
|
||||||
<div class="logo">
|
|
||||||
<MediaQuery query="(max-width: 768px)" let:matches>
|
|
||||||
{@const configs = configsFor(matches)}
|
|
||||||
<a href="#" on:click={(e) => onNavigateTo(e, "/")}>
|
|
||||||
<img src={configs.src} alt="Embroidery viewer logo" width={configs.width} height={configs.height}/>
|
|
||||||
</a>
|
|
||||||
</MediaQuery>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nav-container">
|
|
||||||
<MediaQuery query="(max-width: 768px)" let:matches >
|
|
||||||
<slot let-matches>
|
|
||||||
{#if matches}
|
|
||||||
<button class="hamburger" on:click={() => (isMenuOpen = !isMenuOpen)}>
|
|
||||||
{#if isMenuOpen}x{:else}☰{/if}
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</slot>
|
|
||||||
</MediaQuery>
|
|
||||||
<nav class:is-open={isMenuOpen}>
|
|
||||||
<ul>
|
|
||||||
{#each Object.entries(routes) as [route, config]}
|
|
||||||
<li><a href="#" on:click={(e) => onNavigateTo(e, route)} >{$t(config.nameKey)}</a></li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<a class="common-switch {$locale === 'en' ? 'portuguese-switch' : 'english-switch' }" href="#" on:click|preventDefault={onSwitchToOppositeLang}>
|
|
||||||
<div style="display: flex; width: fit-content;">
|
|
||||||
<span style="font-size: 20px;">{$t("main.languageSwitch")}</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 10px 100px;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo img {
|
|
||||||
height: auto;
|
|
||||||
max-height: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo a {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo a:hover {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-container {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul li {
|
|
||||||
display: flex;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hamburger {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
font-size: 35px;
|
|
||||||
width: 35px;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.common-switch {
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.portuguese-switch {
|
|
||||||
color: #0C8F27;
|
|
||||||
border-bottom: 3px solid #0C8F27 !important;
|
|
||||||
fill: #0C8F27 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.portuguese-switch:hover {
|
|
||||||
background: #0C8F27;
|
|
||||||
color: #ffffff;
|
|
||||||
fill: #ffffff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.english-switch{
|
|
||||||
color: #BE0A2F;
|
|
||||||
border-bottom: 3px solid #BE0A2F;
|
|
||||||
width: fit-content;
|
|
||||||
fill: #BE0A2F !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.english-switch:hover {
|
|
||||||
background: #BE0A2F;
|
|
||||||
color: #ffffff;
|
|
||||||
fill: #ffffff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
header {
|
|
||||||
padding: 10px 20px ;
|
|
||||||
}
|
|
||||||
.hamburger {
|
|
||||||
display: block;
|
|
||||||
width: 35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
display: none;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
position: absolute;
|
|
||||||
top: 60px;
|
|
||||||
right: 0;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.is-open {
|
|
||||||
display: inline-block;
|
|
||||||
margin-top: 25px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul li {
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul li a {
|
|
||||||
display: inline-block;
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,14 +0,0 @@
|
||||||
<script>
|
|
||||||
import Router from "../components/Router.svelte";
|
|
||||||
</script>
|
|
||||||
<main>
|
|
||||||
<Router />
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
main {
|
|
||||||
flex: 1; /* This pushes footer to bottom */
|
|
||||||
padding: 20px;
|
|
||||||
min-height: 85vh;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { mount } from 'svelte';
|
|
||||||
import App from './App.svelte';
|
|
||||||
import "./app.css";
|
import "./app.css";
|
||||||
|
import App from "./App.svelte";
|
||||||
|
|
||||||
const app = mount(App, {
|
const app = new App({
|
||||||
target: document.getElementById('app'),
|
target: document.getElementById("app"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
import Home from '../lib/pages/Home.svelte';
|
|
||||||
import Donate from '../lib/pages/Donate.svelte';
|
|
||||||
import Viewer from '../lib/pages/Viewer.svelte';
|
|
||||||
import NotFound from '../lib/pages/NotFound.svelte';
|
|
||||||
|
|
||||||
export const routes = {
|
|
||||||
'/': {
|
|
||||||
component: Home,
|
|
||||||
nameKey: "nav.home"
|
|
||||||
},
|
|
||||||
'/viewer': {
|
|
||||||
component: Viewer,
|
|
||||||
nameKey: "nav.viewer"
|
|
||||||
},
|
|
||||||
'/donate': {
|
|
||||||
component: Donate,
|
|
||||||
nameKey: "nav.donate"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fallback = NotFound;
|
|
|
@ -1,3 +0,0 @@
|
||||||
import { writable } from 'svelte/store';
|
|
||||||
|
|
||||||
export const path = writable(window.location.pathname);
|
|