Compare commits

..

No commits in common. "main" and "setup_i18n" have entirely different histories.

71 changed files with 849 additions and 4606 deletions

View file

@ -13,10 +13,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2
- name: Use Node.js 19
- name: Use Node.js 16
uses: actions/setup-node@v4
with:
node-version: 19
node-version: 16
- name: Install rsync
run: |

View file

@ -1,7 +1,7 @@
# Embroidery Viewer
A free online tool to view embroidery files.
Available at https://embroideryviewer.xyz.
Available at https://embroideryviewer.xyz..
![Demo](/demo.gif)

BIN
demo.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 MiB

After

Width:  |  Height:  |  Size: 13 MiB

View file

@ -5,18 +5,33 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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="theme-color" content="#ffffff">
<meta name="apple-mobile-web-app-title" content="Embroidery Viewer" />
<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" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="canonical" href="https://embroideryviewer.xyz/">
</head>
<body>

1334
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "embroidery-viewer",
"private": true,
"version": "2.0.3",
"version": "1.2.4",
"type": "module",
"scripts": {
"dev": "vite",
@ -10,8 +10,8 @@
"postbuild": "npx svelte-sitemap --domain https://embroideryviewer.xyz -o dist"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"svelte": "^5.23.3",
"vite": "^6.2.3"
"@sveltejs/vite-plugin-svelte": "^2.4.1",
"svelte": "^3.59.1",
"vite": "^4.3.9"
}
}

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/apple-icon-57x57.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
public/apple-icon-60x60.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
public/apple-icon-72x72.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
public/apple-icon-76x76.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/apple-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

2
public/browserconfig.xml Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 61 KiB

41
public/manifest.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
public/ms-icon-150x150.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
public/ms-icon-310x310.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
public/ms-icon-70x70.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

View file

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

View file

@ -17,10 +17,9 @@
body {
display: flex;
justify-content: center;
flex-direction: column;
margin: 0;
width: 100%;
height: 100%;
min-height: 100vh;
}
#app {
@ -60,15 +59,3 @@ 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;
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 155 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 130 KiB

View file

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

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; 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>`;
});
};

View file

@ -1,35 +1,23 @@
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";
const [baseLang, region] = browserLocale.split("-");
export const DEFAULT_LOCALE = 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;
: "en";
let text = translations[validLocale][key] || translations[fallbackLocale][key];
let text = translations[validLocale][key] || translations["en"][key];
if (!text) {
console.error(`Missing translation for key "${key}" in locale "${validLocale}".`);
@ -44,4 +32,4 @@ function translate(locale, key, vars = {}) {
export const t = derived(locale, ($locale) => (key, vars = {}) =>
translate($locale, key, vars)
);
);

View file

@ -1,124 +1,32 @@
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",
"nav.privacy.policy": "🔐 Privacy Policy",
"nav.terms.of.service": "📝 Terms of Service",
"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.</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. 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",
"about.title": " About Embroidery Viewer",
"about.content": "<p>Hi there! 👋</p><p><strong>⭐️ Embroidery Viewer</strong> was born out of a simple need — helping someone I care about. 💖</p><p>My girlfriend loves embroidery, but she often struggled to find an easy and free way to preview her embroidery design files before stitching them. Most tools she tried were either paid, overly complex, or required technical knowledge — and shes not a techie.</p><p>So, to make things easier for her (and others like her), I decided to build this web application.</p><p>Over the course of a few weeks, I created <strong>Embroidery Viewer</strong> — a lightweight, fast, and free tool that lets you view embroidery files directly in your browser. No installation, no setup, and no tech hurdles. Just upload your file and see your design.</p><p>Its not a super sophisticated tool, but it solves the problem it was meant to solve: making embroidery file previews accessible to everyone.</p><p>If this tool has helped you too, that makes me really happy! I plan to continue improving it based on feedback from users like you.</p><p>Thanks for stopping by — and happy stitching! 🧵✨</p>",
"privacy.policy.title": "🔐 Privacy Policy",
"privacy.policy.last.update": "Last updated: May 9, 2025",
"privacy.policy.content": "<p>At <strong>Embroidery Viewer</strong> (<a href=\"https://embroideryviewer.xyz\">embroideryviewer.xyz</a>), we respect your privacy and are committed to protecting any information you share while using our service.</p><h2>1. Personal Information</h2><p>Embroidery Viewer does <strong>not</strong> collect or store any personal information. You do not need to create an account, and we do not ask for your name, email address, or any identifying details.</p><h2>2. File Uploads</h2><p>When you upload an embroidery file to the viewer, the file is processed in your browser or temporarily on our server (if required) for preview purposes only. <strong>No uploaded files are stored, saved, or shared.</strong></p><p>Please avoid uploading any copyrighted or sensitive material unless you have permission to use it.</p><h2>3. Analytics</h2><p>We use <strong>Umami</strong> to collect anonymous usage statistics about our website, such as the number of visitors, page views, device types, and referral sources. This data helps us understand how the site is being used and improve it over time.</p><p>Umami is a privacy-friendly, cookie-free analytics tool. It does <strong>not</strong> track users across sites, collect personal data, or use cookies. All data is aggregated and anonymized.</p><h2>4. Cookies</h2><p>Embroidery Viewer does <strong>not</strong> use cookies or other tracking mechanisms in your browser.</p><h2>5. Third-Party Services</h2><p>We do not use third-party advertising, embed external trackers, or share data with third parties.</p><h2>6. Changes to This Policy</h2><p>We may update this Privacy Policy from time to time. All updates will be posted on this page with the updated date.</p><h2>7. Contact</h2><p>If you have any questions about this Privacy Policy, you can reach us at <a href=\"mailto:leo@leomurca.xyz\">leo@leomurca.xyz</a>.</p>",
"terms.of.service.title": "📝 Terms of Service",
"terms.of.service.update": "May 9, 2025",
"terms.of.service.content": "<p>Welcome to <strong>Embroidery Viewer</strong> (<a href=\"https://embroideryviewer.xyz\">embroideryviewer.xyz</a>). By accessing or using this website, you agree to be bound by the following Terms of Service. If you do not agree with any part of these terms, please do not use the site.</p><h2>1. Description of Service</h2><p>Embroidery Viewer is a free, browser-based tool that allows users to preview embroidery design files online. The service is intended for personal, non-commercial use.</p><h2>2. Use of the Service</h2><p>You agree to use the service only for lawful purposes. You are solely responsible for any content (including embroidery files) you upload, and you confirm that you have the legal right to use, view, and process those files.</p><p>You agree not to upload any files that are illegal, offensive, infringe on intellectual property rights, or contain malicious code.</p><h2>3. File Processing</h2><p>Files uploaded to Embroidery Viewer are processed either directly in your browser or temporarily on our servers. Files are not stored permanently, shared, or backed up.</p><p>While we aim to keep your content secure, you acknowledge that no system is 100% secure and you use the service at your own risk.</p><h2>4. No Warranty</h2><p>This service is provided \"as is\" and \"as available\" without any warranties, express or implied. We do not guarantee that the service will be uninterrupted, secure, or error-free.</p><h2>5. Limitation of Liability</h2><p>Embroidery Viewer shall not be held liable for any damages resulting from the use or inability to use the service, including but not limited to loss of data, loss of profits, or other incidental or consequential damages.</p><h2>6. Modifications to the Service</h2><p>We reserve the right to modify, suspend, or discontinue the service at any time without notice. We may also update these Terms of Service from time to time. Continued use of the service after changes constitutes your acceptance of the new terms.</p><h2>7. Governing Law</h2><p>These Terms shall be governed by and interpreted in accordance with the laws of Brazil, without regard to its conflict of law principles.</p><h2>8. Contact</h2><p>If you have any questions about these Terms of Service, feel free to contact us at <a href=\"mailto:leo@leomurca.xyz\">leo@leomurca.xyz</a>.</p>",
"main.languageSwitch": "🇧🇷",
"main.fileSize": "Max file size is <strong>{{fileSize}}MB</strong>.",
"main.languageSwitch": "Mudar para Português",
"main.fileSize": "Max file size is <strong>{{fileSize}}kb</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.dropzone": "Drag and drop files here or click to upload.",
"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",
"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",
"nav.privacy.policy": "🔐 Política de Privacidade",
"nav.terms.of.service": "📝 Termos de Serviço",
"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.</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. 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",
"about.title": " Sobre o Embroidery Viewer",
"about.content": "<p>Oi! 👋</p><p><strong>⭐️ Embroidery Viewer</strong> nasceu de uma necessidade simples — ajudar alguém que eu amo. 💖</p><p>Minha namorada adora bordado, mas ela sempre teve dificuldades para encontrar uma maneira fácil e gratuita de visualizar os arquivos de design de bordado antes de começar a costurar. A maioria das ferramentas que ela tentou eram pagas, muito complexas ou exigiam conhecimento técnico — e ela não é da área de tecnologia.</p><p>Então, para facilitar a vida dela (e de outras pessoas como ela), decidi criar este aplicativo web.</p><p>Ao longo de algumas semanas, criei o <strong>Embroidery Viewer</strong> — uma ferramenta leve, rápida e gratuita que permite visualizar arquivos de bordado diretamente no navegador. Sem instalação, sem configuração e sem obstáculos técnicos. Basta enviar o arquivo e ver o design.</p><p>Não é uma ferramenta super sofisticada, mas resolve o problema para o qual foi criada: tornar a visualização de arquivos de bordado acessível para todos.</p><p>Se essa ferramenta também te ajudou, isso me deixa muito feliz! Pretendo continuar melhorando com base no feedback de usuários como você.</p><p>Obrigado por visitar — e bons bordados! 🧵✨</p>",
"privacy.policy.title": "🔐 Política de Privacidade",
"privacy.policy.last.update": "Última atualização: 9 de maio de 2025",
"privacy.policy.content": "<p>No <strong>Embroidery Viewer</strong> (<a href=\"https://embroideryviewer.xyz\">embroideryviewer.xyz</a>), respeitamos sua privacidade e estamos comprometidos em proteger qualquer informação que você compartilhe ao usar nosso serviço.</p><h2>1. Informações Pessoais</h2><p>O Embroidery Viewer <strong>não</strong> coleta nem armazena informações pessoais. Você não precisa criar uma conta e não pedimos seu nome, e-mail ou qualquer dado identificável.</p><h2>2. Envio de Arquivos</h2><p>Quando você envia um arquivo de bordado para o visualizador, o arquivo é processado no seu navegador ou temporariamente em nosso servidor (se necessário) apenas para fins de visualização. <strong>Nenhum arquivo enviado é armazenado, salvo ou compartilhado.</strong></p><p>Evite enviar materiais sensíveis ou protegidos por direitos autorais, a menos que tenha permissão para usá-los.</p><h2>3. Análises</h2><p>Utilizamos o <strong>Umami</strong> para coletar estatísticas anônimas de uso do site, como número de visitantes, visualizações de página, tipos de dispositivo e fontes de acesso. Esses dados nos ajudam a entender como o site está sendo utilizado e melhorá-lo com o tempo.</p><p>O Umami é uma ferramenta de análise que respeita a privacidade, não usa cookies e não rastreia os usuários entre sites. Todos os dados são agregados e anonimizados.</p><h2>4. Cookies</h2><p>O Embroidery Viewer <strong>não</strong> utiliza cookies ou outros mecanismos de rastreamento em seu navegador.</p><h2>5. Serviços de Terceiros</h2><p>Não utilizamos publicidade de terceiros, nem incorporamos rastreadores externos, nem compartilhamos dados com terceiros.</p><h2>6. Alterações nesta Política</h2><p>Podemos atualizar esta Política de Privacidade ocasionalmente. Todas as atualizações serão publicadas nesta página com a data de modificação.</p><h2>7. Contato</h2><p>Se você tiver dúvidas sobre esta Política de Privacidade, entre em contato pelo e-mail <a href=\"mailto:leo@leomurca.xyz\">leo@leomurca.xyz</a>.</p>",
"terms.of.service.title": "📝 Termos de Serviço",
"terms.of.service.update": "Última atualização: 9 de maio de 2025",
"terms.of.service.content": "<p>Bem-vindo ao <strong>Embroidery Viewer</strong> (<a href=\"https://embroideryviewer.xyz\">embroideryviewer.xyz</a>). Ao acessar ou utilizar este site, você concorda em estar vinculado aos seguintes Termos de Serviço. Se você não concordar com qualquer parte destes termos, por favor, não utilize o site.</p><h2>1. Descrição do Serviço</h2><p>O Embroidery Viewer é uma ferramenta gratuita baseada em navegador que permite aos usuários visualizar arquivos de design de bordado online. O serviço é destinado ao uso pessoal e não comercial.</p><h2>2. Uso do Serviço</h2><p>Você concorda em usar o serviço apenas para fins legais. Você é o único responsável por qualquer conteúdo (incluindo arquivos de bordado) que enviar, e confirma que tem o direito legal de usar, visualizar e processar esses arquivos.</p><p>Você concorda em não enviar arquivos que sejam ilegais, ofensivos, infrinjam direitos de propriedade intelectual ou contenham código malicioso.</p><h2>3. Processamento de Arquivos</h2><p>Os arquivos enviados para o Embroidery Viewer são processados diretamente em seu navegador ou temporariamente em nossos servidores. Os arquivos não são armazenados permanentemente, compartilhados ou backupados.</p><p>Embora tenhamos o objetivo de manter seu conteúdo seguro, você reconhece que nenhum sistema é 100% seguro e você utiliza o serviço por sua conta e risco.</p><h2>4. Sem Garantia</h2><p>Este serviço é fornecido \"como está\" e \"como disponível\", sem quaisquer garantias, expressas ou implícitas. Não garantimos que o serviço será ininterrupto, seguro ou sem erros.</p><h2>5. Limitação de Responsabilidade</h2><p>O Embroidery Viewer não será responsabilizado por quaisquer danos resultantes do uso ou da impossibilidade de usar o serviço, incluindo, mas não se limitando a, perda de dados, perda de lucros ou outros danos incidentais ou consequenciais.</p><h2>6. Modificações no Serviço</h2><p>Reservamo-nos o direito de modificar, suspender ou descontinuar o serviço a qualquer momento, sem aviso prévio. Podemos também atualizar estes Termos de Serviço de tempos em tempos. O uso contínuo do serviço após as mudanças constitui sua aceitação dos novos termos.</p><h2>7. Lei Aplicável</h2><p>Estes Termos serão regidos e interpretados de acordo com as leis do Brasil, sem levar em consideração seus princípios de conflitos de leis.</p><h2>8. Contato</h2><p>Se você tiver qualquer dúvida sobre estes Termos de Serviço, sinta-se à vontade para entrar em contato conosco pelo e-mail <a href=\"mailto:leo@leomurca.xyz\">leo@leomurca.xyz</a>.</p>",
"main.title": "Carregar arquivos",
"main.languageSwitch": "🇺🇸",
"main.fileSize": "O tamanho máximo de cada arquivo é <strong>{{fileSize}}MB</strong>.",
"main.languageSwitch": "Switch to English",
"main.fileSize": "O tamanho máximo do arquivo é <strong>{{fileSize}}kb</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.dropzone": "Arraste e solte os arquivos aqui ou clique para fazer upload.",
"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 = [];
@ -35,15 +35,14 @@
<div id="container" style="width: 100%; heigth: 100vh;">
{#each Array.from(files) as file, i}
<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>
<div class="stitches-container" bind:this={stitchesRefs[i]}></div>
<div class="size-container" bind:this={sizeRefs[i]}></div>
<div class="colors-container" bind:this={colorRefs[i]}></div>
<div class="stitches-container" bind:this={stitchesRefs[i]} />
<div class="size-container" bind:this={sizeRefs[i]} />
<div class="colors-container" bind:this={colorRefs[i]} />
<div
id="download-button"
role="button"
tabindex=0
on:keydown={onKeydown}
on:click={() => downloadCanvasAsImage(canvasRefs[i], file.name)}
>
@ -62,7 +61,7 @@
)}
{/each}
<!-- svelte-ignore a11y-missing-content -->
<h1 bind:this={errorMessageRef}></h1>
<h1 bind:this={errorMessageRef} />
</div>
{/if}
@ -84,9 +83,7 @@
max-height: 1000px;
margin-bottom: 15px;
padding: 10px;
/* border: 2px solid black;*/
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
border-radius: 16px;
border: 2px solid black;
}
.canvas {
@ -110,13 +107,10 @@
div[role="button"] {
background-color: #05345f;
font-weight: bold;
font-weight: 500;
color: white;
padding: 10px;
border-radius: 10px;
padding: 10px;
width: 50%;
text-align: center;
border-radius: 0;
}
div[role="button"]:hover {
@ -134,10 +128,5 @@
#container {
width: 100%;
}
div[role="button"] {
width: 100%;
padding: 15px;
}
}
</style>

View file

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

View file

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

View file

@ -2,17 +2,18 @@
import CardList from "./CardList.svelte";
import Dropzone from "./Dropzone.svelte";
import FileList from "./FileList.svelte";
import LanguageIcon from './LanguageIcon.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, locale, locales } from "../i18n"
let acceptedFiles;
let rejectedFiles;
let areAcceptedFilesRendered = false;
const fileRequirements = {
supportedFormats: Object.values(supportedFormats).map((f) => f.ext),
maxSize: 1000000,
maxSize: 700000,
};
const onSubmit = () => {
@ -46,6 +47,11 @@
}
};
const onSwitchToOppositeLang = () => {
const oppositeLang = locales.find(item => item[0] !== $locale);
locale.set(oppositeLang[0]);
}
</script>
<form
@ -55,9 +61,17 @@
>
<div class="title-container">
<h2>{$t("main.title")}</h2>
<a class="common-switch {$locale === 'en' ? 'portuguese-switch' : 'english-switch' }" href="#" on:click|preventDefault={onSwitchToOppositeLang}>
<div style="display: flex; width: fit-content;">
<div class="language-icon">
<LanguageIcon cssClass="{$locale === 'en' ? 'portuguese-switch' : 'english-switch' }" />
</div>
<span>{$t("main.languageSwitch")}</span>
</div>
</a>
</div>
<p>
{@html $t("main.fileSize", { fileSize: fileRequirements.maxSize / 1000000 })}
{@html $t("main.fileSize", { fileSize: fileRequirements.maxSize / 1000 })}
{@html $t("main.supportedFormats", { supportedFormats: fileRequirements.supportedFormats.join(", ") })}
</p>
@ -70,9 +84,7 @@
{onChange}
/>
<input id="submit" type="submit" value={$t("main.render")} />
<p class="disclaimer"><em>Do not upload copyrighted material you do not own or have rights to.</em></p>
<input type="submit" value={$t("main.render")} />
</form>
{#if areAcceptedFilesRendered}
@ -83,28 +95,47 @@
{/if}
<style>
form {
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
}
.disclaimer {
font-size: 13px;
text-align: center;
.language-icon {
width: 30px;
height: 24px;
}
@media only screen and (max-device-width: 768px) {
.common-switch {
width: fit-content;
}
.portuguese-switch {
color: #0C8F27;
border-bottom: 3px solid #0C8F27;
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 only screen and (max-device-width: 812px) {
#form {
width: 100%;
}

18
src/lib/Footer.svelte Normal file
View file

@ -0,0 +1,18 @@
<script>
import { t } from "../i18n"
import { appVersion } from "../utils/env";
</script>
<footer>
<p>{@html $t("main.copyright", { year: new Date().getFullYear(), website: "https://leomurca.xyz" })}</p>
<p>{@html $t("main.version", { version: appVersion() })}</p>
</footer>
<style>
footer {
bottom: 0;
}
p {
text-align: center;
}
</style>

42
src/lib/Header.svelte Normal file
View 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>

View file

@ -0,0 +1,63 @@
<script>
export let cssClass;
</script>
<svg version="1.1" id="Optimized" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 841.8897705 595.2755737" style="enable-background:new 0 0 841.8897705 595.2755737;" xml:space="preserve">
<polygon class={cssClass} style="fill: #020203;" points="512.5472412,488.2385864 542.3688354,537.3009033 558.0941772,491.724762 "/>
<path class={cssClass} style="fill:#020203;" d="M277.7966003,213.2223663c-1.1176147-1.0975342,1.4554749,8.9684143,5.0365906,12.5897064
c6.3496399,6.4062805,11.3096008,7.23172,13.9502563,7.3376465c5.8437805,0.2337646,13.0553894-1.4554596,17.3377991-3.2506104
c4.1435852-1.767746,11.404541-5.4748993,14.1529236-10.8822479c0.5825195-1.1559601,2.1731262-3.0972137,1.1742859-7.8927765
c-0.7579041-3.6888733-3.1064148-4.980011-5.9698181-4.7754517c-2.8634644,0.1935883-11.5323792,2.5055237-15.7253113,3.7948151
c-4.1947021,1.2728424-12.8344116,3.9025269-16.6000366,4.7188416
C287.3968811,215.6767426,279.1151123,214.4842987,277.7966003,213.2223663z"/>
<path class={cssClass} style="fill: #020203;" d="M383.9637451,333.5533752c-1.6582031-0.6026001-35.9649353-14.8140564-40.828064-17.1424255
c-3.979248-1.9137573-13.7365417-6.0391541-18.3276062-7.9127808c12.9312439-19.9383545,21.0942688-34.984314,22.1808472-37.276123
c2.0106506-4.1929321,15.697876-30.9757843,16.0174561-32.6248322c0.3104858-1.6709595,0.6994324-7.8434601,0.3981323-9.3098907
c-0.3013-1.495636-5.3196411,1.3787689-12.1331482,3.6889343c-6.8244629,2.3009491-19.7940674,10.7361145-24.8032837,11.7934723
c-5.0274658,1.048233-21.0942688,7.134964-29.3157349,9.8631897c-8.2214966,2.7283783-23.7732849,7.4745941-30.1704102,9.2021637
c-6.4062805,1.7275696-11.9980316,1.8645325-15.5810242,2.9511414c0,0,0.4766541,5.0183411,1.4280701,6.5231934
c0.9404755,1.5046997,4.329895,5.193634,8.270813,6.2235718c3.9408875,1.037262,10.4640198,0.6208801,13.4352112-0.0584412
c2.9693909-0.6903381,8.1137695-3.203125,8.8040161-4.3006287c0.6976929-1.1158447-0.3596802-4.5527039,0.8144836-5.5917969
c1.1852417-1.0281067,16.8429565-4.6878052,22.754303-6.4738464c5.911377-1.8170166,28.5396118-9.6112061,31.6076355-9.2130585
c-0.9715576,3.2231903-19.1731262,39.2757416-25.0351562,50.0319366
c-5.8639221,10.7544556-39.9259338,58.0690613-47.1777039,66.4074402
c-5.5041504,6.3386841-18.8425598,22.5588379-23.4628143,26.2185059c1.1650848,0.3214722,9.4249115-0.3870544,10.9296875-1.3184509
c9.3774872-5.7762756,24.9968414-25.219635,30.0261078-31.1419983
c14.9492188-17.5313721,28.0831604-35.9465942,38.4978638-51.7503967h0.0109558
c2.0288696,0.8454895,18.4335022,14.2113342,22.7140503,17.1734619c4.2806396,2.9602051,21.172821,12.3851318,24.832489,13.9483643
c3.6596985,1.5832825,17.7249756,8.0681152,18.3166809,5.8730469
C388.7592468,347.1237793,385.6236877,334.1834412,383.9637451,333.5533752z"/>
<path class={cssClass} style="fill-rule:evenodd;clip-rule:evenodd;fill:#020203;" d="M304.6889954,512.2145996
c3.2871399,2.0087891,6.3916626,3.6523438,9.8614197,5.2958984c6.9394836,3.4697876,14.7920837,7.1221313,22.2794495,9.8614502
c10.2266541,3.8349609,20.4532776,6.9395142,30.6799316,9.3135376c5.6611633,1.2783203,11.8701782,2.3740234,17.8966064,3.2871094
c0.5478516,0,16.8009033,2.0087891,20.0880432,2.0087891h16.4356689c6.3916321-0.5478516,12.4180603-0.9130859,18.8096924-1.8261719
c5.1133423-0.7304688,10.7745056-1.6435547,16.2530518-2.921875c4.0176086-0.9130859,8.2178345-1.8261719,12.2354431-3.1045532
c3.8349915-1.0957031,8.2178345-2.5566406,12.4180603-4.0175781c2.7392883-0.9130859,5.6611938-2.1914062,8.5830688-3.2871094
c2.374054-1.0957642,5.2959595-2.3740845,8.0352173-3.4697876c3.2871399-1.4609375,7.1221313-3.4697266,10.7745056-5.2958984
c2.9219055-1.4609985,6.2090149-3.2871704,9.3135681-5.1133423c2.3740234-1.2783508,7.8526001-5.4785767,10.7744751-5.4785767
c3.2871094,0,5.4785767,2.9219055,5.4785767,5.4785767c0,5.2959595-7.1221313,6.9395142-10.4093018,9.3135376
c-3.4697266,2.3740234-7.6699829,4.2002563-11.3223267,6.2090454c-7.3047485,3.8349609-14.7921143,7.1221313-21.9142151,9.8613892
c-9.3135681,3.4697266-19.5401917,6.756897-28.6711121,8.9483032c-3.4697571,0.7304688-6.9395142,1.6435547-10.4092712,2.1914062
c-1.8261719,0.3652344-20.818512,3.2871704-26.1144409,3.2871704h-24.1056519
c-6.3916626-0.5478516-13.1485291-1.2783203-19.5401917-2.1914673c-5.6611633-0.9130859-11.6875916-2.0087891-17.3487549-3.2871094
c-4.382843-0.9130859-9.1309204-2.1914062-13.3311462-3.4697266c-7.3047485-2.0088501-14.4268799-4.5654907-21.366394-7.3047485
c-12.6006775-4.7481079-25.7492065-10.9571533-38.1672668-19.1749878c-2.1914062-1.4609375-2.3740234-2.921875-2.3740234-4.5654297
c0-2.7392883,2.0087891-5.2959595,5.295929-5.2959595C297.7495117,507.4664917,303.5932922,511.6667175,304.6889954,512.2145996z"/>
<g>
<path class={cssClass} d="M639.8383789,180.4025879l-40.7682495-12.975708V48.3634644
c0-3.4697266-2.5567017-5.843811-5.843811-5.843811c-2.5567017,0-84.9176636,28.3059082-91.4918823,30.6799316
c-22.3344727,7.4448242-86.798645,29.7334595-86.798645,29.7334595l-0.000061,0.0002441
c-1.0336304,0.2964478-2.6376953,0.7871704-4.7411499,1.4482422L252.854248,48.8499756
c-1.1881714-0.4193726-2.43396,0.4620972-2.43396,1.7220459v2v104.723877
c-24.4053345,8.1710205-41.8085327,14.0198364-42.7017212,14.335083c-1.6435547,0.5478516-4.2002563,0.9130859-5.6611938,2.921875
c-0.7304688,0.7304688-0.9130859,2.0088501-1.2783203,2.921936V484.456543c0,0.3652344,0.1826172,0.5478516,0.1826172,0.7304688
c1.0957031,2.3740845,3.1044922,3.835022,5.2959595,3.835022c2.7392578,0,208.3677368-69.0298462,212.9332275-70.8560181
c0.2156372-0.0718994,0.458374-0.2409668,0.6971436-0.4380493l218.84198,69.75177
c1.1779175,0.3754883,2.3807373-0.50354,2.3807373-1.7399292V182.1427612
C641.1107178,181.3475952,640.5961304,180.6437988,639.8383789,180.4025879z M410.9730225,409.4003296l-199.0542603,66.2905273
V182.0402222l199.0542603-66.2905273V409.4003296z M587.9303589,55.6682129v108.2130737l-164.4921875-52.3546143
L587.9303589,55.6682129z M567.6870728,385.267334l-10.5206299-38.4284668l-60.5161133-18.3422241l-13.0134277,31.3044434
l-29.2919922-8.8861084l62.1779785-152.5870361l28.5085449,8.6360474l51.9385376,187.1876831L567.6870728,385.267334z"/>
<polygon class={cssClass} style="fill: #020203;" points="507.8375244,300.3788452 547.7670288,312.4828491 529.5562744,247.8833618 "/>
</g>
</svg>

View file

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

View file

@ -1,29 +0,0 @@
<script>
import { t } from "../../i18n"
</script>
<section aria-labelledby="about-heading">
<h1 id="about-heading">{$t('about.title')}</h1>
{@html $t("about.content")}
</section>
<style>
section {
width: 70%;
margin: 0 auto;
}
h1 {
padding: 0;
margin-bottom: 7px;
}
@media (max-width: 768px) {
section {
width: 100%;
}
}
</style>

View file

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

View file

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

View file

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

View file

@ -1,25 +0,0 @@
<script>
import { t } from "../../i18n"
</script>
<section aria-labelledby="privacy-policy-heading">
<h1 id="privacy-policy-heading">{$t('privacy.policy.title')}</h1>
<p><em>{$t('privacy.policy.last.update')}</em></p>
{@html $t('privacy.policy.content')}
</section>
<style>
section {
width: 70%;
margin: 0 auto;
}
h2 {
font-size: 17px;
}
@media (max-width: 768px) {
section {
width: 100%;
}
}
</style>

View file

@ -1,26 +0,0 @@
<script>
import { t } from "../../i18n"
</script>
<section aria-labelledby="tos-heading">
<h1 id="tos-heading">{$t('terms.of.service.title')}</h1>
<p><em>{$t('terms.of.service.update')}</em></p>
{@html $t('terms.of.service.content')}
</section>
<style>
section {
width: 70%;
margin: 0 auto;
}
h2 {
font-size: 17px;
}
@media (max-width: 768px) {
section {
width: 100%;
}
}
</style>

View file

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

View file

@ -1,87 +0,0 @@
<script>
import { t } from "../../i18n";
import { appVersion } from "../../utils/env";
import { footerRoutes } from "../../utils/routes"
</script>
<footer>
<div class="footer-content">
<div class="footer-info">
<p>{@html $t("main.copyright", {
year: new Date().getFullYear(),
website: "https://leomurca.xyz"
})}</p>
<p>{@html $t("main.version", { version: appVersion() })}</p>
</div>
<nav class="footer-nav">
{#each Object.entries(footerRoutes) as [route, config]}
<a href={route} >{$t(config.nameKey)}</a>
{/each}
</nav>
</div>
</footer>
<style>
footer {
background-color: #f8f9fa;
border-top: 1px solid #ddd;
padding: 20px;
width: 100%;
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
max-width: 960px;
margin: 0 auto;
text-align: center;
}
.footer-info {
flex: 1 1 100%;
margin-bottom: 10px;
}
.footer-info p {
margin: 4px 0;
font-size: 14px;
color: #333;
}
.footer-info p:first-child {
font-weight: bold;
}
.footer-nav {
flex: 1 1 100%;
}
.footer-nav a {
margin: 0 10px;
font-size: 14px;
}
@media (min-width: 600px) {
.footer-content {
flex-wrap: nowrap;
text-align: left;
}
.footer-info,
.footer-nav {
flex: 1 1 50%;
margin-bottom: 0;
}
.footer-info {
text-align: left;
}
.footer-nav {
text-align: right;
}
}
</style>

View file

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

View file

@ -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).filter(r => r[1].nameKey !== undefined) 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>

View file

@ -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: 90vh;
}
</style>

View file

@ -1,9 +1,8 @@
import { mount } from 'svelte';
import App from './App.svelte';
import "./app.css";
import App from "./App.svelte";
const app = mount(App, {
target: document.getElementById('app'),
const app = new App({
target: document.getElementById("app"),
});
export default app;
export default app;

View file

@ -1,51 +0,0 @@
import Home from '../lib/pages/Home.svelte';
import Donate from '../lib/pages/Donate.svelte';
import About from '../lib/pages/About.svelte';
import PrivacyPolicy from '../lib/pages/PrivacyPolicy.svelte';
import TermsOfService from '../lib/pages/TermsOfService.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"
},
'/about': {
component: About,
nameKey: "nav.about"
},
'/privacy-policy': {
component: PrivacyPolicy,
nameKey: undefined
},
'/terms-of-service': {
component: TermsOfService,
nameKey: undefined
},
};
export const footerRoutes = {
'/about': {
component: About,
nameKey: "nav.about"
},
'/privacy-policy': {
component: PrivacyPolicy,
nameKey: "nav.privacy.policy"
},
'/terms-of-service': {
component: TermsOfService,
nameKey: "nav.terms.of.service"
},
}
export const fallback = NotFound;

View file

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