Compare commits

...

10 commits

58 changed files with 3347 additions and 147 deletions

View file

@ -10,22 +10,13 @@
<script defer src="https://umami.leomurca.xyz/script.js" data-website-id="bd4c0533-36e6-402d-ac04-577993aaf43a"></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">
<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/">
<meta name="msapplication-TileColor" content="#ffffff">
</head>
<body>

View file

@ -1,7 +1,7 @@
{
"name": "embroidery-viewer",
"private": true,
"version": "1.2.6",
"version": "2.0.0",
"type": "module",
"scripts": {
"dev": "vite",

View file

@ -1 +0,0 @@
google.com, pub-5761689301112420, DIRECT, f08c47fec0942fa0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -1,2 +0,0 @@
<?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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 15 KiB

3
public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 61 KiB

View file

@ -1,41 +0,0 @@
{
"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"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

21
public/site.webmanifest Normal file
View file

@ -0,0 +1,21 @@
{
"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"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View file

@ -1,24 +1,11 @@
<script>
import Head from "./lib/Head.svelte";
import Header from "./lib/Header.svelte";
import FileViewer from "./lib/FileViewer.svelte";
import Footer from "./lib/Footer.svelte";
import Head from "./lib/sections/Head.svelte";
import Header from "./lib/sections/Header.svelte";
import Footer from "./lib/sections/Footer.svelte";
import Main from "./lib/sections/Main.svelte";
</script>
<Head/>
<Header />
<main>
<FileViewer />
</main>
<Main />
<Footer />
<style>
main {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
box-sizing: border-box;
padding: 15px;
}
</style>

View file

@ -23,12 +23,6 @@ body {
height: 100%;
}
main {
flex: 1; /* This pushes footer to bottom */
padding: 20px;
min-height: 85vh;
}
#app {
display: flex;
flex-direction: column;
@ -66,3 +60,15 @@ 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;
}

543
src/assets/bitcoin.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 84 KiB

1230
src/assets/monero.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 155 KiB

1013
src/assets/paypal.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 130 KiB

18
src/assets/upload.svg Normal file
View file

@ -0,0 +1,18 @@
<?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>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -208,7 +208,7 @@ Pattern.prototype.drawShapeTo = function (canvas) {
Pattern.prototype.drawColorsTo = function (colorContainer) {
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;'></div>`;
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>`;
});
};

View file

@ -5,22 +5,50 @@ export default {
"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.donate": "Donate",
"nav.about": "About",
"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 its 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}}kb</strong>.",
"main.fileSize": "Max file size is <strong>{{fileSize}}MB</strong>.",
"main.supportedFormats": "Accepted formats: <strong>{{supportedFormats}}</strong>.",
"main.render": "Render files",
"main.dropzone": "Drag and drop files here or click to upload.",
"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}}"
"main.version": "🧵 Version: {{version}}"
},
pt: {
"head.title": "Visualizador de arquivos de bordado online gratuito Abra PES, DST, EXP e mais",
@ -28,21 +56,49 @@ export default {
"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.donate": "Doe",
"nav.about": "Sobre",
"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 do arquivo é <strong>{{fileSize}}kb</strong>.",
"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": "Arraste e solte os arquivos aqui ou clique para fazer upload.",
"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}}"
"main.version": "🧵 Versão: {{version}}"
},
};

View file

@ -1,6 +1,6 @@
<script>
import { t } from "../i18n"
import renderFileToCanvas from "../file-renderer";
import { t } from "../../i18n"
import renderFileToCanvas from "../../file-renderer";
export let files = [];
let canvasRefs = [];
@ -84,7 +84,9 @@
max-height: 1000px;
margin-bottom: 15px;
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 {
@ -108,10 +110,13 @@
div[role="button"] {
background-color: #05345f;
font-weight: 500;
font-weight: bold;
color: white;
padding: 10px;
border-radius: 0;
border-radius: 10px;
padding: 10px;
width: 50%;
text-align: center;
}
div[role="button"]:hover {
@ -129,5 +134,10 @@
#container {
width: 100%;
}
div[role="button"] {
width: 100%;
padding: 15px;
}
}
</style>

View file

@ -1,5 +1,6 @@
<script>
import { t } from "../i18n"
import { t } from "../../i18n"
import upload from "../../assets/upload.svg"
export let files;
export let supportedFormats;
@ -15,13 +16,11 @@
tabindex={0}
role="region"
on:keydown={onKeydown}
on:click={onClick}
on:dragover|preventDefault|stopPropagation
on:drop|preventDefault|stopPropagation={onDrop}
>
<label id="file-label" for="file-input"
>{$t("main.dropzone")}</label
>
<img src={upload} width="40" height="40" alt="Upload icon" />
<label id="file-label" for="file-input">{@html $t("main.dropzone")}</label>
<input
id="file-input"
type="file"
@ -29,38 +28,58 @@
accept={supportedFormats.join(",")}
multiple
on:change={onChange}
bind:files
bind:this={files}
/>
<button on:click|preventDefault={onClick}>{$t("main.browse")}</button>
</div>
<style>
#dropzone {
display: flex;
height: 100px;
flex-direction: column;
align-items: center;
text-align: center;
border: 1px solid #d3dce6;
border-radius: 12px;
width: 100%;
border: 5px dotted black;
padding: 15px;
z-index: 10;
}
#file-label {
z-index: -1;
font-weight: 600;
margin-top: 10px;
}
#file-input {
display: none;
}
#dropzone:hover {
button {
margin-top: 20px;
padding: 12px 24px;
background-color: #06345F;
color: white;
border: none;
border-radius: 10px;
font-size: 1rem;
cursor: pointer;
border: 5px dotted #05345f;
color: #05345f;
width: 60%;
font-weight: bold;
}
button:hover {
color: #fff;
background-color: #000;
}
@media only screen and (max-device-width: 812px) {
#dropzone {
width: 100%;
}
button {
width: 100%;
}
}
</style>

View file

@ -7,24 +7,39 @@
{#if files.length !== 0}
<div id="selected-files-container">
<h2>{title}:</h2>
<div id="files-list">
{#each Array.from(files) as file}
<div id={isError ? "selected-file-card-error" : "selected-file-card"}>
<p>{file.name} ({file.size / 1000} kb)</p>
<span>{file.name}</span>
<span>{Math.round(file.size / 1000)} KB</span>
</div>
{/each}
</div>
</div>
{/if}
<style>
#files-list{
display: flex;
flex-direction: column;
align-items: center;
}
#selected-file-card {
border: 1px solid #000;
display: flex;
justify-content: space-between;
color: #06345F;
font-weight: bolder;
width: 500px;
padding-left: 15px;
margin-top: 10px;
}
#selected-file-card-error {
border: 1px solid red;
display: flex;
justify-content: space-between;
color: #06345F;
font-weight: bolder;
width: 500px;
padding-left: 15px;
margin-top: 10px;

View file

@ -3,16 +3,16 @@
import Dropzone from "./Dropzone.svelte";
import FileList from "./FileList.svelte";
import { filterFiles } from "../utils/filterFiles";
import { supportedFormats } from "../format-readers";
import { t } from "../i18n"
import { filterFiles } from "../../utils/filterFiles";
import { supportedFormats } from "../../format-readers";
import { t } from "../../i18n"
let acceptedFiles;
let rejectedFiles;
let areAcceptedFilesRendered = false;
const fileRequirements = {
supportedFormats: Object.values(supportedFormats).map((f) => f.ext),
maxSize: 700000,
maxSize: 1000000,
};
const onSubmit = () => {
@ -57,7 +57,7 @@
<h2>{$t("main.title")}</h2>
</div>
<p>
{@html $t("main.fileSize", { fileSize: fileRequirements.maxSize / 1000 })}
{@html $t("main.fileSize", { fileSize: fileRequirements.maxSize / 1000000 })}
{@html $t("main.supportedFormats", { supportedFormats: fileRequirements.supportedFormats.join(", ") })}
</p>
@ -70,7 +70,7 @@
{onChange}
/>
<input type="submit" value={$t("main.render")} />
<input id="submit" type="submit" value={$t("main.render")} />
</form>
{#if areAcceptedFilesRendered}
@ -81,13 +81,23 @@
{/if}
<style>
form {
width: fit-content;
margin: 0 auto;
}
.title-container {
display: flex;
justify-content: space-between;
align-items: center;
}
@media only screen and (max-device-width: 812px) {
#submit {
border: none;
border-radius: 10px;
padding: 15px
}
@media only screen and (max-device-width: 768px) {
#form {
width: 100%;
}

View file

@ -0,0 +1,24 @@
<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} />

195
src/lib/pages/Donate.svelte Normal file
View file

@ -0,0 +1,195 @@
<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&currency_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>

60
src/lib/pages/Home.svelte Normal file
View file

@ -0,0 +1,60 @@
<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>

View file

@ -0,0 +1,2 @@
<h1>404 - Not Found</h1>
<p>Oops! That route does not exist.</p>

View file

@ -0,0 +1,4 @@
<script>
import FileViewer from "../components/FileViewer.svelte"
</script>
<FileViewer/>

View file

@ -1,6 +1,6 @@
<script>
import { t } from "../i18n"
import { appVersion } from "../utils/env";
import { t } from "../../i18n"
import { appVersion } from "../../utils/env";
</script>
<footer>
@ -43,7 +43,7 @@
}
footer p {
font-size: 12px;
font-size: 12px;
}
}
</style>

View file

@ -1,6 +1,6 @@
<script>
import { t, locale } from "../i18n";
import thumbnail from "../assets/thumbnail.webp";
import { t, locale } from "../../i18n";
import thumbnail from "../../assets/thumbnail.webp";
$: document.documentElement.lang = $locale;
</script>

View file

@ -1,7 +1,9 @@
<script>
import MediaQuery from "../lib/MediaQuery.svelte";
import logo from "../assets/logo.webp";
import { t, locale, locales } from "../i18n"
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
@ -9,17 +11,20 @@
: { src: logo, width: 150, height: 100 }; // desktop
};
const navLinks = [
{ name: () => $t("nav.home"), href: '/' },
{ name: () => $t("nav.donate"), href: '/donate' },
{ name: () => $t("nav.about"), href: '/about' },
];
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>
@ -27,13 +32,12 @@
<div class="logo">
<MediaQuery query="(max-width: 768px)" let:matches>
{@const configs = configsFor(matches)}
<a href="/">
<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>
@ -46,8 +50,8 @@
</MediaQuery>
<nav class:is-open={isMenuOpen}>
<ul>
{#each navLinks as link}
<li><a href={link.href}>{link.name()}</a></li>
{#each Object.entries(routes) as [route, config]}
<li><a href="#" on:click={(e) => onNavigateTo(e, route)} >{$t(config.nameKey)}</a></li>
{/each}
</ul>
</nav>
@ -114,11 +118,6 @@
display: none;
}
.language-icon {
width: 30px;
height: 24px;
}
.common-switch {
width: fit-content;
}

View file

@ -0,0 +1,14 @@
<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>

21
src/utils/routes.js Normal file
View file

@ -0,0 +1,21 @@
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;

3
src/utils/stores.js Normal file
View file

@ -0,0 +1,3 @@
import { writable } from 'svelte/store';
export const path = writable(window.location.pathname);