Compare commits
13 commits
redesign_a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 97477eea0a | |||
| d5415cfedd | |||
| 705ceb11e3 | |||
| cf9714fc61 | |||
| 9b1d96b153 | |||
| 0712cc2828 | |||
| e7a61c628b | |||
| e50900d5c6 | |||
| 8219607c5a | |||
| e9f5eb81dd | |||
| bd5f584afd | |||
| 18f6b1a2c2 | |||
| 7e184d16b0 |
31 changed files with 394 additions and 214 deletions
69
README.md
69
README.md
|
|
@ -1,14 +1,73 @@
|
||||||
# Embroidery Viewer
|
# 🧵 Embroidery Viewer
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
A free online tool to view embroidery files.
|
**The simplest way to preview embroidery files — instantly, in your browser.**
|
||||||
Available at https://embroideryviewer.xyz.
|
|
||||||
|
👉 **Try it now:** https://embroideryviewer.xyz
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Current supported formats: **.pes, .dst, .pec, .jef and .exp**.
|
<a href="https://buymeacoffee.com/embroideryviewerxyz">
|
||||||
|
<img src="docs/yellow-button.png" width="200" alt="Alt Text">
|
||||||
|
</a>
|
||||||
|
|
||||||
Inspired by https://github.com/redteam316/html5-embroidery.git.
|
---
|
||||||
|
|
||||||
|
## ✨ Why Embroidery Viewer?
|
||||||
|
|
||||||
|
Working with embroidery files shouldn’t require heavy, expensive software.
|
||||||
|
|
||||||
|
Embroidery Viewer was built to solve a simple problem:
|
||||||
|
|
||||||
|
> _“I just want to quickly see my design.”_
|
||||||
|
|
||||||
|
No installs. No friction. Just drag, drop, and view.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Features
|
||||||
|
|
||||||
|
- ⚡ **Instant preview** — open files in seconds
|
||||||
|
- 🔒 **Private by design** — everything runs in your browser
|
||||||
|
- 🧵 **Multiple formats supported**
|
||||||
|
- 🖥️ **Works on any device** (desktop, tablet, mobile)
|
||||||
|
- 📂 **Batch-friendly** — view multiple files in sequence
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Supported Formats
|
||||||
|
|
||||||
|
- `.pes`
|
||||||
|
- `.dst`
|
||||||
|
- `.pec`
|
||||||
|
- `.jef`
|
||||||
|
- `.exp`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 How it works
|
||||||
|
|
||||||
|
All processing happens **client-side** using modern web technologies.
|
||||||
|
Your files are never uploaded to any server.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Inspiration
|
||||||
|
|
||||||
|
Inspired by:
|
||||||
|
https://github.com/redteam316/html5-embroidery.git
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❤️ Support the project
|
||||||
|
|
||||||
|
If this tool helped you, consider supporting its development:
|
||||||
|
|
||||||
|
- Share it with others
|
||||||
|
- Give feedback
|
||||||
|
- Or contribute to the project
|
||||||
|
|
||||||
|
---
|
||||||
|
|
|
||||||
BIN
docs/yellow-button.png
Executable file
BIN
docs/yellow-button.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "embroidery-viewer",
|
"name": "embroidery-viewer",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "3.0.0",
|
"version": "3.0.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|
|
||||||
26
src/app.html
26
src/app.html
|
|
@ -14,23 +14,31 @@ Buy me a coffee and help keep it running ☕"
|
||||||
data-x_margin="18"
|
data-x_margin="18"
|
||||||
data-y_margin="18"
|
data-y_margin="18"
|
||||||
></script>
|
></script>
|
||||||
|
<!-- Basic -->
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="author" content="Embroidery Viewer" />
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Caveat:wght@400..700&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="shortcut icon" href="/favicon.ico" />
|
<link rel="shortcut icon" href="/favicon.ico" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
|
|
||||||
<link rel="canonical" href="https://embroideryviewer.xyz/" />
|
<link rel="canonical" href="https://embroideryviewer.xyz/" />
|
||||||
|
|
||||||
|
<!-- Mobile / PWA friendliness -->
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/fonts/merienda.regular.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
|
|
||||||
69
src/lib/components/AnnouncementBar.svelte
Normal file
69
src/lib/components/AnnouncementBar.svelte
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
<script>
|
||||||
|
import { t } from '$lib/translations';
|
||||||
|
|
||||||
|
const trackEvent = () => {
|
||||||
|
// @ts-ignore
|
||||||
|
window.hk?.event?.('install_now');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="bar">
|
||||||
|
<div class="content">
|
||||||
|
<p>
|
||||||
|
{$t('announcement.message')}
|
||||||
|
<a
|
||||||
|
href="https://play.google.com/store/apps/details?id=xyz.embroideryviewer.android"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="cta"
|
||||||
|
onclick={trackEvent}
|
||||||
|
>
|
||||||
|
{$t('announcement.cta-text')}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bar {
|
||||||
|
width: 100%;
|
||||||
|
background: #06345f;
|
||||||
|
color: white;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: underline;
|
||||||
|
color: #ffffff;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.content {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
89
src/lib/components/Head.svelte
Normal file
89
src/lib/components/Head.svelte
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
<script>
|
||||||
|
import { PUBLIC_IMAGE_BASE_URL } from '$env/static/public';
|
||||||
|
import { locale, t } from '$lib/translations';
|
||||||
|
import { normalizeLocaleUnderscore } from '$lib/utils/normalizeLocaleUnderscore';
|
||||||
|
import { isDevelopment } from '$lib/utils/env';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* =========================
|
||||||
|
* Props
|
||||||
|
* =========================
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {string} Page title (translation key) */
|
||||||
|
export let title;
|
||||||
|
|
||||||
|
/** @type {string} Page description (translation key) */
|
||||||
|
export let description;
|
||||||
|
|
||||||
|
/** @type {string} SEO keywords (translation key or raw string) */
|
||||||
|
export let keywords;
|
||||||
|
|
||||||
|
/** @type {string} Canonical URL (absolute) */
|
||||||
|
export let url;
|
||||||
|
|
||||||
|
/** @type {string} Open Graph type (e.g., 'website', 'article') */
|
||||||
|
export let ogType = 'website';
|
||||||
|
|
||||||
|
/** @type {string} Optional override for Open Graph description */
|
||||||
|
export let ogDescription = description;
|
||||||
|
|
||||||
|
/** @type {string} Twitter card type */
|
||||||
|
export let twitterCard = 'summary_large_image';
|
||||||
|
|
||||||
|
/** @type {boolean} Whether the page should be indexed */
|
||||||
|
export let shouldBeIndexed = !isDevelopment();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* =========================
|
||||||
|
* Derived / Computed values
|
||||||
|
* =========================
|
||||||
|
*/
|
||||||
|
|
||||||
|
let image = `${PUBLIC_IMAGE_BASE_URL}/t/f_webp/embroidery-viewer/logo-icon.webp`;
|
||||||
|
|
||||||
|
// Translations (avoid repeating $t everywhere)
|
||||||
|
$: translatedTitle = title ? $t(title) : '';
|
||||||
|
$: translatedDescription = description ? $t(description) : '';
|
||||||
|
$: translatedKeywords = keywords ? $t(keywords) : '';
|
||||||
|
|
||||||
|
// Fallbacks
|
||||||
|
$: finalOgDescription = ogDescription
|
||||||
|
? $t(ogDescription)
|
||||||
|
: translatedDescription;
|
||||||
|
|
||||||
|
// Locale formatting (e.g., en-US -> en_US)
|
||||||
|
$: ogLocale = normalizeLocaleUnderscore($locale);
|
||||||
|
|
||||||
|
// Robots directive
|
||||||
|
$: robotsContent = shouldBeIndexed ? 'index, follow' : 'noindex, nofollow';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<!-- Primary SEO -->
|
||||||
|
<title>{translatedTitle}</title>
|
||||||
|
<meta name="description" content={translatedDescription} />
|
||||||
|
<meta name="keywords" content={translatedKeywords} />
|
||||||
|
|
||||||
|
<!-- Robots -->
|
||||||
|
<meta name="robots" content={robotsContent} />
|
||||||
|
<meta name="googlebot" content={robotsContent} />
|
||||||
|
|
||||||
|
<!-- Open Graph (Facebook, LinkedIn, etc.) -->
|
||||||
|
<meta property="og:type" content={ogType} />
|
||||||
|
<meta property="og:title" content={translatedTitle} />
|
||||||
|
<meta property="og:description" content={finalOgDescription} />
|
||||||
|
<meta property="og:image" content={image} />
|
||||||
|
<meta property="og:url" content={url} />
|
||||||
|
<meta property="og:locale" content={ogLocale} />
|
||||||
|
<meta property="og:site_name" content="Embroidery Viewer" />
|
||||||
|
|
||||||
|
<!-- Twitter -->
|
||||||
|
<meta name="twitter:card" content={twitterCard} />
|
||||||
|
<meta name="twitter:title" content={translatedTitle} />
|
||||||
|
<meta name="twitter:description" content={finalOgDescription} />
|
||||||
|
<meta name="twitter:image" content={image} />
|
||||||
|
|
||||||
|
<!-- Optional: improves link previews in some platforms -->
|
||||||
|
<meta property="og:image:alt" content={translatedTitle} />
|
||||||
|
</svelte:head>
|
||||||
|
|
@ -51,7 +51,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 10px 30px 10px 100px;
|
padding: 45px 30px 10px 100px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,14 +212,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1159px) {
|
||||||
|
header {
|
||||||
|
padding-top: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
header {
|
header {
|
||||||
padding: 10px 20px;
|
padding: 110px 20px 10px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#menu {
|
#menu {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
top: -80px;
|
top: -110px;
|
||||||
margin: 0px 0 0 0;
|
margin: 0px 0 0 0;
|
||||||
right: -20px;
|
right: -20px;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
<script>
|
|
||||||
import { t } from '$lib/translations';
|
|
||||||
|
|
||||||
/** @type {string} Title of the page */
|
|
||||||
export let title;
|
|
||||||
|
|
||||||
/** @type {string} Description of the page */
|
|
||||||
export let description;
|
|
||||||
|
|
||||||
/** @type {string} SEO keywords */
|
|
||||||
export let keywords;
|
|
||||||
|
|
||||||
/** @type {string} Canonical URL of the page */
|
|
||||||
export let url;
|
|
||||||
|
|
||||||
/** @type {string} Main image URL for sharing */
|
|
||||||
export let image;
|
|
||||||
|
|
||||||
/** @type {string} Open Graph type (e.g., 'website', 'article') */
|
|
||||||
export let ogType = 'website';
|
|
||||||
|
|
||||||
/** @type {string} Open Graph title (defaults to title) */
|
|
||||||
export let ogTitle = title;
|
|
||||||
|
|
||||||
/** @type {string} Open Graph description (defaults to description) */
|
|
||||||
export let ogDescription = description;
|
|
||||||
|
|
||||||
/** @type {string} Open Graph image (defaults to image) */
|
|
||||||
export let ogImage = image;
|
|
||||||
|
|
||||||
/** @type {string} Twitter card type (e.g., 'summary_large_image') */
|
|
||||||
export let twitterCard = 'summary_large_image';
|
|
||||||
|
|
||||||
/** @type {string} Twitter title (defaults to title) */
|
|
||||||
export let twitterTitle = title;
|
|
||||||
|
|
||||||
/** @type {string} Twitter description (defaults to description) */
|
|
||||||
export let twitterDescription = description;
|
|
||||||
|
|
||||||
/** @type {string} Twitter image (defaults to image) */
|
|
||||||
export let twitterImage = image;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>{$t(title)}</title>
|
|
||||||
<meta name="description" content={$t(description)} />
|
|
||||||
<meta name="keywords" content={$t(keywords)} />
|
|
||||||
|
|
||||||
<!-- Open Graph -->
|
|
||||||
<meta property="og:type" content={ogType} />
|
|
||||||
<meta property="og:title" content={$t(ogTitle)} />
|
|
||||||
<meta property="og:description" content={$t(ogDescription)} />
|
|
||||||
<!-- <meta property="og:image" content={$t(ogImage)} /> -->
|
|
||||||
<meta property="og:url" content={$t(url)} />
|
|
||||||
|
|
||||||
<!-- Twitter -->
|
|
||||||
<!-- <meta name="twitter:card" content={$t(twitterCard)} /> -->
|
|
||||||
<meta name="twitter:title" content={$t(twitterTitle)} />
|
|
||||||
<meta name="twitter:description" content={$t(twitterDescription)} />
|
|
||||||
<!-- <meta name="twitter:image" content={$t(twitterImage)} /> -->
|
|
||||||
</svelte:head>
|
|
||||||
|
|
@ -70,8 +70,64 @@
|
||||||
.app {
|
.app {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 6rem 1.5rem;
|
padding: 6rem 1.5rem;
|
||||||
background: #ffffff;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
/* Base */
|
||||||
|
background-color: #ffffff;
|
||||||
|
|
||||||
|
/* Embroidery-inspired layers */
|
||||||
|
background-image:
|
||||||
|
/* soft radial "fabric tension" */
|
||||||
|
radial-gradient(
|
||||||
|
circle at 20% 30%,
|
||||||
|
rgba(6, 52, 95, 0.06),
|
||||||
|
transparent 60%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
circle at 80% 70%,
|
||||||
|
rgba(6, 52, 95, 0.05),
|
||||||
|
transparent 60%
|
||||||
|
),
|
||||||
|
/* diagonal "thread lines" */
|
||||||
|
repeating-linear-gradient(
|
||||||
|
45deg,
|
||||||
|
rgba(6, 52, 95, 0.04) 0px,
|
||||||
|
rgba(6, 52, 95, 0.04) 1px,
|
||||||
|
transparent 1px,
|
||||||
|
transparent 12px
|
||||||
|
),
|
||||||
|
/* opposite direction stitching */
|
||||||
|
repeating-linear-gradient(
|
||||||
|
-45deg,
|
||||||
|
rgba(6, 52, 95, 0.025) 0px,
|
||||||
|
rgba(6, 52, 95, 0.025) 1px,
|
||||||
|
transparent 1px,
|
||||||
|
transparent 14px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(6, 52, 95, 0.03) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(6, 52, 95, 0.03) 1px, transparent 1px);
|
||||||
|
|
||||||
|
background-size: 40px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blob {
|
||||||
|
position: absolute;
|
||||||
|
width: 500px;
|
||||||
|
height: 500px;
|
||||||
|
background: radial-gradient(circle, rgba(6, 52, 95, 0.15), transparent 70%);
|
||||||
|
filter: blur(80px);
|
||||||
|
top: -100px;
|
||||||
|
right: -100px;
|
||||||
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
|
|
||||||
4
src/lib/translations/en-US/announcement.json
Normal file
4
src/lib/translations/en-US/announcement.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"message": "🚀 Embroidery Viewer is now on Android — view your designs anywhere, customize thread & background colors.",
|
||||||
|
"cta-text": "Install now"
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,5 @@
|
||||||
"seo.title": "Free Online Embroidery File Viewer - Fast, Private & No Signup",
|
"seo.title": "Free Online Embroidery File Viewer - Fast, Private & No Signup",
|
||||||
"seo.description": "Upload and preview embroidery files instantly with Embroidery Viewer. Supports DST, PES, JEF, EXP, VP3 and more. No installs, no uploads – 100% browser-based and free.",
|
"seo.description": "Upload and preview embroidery files instantly with Embroidery Viewer. Supports DST, PES, JEF, EXP, VP3 and more. No installs, no uploads – 100% browser-based and free.",
|
||||||
"seo.keywords": "embroidery viewer, online embroidery viewer, embroidery file preview, DST viewer, PES viewer, free embroidery tool, JEF viewer, EXP embroidery, VP3 embroidery viewer, embroidery preview tool, browser embroidery renderer, convert embroidery to PNG",
|
"seo.keywords": "embroidery viewer, online embroidery viewer, embroidery file preview, DST viewer, PES viewer, free embroidery tool, JEF viewer, EXP embroidery, VP3 embroidery viewer, embroidery preview tool, browser embroidery renderer, convert embroidery to PNG",
|
||||||
"seo.url": "https://embroideryviewer.xyz",
|
"seo.url": "https://embroideryviewer.xyz"
|
||||||
"seo.image": "https://embroideryviewer.xyz/og/"
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,16 @@ const config = {
|
||||||
key: 'mobile',
|
key: 'mobile',
|
||||||
loader: async () => (await import('./en-US/mobile.json')).default,
|
loader: async () => (await import('./en-US/mobile.json')).default,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
locale: SUPPORTED_LOCALES.PT_BR,
|
||||||
|
key: 'announcement',
|
||||||
|
loader: async () => (await import('./pt-BR/announcement.json')).default,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
locale: SUPPORTED_LOCALES.EN_US,
|
||||||
|
key: 'announcement',
|
||||||
|
loader: async () => (await import('./en-US/announcement.json')).default,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
4
src/lib/translations/pt-BR/announcement.json
Normal file
4
src/lib/translations/pt-BR/announcement.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"message": "🚀 Embroidery Viewer agora está no Android — visualize seus designs em qualquer lugar, personalize as cores das linhas e do fundo.",
|
||||||
|
"cta-text": "Instale agora"
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,5 @@
|
||||||
"seo.title": "Visualizador de Bordado Online Grátis - Rápido, Privado e Sem Cadastro",
|
"seo.title": "Visualizador de Bordado Online Grátis - Rápido, Privado e Sem Cadastro",
|
||||||
"seo.description": "Envie e visualize arquivos de bordado instantaneamente com o Embroidery Viewer. Compatível com DST, PES, JEF, EXP, VP3 e mais. Sem instalações, sem uploads – 100% no navegador e gratuito.",
|
"seo.description": "Envie e visualize arquivos de bordado instantaneamente com o Embroidery Viewer. Compatível com DST, PES, JEF, EXP, VP3 e mais. Sem instalações, sem uploads – 100% no navegador e gratuito.",
|
||||||
"seo.keywords": "visualizador de bordado, visualizador online de bordado, visualizar arquivos de bordado, visualizar DST, visualizar PES, ferramenta gratuita de bordado, visualizador JEF, bordado EXP, visualizador VP3, pré-visualização de bordado, renderizador de bordado no navegador, converter bordado em PNG",
|
"seo.keywords": "visualizador de bordado, visualizador online de bordado, visualizar arquivos de bordado, visualizar DST, visualizar PES, ferramenta gratuita de bordado, visualizador JEF, bordado EXP, visualizador VP3, pré-visualização de bordado, renderizador de bordado no navegador, converter bordado em PNG",
|
||||||
"seo.url": "https://embroideryviewer.xyz",
|
"seo.url": "https://embroideryviewer.xyz"
|
||||||
"seo.image": "https://embroideryviewer.xyz/og/"
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
src/lib/utils/normalizeLocaleUnderscore.js
Normal file
15
src/lib/utils/normalizeLocaleUnderscore.js
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* Converts a locale string from hyphen format (e.g., "en-US")
|
||||||
|
* to underscore format (e.g., "en_US").
|
||||||
|
*
|
||||||
|
* Useful for APIs or systems that expect locales with underscores.
|
||||||
|
*
|
||||||
|
* @param {string} locale - The locale string in BCP 47 format (e.g., "en-US").
|
||||||
|
* @returns {string} The normalized locale string using underscores (e.g., "en_US").
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* normalizeLocaleUnderscore("en-US"); // "en_US"
|
||||||
|
*/
|
||||||
|
export const normalizeLocaleUnderscore = (locale) => {
|
||||||
|
return locale.split('-').join('_');
|
||||||
|
};
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
import Header from '$lib/components/Header.svelte';
|
import Header from '$lib/components/Header.svelte';
|
||||||
import Footer from '$lib/components/Footer.svelte';
|
import Footer from '$lib/components/Footer.svelte';
|
||||||
import Analytics from '$lib/components/Analytics.svelte';
|
import Analytics from '$lib/components/Analytics.svelte';
|
||||||
|
import AnnouncementBar from '$lib/components/AnnouncementBar.svelte';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
|
|
@ -24,6 +25,7 @@
|
||||||
<Analytics />
|
<Analytics />
|
||||||
|
|
||||||
{#if mounted}
|
{#if mounted}
|
||||||
|
<AnnouncementBar />
|
||||||
<Header />
|
<Header />
|
||||||
<main>
|
<main>
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
/** @type {import('./$types').PageLoad} */
|
|
||||||
export function load() {
|
|
||||||
return {
|
|
||||||
metadata: {
|
|
||||||
title: 'home.seo.title',
|
|
||||||
description: 'home.seo.description',
|
|
||||||
keywords: 'home.seo.keywords',
|
|
||||||
url: 'home.seo.url',
|
|
||||||
image: 'home.seo.image',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
<script>
|
<script>
|
||||||
import Seo from '$lib/components/Seo.svelte';
|
import Head from '$lib/components/Head.svelte';
|
||||||
import Hero from '$lib/sections/Hero.svelte';
|
import Hero from '$lib/sections/Hero.svelte';
|
||||||
import Features from '$lib/sections/Features.svelte';
|
import Features from '$lib/sections/Features.svelte';
|
||||||
import Faq from '$lib/sections/Faq.svelte';
|
import Faq from '$lib/sections/Faq.svelte';
|
||||||
import MobileApp from '$lib/sections/MobileApp.svelte';
|
import MobileApp from '$lib/sections/MobileApp.svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
|
||||||
|
|
||||||
// svelte-ignore state_referenced_locally
|
|
||||||
const metadata = data.metadata;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Seo {...metadata} />
|
<Head
|
||||||
|
title="home.seo.title"
|
||||||
|
description="home.seo.description"
|
||||||
|
keywords="home.seo.keywords"
|
||||||
|
url="home.seo.url"
|
||||||
|
/>
|
||||||
|
|
||||||
<Hero />
|
<Hero />
|
||||||
<Features />
|
<Features />
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
/** @type {import('./$types').PageLoad} */
|
|
||||||
export function load() {
|
|
||||||
return {
|
|
||||||
metadata: {
|
|
||||||
title: 'about.seo.title',
|
|
||||||
description: 'about.seo.description',
|
|
||||||
keywords: 'about.seo.keywords',
|
|
||||||
url: 'about.seo.url',
|
|
||||||
image: 'about.seo.image',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +1,23 @@
|
||||||
<script>
|
<script>
|
||||||
import DonateIcon from '$lib/components/icons/DonateIcon.svelte';
|
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import { t } from '$lib/translations';
|
|
||||||
|
|
||||||
import Seo from '$lib/components/Seo.svelte';
|
|
||||||
import { PUBLIC_IMAGE_BASE_URL } from '$env/static/public';
|
import { PUBLIC_IMAGE_BASE_URL } from '$env/static/public';
|
||||||
|
import { t } from '$lib/translations';
|
||||||
import { isMobile } from '$lib/utils/isMobile';
|
import { isMobile } from '$lib/utils/isMobile';
|
||||||
|
|
||||||
/** @type {import('./$types').PageProps} */
|
import Head from '$lib/components/Head.svelte';
|
||||||
let { data } = $props();
|
import DonateIcon from '$lib/components/icons/DonateIcon.svelte';
|
||||||
|
|
||||||
// svelte-ignore state_referenced_locally
|
|
||||||
const metadata = data.metadata;
|
|
||||||
|
|
||||||
const backgroundImage = isMobile()
|
const backgroundImage = isMobile()
|
||||||
? `${PUBLIC_IMAGE_BASE_URL}/t/f_webp/embroidery-viewer/route-wallpaper-mobile.webp`
|
? `${PUBLIC_IMAGE_BASE_URL}/t/f_webp/embroidery-viewer/route-wallpaper-mobile.webp`
|
||||||
: `${PUBLIC_IMAGE_BASE_URL}/t/f_webp,w_1920,h_1080/embroidery-viewer/route-wallpaper.webp`;
|
: `${PUBLIC_IMAGE_BASE_URL}/t/f_webp,w_1920,h_1080/embroidery-viewer/route-wallpaper.webp`;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Seo {...metadata} />
|
<Head
|
||||||
|
title="about.seo.title"
|
||||||
|
description="about.seo.description"
|
||||||
|
keywords="about.seo.keywords"
|
||||||
|
url="about.seo.url"
|
||||||
|
/>
|
||||||
|
|
||||||
<section aria-labelledby="about-heading">
|
<section aria-labelledby="about-heading">
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
/** @type {import('./$types').PageLoad} */
|
|
||||||
export function load() {
|
|
||||||
return {
|
|
||||||
metadata: {
|
|
||||||
title: 'mobile.app.privacy.policy.seo.title',
|
|
||||||
description: 'mobile.app.privacy.policy.seo.description',
|
|
||||||
keywords: 'mobile.app.privacy.policy.seo.keywords',
|
|
||||||
url: 'mobile.app.privacy.policy.seo.url',
|
|
||||||
image: 'mobile.app.privacy.policy.seo.image',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { t } from '$lib/translations';
|
import { t } from '$lib/translations';
|
||||||
import Seo from '$lib/components/Seo.svelte';
|
import Head from '$lib/components/Head.svelte';
|
||||||
|
|
||||||
/** @type {import('./$types').PageProps} */
|
|
||||||
let { data } = $props();
|
|
||||||
|
|
||||||
// svelte-ignore state_referenced_locally
|
|
||||||
const metadata = data.metadata;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Seo {...metadata} />
|
<Head
|
||||||
|
title="mobile.app.privacy.policy.seo.title"
|
||||||
|
description="mobile.app.privacy.policy.seo.description"
|
||||||
|
keywords="mobile.app.privacy.policy.seo.keywords"
|
||||||
|
url="mobile.app.privacy.policy.seo.url"
|
||||||
|
/>
|
||||||
|
|
||||||
<section aria-labelledby="privacy-policy-heading">
|
<section aria-labelledby="privacy-policy-heading">
|
||||||
<h1 id="privacy-policy-heading">{$t('mobile.app.privacy.policy.title')}</h1>
|
<h1 id="privacy-policy-heading">{$t('mobile.app.privacy.policy.title')}</h1>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
/** @type {import('./$types').PageLoad} */
|
|
||||||
export function load() {
|
|
||||||
return {
|
|
||||||
metadata: {
|
|
||||||
title: 'privacy.policy.seo.title',
|
|
||||||
description: 'privacy.policy.seo.description',
|
|
||||||
keywords: 'privacy.policy.seo.keywords',
|
|
||||||
url: 'privacy.policy.seo.url',
|
|
||||||
image: 'privacy.policy.seo.image',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { t } from '$lib/translations';
|
import { t } from '$lib/translations';
|
||||||
import Seo from '$lib/components/Seo.svelte';
|
import Head from '$lib/components/Head.svelte';
|
||||||
|
|
||||||
/** @type {import('./$types').PageProps} */
|
|
||||||
let { data } = $props();
|
|
||||||
|
|
||||||
// svelte-ignore state_referenced_locally
|
|
||||||
const metadata = data.metadata;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Seo {...metadata} />
|
<Head
|
||||||
|
title="privacy.policy.seo.title"
|
||||||
|
description="privacy.policy.seo.description"
|
||||||
|
keywords="privacy.policy.seo.keywords"
|
||||||
|
url="privacy.policy.seo.url"
|
||||||
|
/>
|
||||||
|
|
||||||
<section aria-labelledby="privacy-policy-heading">
|
<section aria-labelledby="privacy-policy-heading">
|
||||||
<h1 id="privacy-policy-heading">{$t('privacy.policy.title')}</h1>
|
<h1 id="privacy-policy-heading">{$t('privacy.policy.title')}</h1>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
/** @type {import('./$types').PageLoad} */
|
|
||||||
export function load() {
|
|
||||||
return {
|
|
||||||
metadata: {
|
|
||||||
title: 'support-us.seo.title',
|
|
||||||
description: 'support-us.seo.description',
|
|
||||||
keywords: 'support-us.seo.keywords',
|
|
||||||
url: 'support-us.seo.url',
|
|
||||||
image: 'support-us.seo.image',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -4,20 +4,19 @@
|
||||||
import { isMobile } from '$lib/utils/isMobile';
|
import { isMobile } from '$lib/utils/isMobile';
|
||||||
|
|
||||||
import BuyMeACoffeeIcon from '$lib/components/icons/BuyMeACoffeeIcon.svelte';
|
import BuyMeACoffeeIcon from '$lib/components/icons/BuyMeACoffeeIcon.svelte';
|
||||||
import Seo from '$lib/components/Seo.svelte';
|
import Head from '$lib/components/Head.svelte';
|
||||||
|
|
||||||
/** @type {import('./$types').PageProps} */
|
|
||||||
let { data } = $props();
|
|
||||||
|
|
||||||
// svelte-ignore state_referenced_locally
|
|
||||||
const metadata = data.metadata;
|
|
||||||
|
|
||||||
const backgroundImage = isMobile()
|
const backgroundImage = isMobile()
|
||||||
? `${PUBLIC_IMAGE_BASE_URL}/t/f_webp/embroidery-viewer/route-wallpaper-mobile.webp`
|
? `${PUBLIC_IMAGE_BASE_URL}/t/f_webp/embroidery-viewer/route-wallpaper-mobile.webp`
|
||||||
: `${PUBLIC_IMAGE_BASE_URL}/t/f_webp,w_1920,h_1080/embroidery-viewer/route-wallpaper.webp`;
|
: `${PUBLIC_IMAGE_BASE_URL}/t/f_webp,w_1920,h_1080/embroidery-viewer/route-wallpaper.webp`;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Seo {...metadata} />
|
<Head
|
||||||
|
title="support-us.seo.title"
|
||||||
|
description="support-us.seo.description"
|
||||||
|
keywords="support-us.seo.keywords"
|
||||||
|
url="support-us.seo.url"
|
||||||
|
/>
|
||||||
|
|
||||||
<section aria-labelledby="support-us">
|
<section aria-labelledby="support-us">
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
/** @type {import('./$types').PageLoad} */
|
|
||||||
export function load() {
|
|
||||||
return {
|
|
||||||
metadata: {
|
|
||||||
title: 'terms.of.service.seo.title',
|
|
||||||
description: 'terms.of.service.seo.description',
|
|
||||||
keywords: 'terms.of.service.seo.keywords',
|
|
||||||
url: 'terms.of.service.seo.url',
|
|
||||||
image: 'terms.of.service.seo.image',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { t } from '$lib/translations';
|
import { t } from '$lib/translations';
|
||||||
import Seo from '$lib/components/Seo.svelte';
|
import Head from '$lib/components/Head.svelte';
|
||||||
|
|
||||||
/** @type {import('./$types').PageProps} */
|
|
||||||
let { data } = $props();
|
|
||||||
|
|
||||||
// svelte-ignore state_referenced_locally
|
|
||||||
const metadata = data.metadata;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Seo {...metadata} />
|
<Head
|
||||||
|
title="terms.of.service.seo.title"
|
||||||
|
description="terms.of.service.seo.description"
|
||||||
|
keywords="terms.of.service.seo.keywords"
|
||||||
|
url="terms.of.service.seo.url"
|
||||||
|
/>
|
||||||
|
|
||||||
<section aria-labelledby="tos-heading">
|
<section aria-labelledby="tos-heading">
|
||||||
<h1 id="tos-heading">{$t('terms.of.service.title')}</h1>
|
<h1 id="tos-heading">{$t('terms.of.service.title')}</h1>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
import CardList from '$lib/components/CardList.svelte';
|
import CardList from '$lib/components/CardList.svelte';
|
||||||
import Dropzone from '$lib/components/Dropzone.svelte';
|
import Dropzone from '$lib/components/Dropzone.svelte';
|
||||||
import FileList from '$lib/components/FileList.svelte';
|
import FileList from '$lib/components/FileList.svelte';
|
||||||
import Seo from '$lib/components/Seo.svelte';
|
import Head from '$lib/components/Head.svelte';
|
||||||
|
|
||||||
import { filterFiles } from '$lib/utils/filterFiles';
|
import { filterFiles } from '$lib/utils/filterFiles';
|
||||||
import { supportedFormats } from '$lib/format-readers';
|
import { supportedFormats } from '$lib/format-readers';
|
||||||
|
|
@ -78,15 +78,14 @@
|
||||||
if (el) el.click();
|
if (el) el.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {import('./$types').PageProps} */
|
|
||||||
let { data } = $props();
|
|
||||||
|
|
||||||
// svelte-ignore state_referenced_locally
|
|
||||||
const metadata = data.metadata;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Seo {...metadata} />
|
<Head
|
||||||
|
title="viewer.seo.title"
|
||||||
|
description="viewer.seo.description"
|
||||||
|
keywords="viewer.seo.keywords"
|
||||||
|
url="viewer.seo.url"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- eslint-disable svelte/no-at-html-tags -->
|
<!-- eslint-disable svelte/no-at-html-tags -->
|
||||||
<form id="form" enctype="multipart/form-data" onsubmit={onSubmit}>
|
<form id="form" enctype="multipart/form-data" onsubmit={onSubmit}>
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
google.com, pub-5761689301112420, DIRECT, f08c47fec0942fa0
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
Loading…
Add table
Reference in a new issue