Compare commits

..

No commits in common. "main" and "migrate-to-sveltekit" have entirely different histories.

40 changed files with 225 additions and 867 deletions

View file

@ -13,24 +13,24 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up SSH
run: |
mkdir -p ~/.ssh/
echo "${{ secrets.SSH_KEY }}" > ./deploy.key
chmod 600 ./deploy.key
echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
- name: Use Node.js 19
uses: actions/setup-node@v4
with:
node-version: 19
- name: Create env file
run: |
touch .env
echo EMAIL_ACCESS_KEY=${{ secrets.EMAIL_ACCESS_KEY }} >> .env
echo EMAIL_BASE_URL=${{ secrets.EMAIL_BASE_URL }} >> .env
- name: Verify .env file creation
run: cat .env
env:
SSH_PRIVATE_KEY: ${{secrets.SSH_KEY}}
SSH_KNOWN_HOSTS: ${{secrets.SSH_KNOWN_HOSTS}}
- name: Install PM2
run: npm i -g pm2
- name: Add Deploy Key to SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_KEY }}" >> ./deploy.key
sudo chmod 600 ./deploy.key
echo "${{ secrets.SSH_KNOWN_HOSTS}}" > ~/.ssh/known_hosts
- name: Deploy
run: env $(cat .env | grep -v \"#\" | xargs) pm2 deploy ecosystem.config.cjs production
run: pm2 deploy ecosystem.config.cjs production

65
.gitignore vendored
View file

@ -8,68 +8,3 @@ node_modules
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
deploy.key
vite: {
server: {
port: 7280,
},
},
server {
if ($host = www.embroideryviewer.xyz) {
return 301 https://$host$request_uri;
} # managed by Certbot
if ($host = embroideryviewer.xyz) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
listen [::]:80;
server_name embroideryviewer.xyz www.embroideryviewer.xyz;
location / {
proxy_pass http://embroidery; # Replace with actual port
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
server {
listen 443 ssl;
server_name embroideryviewer.xyz www.embroideryviewer.xyz;
location / {
proxy_pass http://embroidery; # Replace with actual port
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
ssl_certificate /etc/letsencrypt/live/embroideryviewer.xyz/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/embroideryviewer.xyz/privkey.pem; # managed by Certbot
}
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 444; # Or return 404;
}

View file

@ -1,14 +1,38 @@
# Embroidery Viewer
# sv
![Logo](/logo.webp)
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
![Deploy workflow status](https://git.leomurca.xyz/leomurca/embroidery-viewer/actions/workflows/deploy.yml/badge.svg)
## Creating a project
A free online tool to view embroidery files.
Available at https://embroideryviewer.xyz.
If you're seeing this, you've probably already done this step. Congrats!
![Demo](/demo.gif)
```bash
# create a new project in the current directory
npx sv create
Current supported formats: **.pes, .dst, .pec, .jef and .exp**.
# create a new project in my-app
npx sv create my-app
```
Inspired by https://github.com/redteam316/html5-embroidery.git.
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

BIN
demo.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 MiB

View file

@ -1,7 +1,7 @@
module.exports = {
apps: [
{
name: 'embroidery-viewer-prod',
name: 'embroidery-viewer',
script: './build/index.js',
time: true,
instances: 1,
@ -10,10 +10,7 @@ module.exports = {
watch: false,
max_memory_restart: '1G',
env: {
EMAIL_ACCESS_KEY: process.env.EMAIL_ACCESS_KEY,
EMAIL_BASE_URL: process.env.EMAIL_BASE_URL,
NODE_ENV: 'production',
PORT: 7281,
},
},
],
@ -27,11 +24,9 @@ module.exports = {
path: '/home/deployer/embroidery-viewer',
'pre-deploy': 'rm package-lock.json && npm i',
'post-deploy':
'npm run build && pm2 reload ecosystem.config.cjs --only embroidery-viewer-prod --env production && pm2 save',
'npm run build && pm2 reload ecosystem.config.cjs --only acelera-alagoas-prod --env production && pm2 save',
env: {
EMAIL_ACCESS_KEY: process.env.EMAIL_ACCESS_KEY,
EMAIL_BASE_URL: process.env.EMAIL_BASE_URL,
PORT: 7281,
PORT: 7017,
NODE_ENV: 'production',
},
},

BIN
logo.webp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

293
package-lock.json generated
View file

@ -8,13 +8,13 @@
"name": "embroidery-viewer",
"version": "0.0.1",
"dependencies": {
"@sveltejs/adapter-node": "^5.2.12",
"accept-language-parser": "^1.5.0",
"sveltekit-i18n": "^2.4.2"
},
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"eslint": "^9.28.0",
@ -49,6 +49,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -65,6 +66,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -81,6 +83,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -97,6 +100,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -113,6 +117,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -129,6 +134,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -145,6 +151,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -161,6 +168,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -177,6 +185,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -193,6 +202,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -209,6 +219,7 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -225,6 +236,7 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -241,6 +253,7 @@
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -257,6 +270,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -273,6 +287,7 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -289,6 +304,7 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -305,6 +321,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -321,6 +338,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -337,6 +355,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -353,6 +372,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -369,6 +389,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -385,6 +406,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -401,6 +423,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -417,6 +440,7 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -433,6 +457,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -732,109 +757,9 @@
"version": "1.0.0-next.29",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/plugin-commonjs": {
"version": "28.0.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.3.tgz",
"integrity": "sha512-pyltgilam1QPdn+Zd9gaCfOLcnjMEJ9gV+bTw6/r73INdvzf1ah9zLIJBm+kW7R6IUFIQ1YO+VqZtYxZNWFPEQ==",
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"commondir": "^1.0.1",
"estree-walker": "^2.0.2",
"fdir": "^6.2.0",
"is-reference": "1.2.1",
"magic-string": "^0.30.3",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=16.0.0 || 14 >= 14.17"
},
"peerDependencies": {
"rollup": "^2.68.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-commonjs/node_modules/is-reference": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
"license": "MIT",
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/@rollup/plugin-json": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz",
"integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==",
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.1.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz",
"integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==",
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2",
"deepmerge": "^4.2.2",
"is-module": "^1.0.0",
"resolve": "^1.22.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.78.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
"integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz",
@ -842,6 +767,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -855,6 +781,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -868,6 +795,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -881,6 +809,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -894,6 +823,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -907,6 +837,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -920,6 +851,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -933,6 +865,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -946,6 +879,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -959,6 +893,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -972,6 +907,7 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -985,6 +921,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -998,6 +935,7 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1011,6 +949,7 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1024,6 +963,7 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1037,6 +977,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1050,6 +991,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1063,6 +1005,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1076,6 +1019,7 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1089,6 +1033,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1104,25 +1049,21 @@
"acorn": "^8.9.0"
}
},
"node_modules/@sveltejs/adapter-node": {
"version": "5.2.12",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz",
"integrity": "sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==",
"node_modules/@sveltejs/adapter-auto": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-6.0.1.tgz",
"integrity": "sha512-mcWud3pYGPWM2Pphdj8G9Qiq24nZ8L4LB7coCUckUEy5Y7wOWGJ/enaZ4AtJTcSm5dNK1rIkBRoqt+ae4zlxcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.0",
"rollup": "^4.9.5"
},
"peerDependencies": {
"@sveltejs/kit": "^2.4.0"
"@sveltejs/kit": "^2.0.0"
}
},
"node_modules/@sveltejs/kit": {
"version": "2.21.1",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.21.1.tgz",
"integrity": "sha512-vLbtVwtDcK8LhJKnFkFYwM0uCdFmzioQnif0bjEYH1I24Arz22JPr/hLUiXGVYAwhu8INKx5qrdvr4tHgPwX6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sveltejs/acorn-typescript": "^1.0.5",
@ -1154,6 +1095,7 @@
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.0.3.tgz",
"integrity": "sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sveltejs/vite-plugin-svelte-inspector": "^4.0.1",
@ -1175,6 +1117,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz",
"integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.3.7"
@ -1207,6 +1150,7 @@
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
@ -1222,12 +1166,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"license": "MIT"
},
"node_modules/accept-language-parser": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/accept-language-parser/-/accept-language-parser-1.5.0.tgz",
@ -1404,12 +1342,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
"license": "MIT"
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -1421,6 +1353,7 @@
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@ -1458,6 +1391,7 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@ -1482,6 +1416,7 @@
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@ -1491,12 +1426,14 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz",
"integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==",
"dev": true,
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
"integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@ -1756,12 +1693,6 @@
"node": ">=4.0"
}
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@ -1797,6 +1728,7 @@
"version": "6.4.5",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz",
"integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"picomatch": "^3 || ^4"
@ -1862,6 +1794,7 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@ -1872,15 +1805,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@ -1917,18 +1841,6 @@
"node": ">=8"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -1966,21 +1878,6 @@
"node": ">=0.8.19"
}
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -2004,12 +1901,6 @@
"node": ">=0.10.0"
}
},
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"license": "MIT"
},
"node_modules/is-reference": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
@ -2074,6 +1965,7 @@
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@ -2165,6 +2057,7 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@ -2174,6 +2067,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@ -2183,12 +2077,14 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
@ -2293,22 +2189,18 @@
"node": ">=8"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@ -2321,6 +2213,7 @@
"version": "8.5.4",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz",
"integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==",
"dev": true,
"funding": [
{
"type": "opencollective",
@ -2514,26 +2407,6 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@ -2548,6 +2421,7 @@
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz",
"integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.7"
@ -2587,6 +2461,7 @@
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
"dev": true,
"license": "MIT",
"dependencies": {
"mri": "^1.1.0"
@ -2612,6 +2487,7 @@
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"dev": true,
"license": "MIT"
},
"node_modules/shebang-command": {
@ -2641,6 +2517,7 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz",
"integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@polka/url": "^1.0.0-next.24",
@ -2655,6 +2532,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@ -2686,18 +2564,6 @@
"node": ">=8"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/svelte": {
"version": "5.33.13",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.13.tgz",
@ -2796,6 +2662,7 @@
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.4.4",
@ -2812,6 +2679,7 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@ -2865,6 +2733,7 @@
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
@ -2939,6 +2808,7 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz",
"integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==",
"dev": true,
"license": "MIT",
"workspaces": [
"tests/deps/*",
@ -2983,6 +2853,7 @@
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,

View file

@ -1,7 +1,7 @@
{
"name": "embroidery-viewer",
"private": true,
"version": "2.1.3",
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
@ -16,6 +16,7 @@
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"eslint": "^9.28.0",
@ -30,7 +31,6 @@
"vite": "^6.2.6"
},
"dependencies": {
"@sveltejs/adapter-node": "^5.2.12",
"accept-language-parser": "^1.5.0",
"sveltekit-i18n": "^2.4.2"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

View file

@ -1,13 +0,0 @@
<script>
import { isDevelopment } from '$lib/utils/env';
</script>
<svelte:head>
{#if !isDevelopment()}
<script
defer
src="https://umami.leomurca.xyz/script.js"
data-website-id="bd4c0533-36e6-402d-ac04-577993aaf43a"
></script>
{/if}
</svelte:head>

View file

@ -2,30 +2,17 @@
import { t } from '$lib/translations';
import renderFileToCanvas from '$lib/file-renderer';
/**
* @type {ArrayLike<any>}
*/
export let files = [];
/**
* @type {HTMLElement}
*/
let errorMessageRef;
let canvasRefs = [];
let colorRefs = [];
let stitchesRefs = [];
let sizeRefs = [];
let errorMessageRef;
let localizedStrings = {
stitches: $t('viewer.stitches'),
dimensions: $t('viewer.dimensions'),
};
/**
* Downloads a given HTMLCanvasElement as a PNG image.
*
* @param {HTMLCanvasElement} canvas - The canvas element to export as an image.
* @param {string} filename - The desired name of the downloaded file (extension will be replaced with `.png`).
*/
const downloadCanvasAsImage = (canvas, filename) => {
const image = canvas
.toDataURL('image/png')
@ -37,15 +24,9 @@
link.click();
};
/**
* Cliks the button to render the files when user press enter.
*
* @param {KeyboardEvent} evt - The event that triggered the language switch.
*/
const onKeydown = (evt) => {
if (evt.key === 'Enter') {
const button = document.getElementById('download-button');
if (button) button.click();
document.getElementById('download-button').click();
}
};
</script>
@ -63,8 +44,8 @@
id="download-button"
role="button"
tabindex="0"
onkeydown={onKeydown}
onclick={() => downloadCanvasAsImage(canvasRefs[i], file.name)}
on:keydown={onKeydown}
on:click={() => downloadCanvasAsImage(canvasRefs[i], file.name)}
>
{$t('viewer.download')}
</div>

View file

@ -11,7 +11,6 @@
</script>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
id="dropzone"
tabindex={0}

View file

@ -1,8 +1,5 @@
<script>
export let title;
/**
* @type {ArrayLike<any>}
*/
export let files = [];
export let isError = false;
</script>

View file

@ -1,28 +1,15 @@
<script>
import { t, locale, SUPPORTED_LOCALES } from '$lib/translations';
import { t, locale, locales, SUPPORTED_LOCALES } from '$lib/translations';
import logo from '$lib/assets/logo.webp';
import MediaQuery from './MediaQuery.svelte';
/**
*
* Returns logo image configuration based on the screen size match.
*
* @param {boolean} matches - The event that triggered the language switch.
*/
const configsFor = (matches) => {
const configsFor = (/** @type {boolean} */ matches) => {
return matches
? { src: logo, width: 150, height: 70 } // mobile
: { src: logo, width: 150, height: 100 }; // desktop
};
/**
* Switches the current locale to the opposite language (EN_US <-> PT_BR).
* Prevents the default link behavior (e.g., page jump).
*
* @param {MouseEvent | KeyboardEvent} e - The event that triggered the language switch.
*/
const onSwitchToOppositeLang = (e) => {
e.preventDefault();
const onSwitchToOppositeLang = () => {
$locale =
$locale === SUPPORTED_LOCALES.EN_US
? SUPPORTED_LOCALES.PT_BR
@ -51,7 +38,7 @@
<MediaQuery query="(max-width: 768px)" let:matches>
<slot let-matches>
{#if matches}
<button class="hamburger" onclick={() => (isMenuOpen = !isMenuOpen)}>
<button class="hamburger" on:click={() => (isMenuOpen = !isMenuOpen)}>
{#if isMenuOpen}x{:else}{/if}
</button>
{/if}
@ -74,22 +61,17 @@
</ul>
</nav>
<button
type="button"
<a
class="common-switch {$locale === SUPPORTED_LOCALES.EN_US
? 'portuguese-switch'
: 'english-switch'}"
onclick={onSwitchToOppositeLang}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
onSwitchToOppositeLang(e);
}
}}
href="#"
on:click|preventDefault={onSwitchToOppositeLang}
>
<div style="display: flex; width: fit-content;">
<span style="font-size: 20px;">{$t('header.languageSwitch')}</span>
</div>
</button>
</a>
</div>
</header>
@ -148,10 +130,7 @@
}
.common-switch {
border: none;
background-color: unset;
width: fit-content;
cursor: pointer;
}
.portuguese-switch {

View file

@ -50,12 +50,12 @@
<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)} />
<meta property="og:image" content={ogImage} />
<meta property="og:url" content={url} />
<!-- Twitter -->
<!-- <meta name="twitter:card" content={$t(twitterCard)} /> -->
<meta name="twitter:title" content={$t(twitterTitle)} />
<meta name="twitter:card" content={twitterCard} />
<meta name="twitter:title" content={twitterTitle} />
<meta name="twitter:description" content={$t(twitterDescription)} />
<!-- <meta name="twitter:image" content={$t(twitterImage)} /> -->
<meta name="twitter:image" content={twitterImage} />
</svelte:head>

View file

@ -12,7 +12,7 @@
"paypal.link": "Open Donation link",
"seo.title": "💖 Donate Support Embroidery Viewer",
"seo.description": "Help keep Embroidery Viewer free and improving by making a donation. Choose from Bitcoin, Monero, PayPal, or other secure options to support ongoing development and hosting.",
"seo.keywords": "donate embroidery viewer, support embroidery viewer, embroidery viewer donations, help embroidery viewer, fund embroidery viewer, bitcoin donation embroidery, monero donation embroidery, paypal donation embroidery",
"keywords": "donate embroidery viewer, support embroidery viewer, embroidery viewer donations, help embroidery viewer, fund embroidery viewer, bitcoin donation embroidery, monero donation embroidery, paypal donation embroidery",
"url": "https://embroideryviewer.xyz/donate",
"image": "https://embroideryviewer.xyz/og/donate.png"
}

View file

@ -18,14 +18,5 @@
"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.url": "https://embroideryviewer.xyz",
"seo.image": "https://embroideryviewer.xyz/og/",
"banner.title": "🚀 Be a Beta Tester",
"banner.subtitle": "Were launching the <strong style=\"color: white\">Embroidery Viewer Android App </strong> — and you can be one of the first to try it!",
"banner.description": "Enjoy a smooth, ad-free experience to visualize PES, JEF, PEC, VP3, DST and EXP embroidery files right from your phone. Help us improve it and get early access before everyone else.",
"banner.name": "Name",
"banner.email": "Google Account Email (used for accessing the Play Store)",
"banner.cta": "Join the Beta",
"banner.cta.loading": "Joining...",
"banner.feedback.success": "Name and email sent successfully! We'll contact you soon!",
"banner.feedback.error": "Something went wrong. Try sending you name and e-mail directly to leo@leomurca.xyz."
"seo.image": "https://embroideryviewer.xyz/og/"
}

View file

@ -1,10 +0,0 @@
{
"title": "🔐 Privacy Policy",
"last.update": "Last updated: Jul 11, 2025",
"content": "<h1>Privacy Policy</h1><p>Thank you for using the Embroidery Viewer Companion app. Your privacy is important to us. This Privacy Policy explains how we handle your data.</p><h2>1. No Personal Data Collected</h2><p>The app does not collect, store, or transmit any personal data. All your embroidery files are processed locally on your device. We do not access or send your files anywhere.</p><h2>2. Storage Permissions</h2><p>The app requests access to your files only so that you can open and view embroidery files stored on your device. This permission is used solely for that purpose.</p><h2>3. Name Personalization</h2><p>If you choose to enter your name, it is stored locally on your device to personalize greetings (like \"Good morning, Leo\"). This information is not shared and is never sent over the internet.</p><h2>4. No Analytics or Advertising</h2><p>This app does not use any analytics tools or display ads. We believe in a distraction-free and respectful experience.</p><h2>5. Questions</h2><p>If you have any questions about this policy or the app, feel free to contact us at: <a href=\"mailto:leo@leomurca.xyz\">leo@leomurca.xyz</a></p>",
"seo.title": "🔐 Privacy Policy - Embroidery Viewer Companion App",
"seo.description": "Learn how Embroidery Viewer Companion App respects your privacy. No personal data collected, files processed locally or temporarily, anonymous analytics only, no trackers used.",
"seo.keywords": "privacy policy, data protection, embroidery viewer privacy, file uploads privacy, anonymous analytics, no cookies, user privacy, privacy-friendly analytics, data security, embroideryviewer.xyz",
"seo.url": "https://embroideryviewer.xyz/mobile-app/privacy-policy",
"seo.image": "https://embroideryviewer.xyz/og/privacy-policy.png"
}

View file

@ -16,7 +16,7 @@ export const SUPPORTED_LOCALES = Object.freeze({
/** @type {import('sveltekit-i18n').Config} */
const config = {
initLocale: SUPPORTED_LOCALES.EN_US,
initLocale: navigator.language,
fallbackLocale: SUPPORTED_LOCALES.EN_US,
loaders: [
{
@ -52,12 +52,6 @@ const config = {
key: 'privacy.policy',
routes: ['/privacy-policy'],
loader: async () => (await import('./en-US/privacy-policy.json')).default,
},
{
locale: SUPPORTED_LOCALES.EN_US,
key: 'mobile.app.privacy.policy',
routes: ['/mobile-app/privacy-policy'],
loader: async () => (await import('./en-US/mobile-app-privacy-policy.json')).default,
},
{
locale: SUPPORTED_LOCALES.EN_US,
@ -106,12 +100,6 @@ const config = {
routes: ['/privacy-policy'],
loader: async () => (await import('./pt-BR/privacy-policy.json')).default,
},
{
locale: SUPPORTED_LOCALES.PT_BR,
key: 'mobile.app.privacy.policy',
routes: ['/mobile-app/privacy-policy'],
loader: async () => (await import('./pt-BR/mobile-app-privacy-policy.json')).default,
},
{
locale: SUPPORTED_LOCALES.PT_BR,
key: 'terms.of.service',
@ -139,10 +127,7 @@ export const {
} = new i18n(config);
locale.subscribe(($locale) => {
if (typeof localStorage !== 'undefined' && $locale) {
const existing = localStorage.getItem('locale');
if (existing !== $locale) {
localStorage.setItem('locale', $locale);
}
if (typeof document !== 'undefined') {
document.cookie = `locale=${$locale}; path=/; SameSite=Strict;`;
}
});

View file

@ -18,14 +18,5 @@
"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.url": "https://embroideryviewer.xyz",
"seo.image": "https://embroideryviewer.xyz/og/",
"banner.title": "🚀 Seja um Testador Beta",
"banner.subtitle": "Estamos lançando o aplicativo <strong style=\"color: white\">Embroidery Viewer para Android</strong> — e você pode ser um dos primeiros a experimentá-lo!",
"banner.description": "Desfrute de uma experiência fluida e sem anúncios para visualizar arquivos de bordado PES, JEF, PEC, VP3, DST e EXP diretamente do seu celular. Ajude-nos a melhorá-lo e obtenha acesso antecipado antes de todo mundo.",
"banner.name": "Nome",
"banner.email": "E-mail da sua conta Google (usada na Play Store)",
"banner.cta": "Seja um Testador",
"banner.cta.loading": "Enviando...",
"banner.feedback.success": "Nome e e-mail enviados com sucesso! Entraremos em contato em breve!",
"banner.feedback.error": "Algo deu errado. Tente enviar seu nome e e-mail diretamente para leo@leomurca.xyz."
"seo.image": "https://embroideryviewer.xyz/og/"
}

View file

@ -1,10 +0,0 @@
{
"title": "🔐 Política de Privacidade",
"last.update": "Última atualização: 11 de julho de 2025",
"content": "<h1>Política de Privacidade</h1><p>Obrigado por usar o aplicativo Embroidery Viewer Companion. Sua privacidade é importante para nós. Esta Política de Privacidade explica como lidamos com seus dados.</p><h2>1. Nenhum dado pessoal coletado</h2><p>O aplicativo não coleta, armazena ou transmite nenhum dado pessoal. Todos os seus arquivos de bordado são processados localmente no seu dispositivo. Não acessamos nem enviamos seus arquivos para lugar algum.</p><h2>2. Permissões de armazenamento</h2><p>O aplicativo solicita acesso aos seus arquivos apenas para que você possa abrir e visualizar arquivos de bordado armazenados no seu dispositivo. Essa permissão é usada exclusivamente para esse fim.</p><h2>3. Personalização com nome</h2><p>Se você optar por informar seu nome, ele será armazenado localmente no seu dispositivo para personalizar as saudações (como \"Bom dia, Leo\"). Esta informação não é compartilhada e nunca é enviada pela internet.</p><h2>4. Sem análises ou anúncios</h2><p>Este aplicativo não utiliza ferramentas de análise nem exibe anúncios. Acreditamos em uma experiência respeitosa e sem distrações.</p><h2>5. Dúvidas</h2><p>Se você tiver alguma dúvida sobre esta política ou sobre o aplicativo, entre em contato conosco pelo e-mail: <a href=\"mailto:leo@leomurca.xyz\">leo@leomurca.xyz</a></p>",
"seo.title": "🔐 Política de Privacidade - Embroidery Viewer Companion App",
"seo.description": "Saiba como o Embroidery Viewer respeita sua privacidade. Nenhum dado pessoal é coletado, arquivos processados localmente ou temporariamente, análises anônimas, sem cookies ou rastreadores.",
"seo.keywords": "política de privacidade, proteção de dados, privacidade embroidery viewer, upload de arquivos, análises anônimas, sem cookies, privacidade do usuário, análises que respeitam a privacidade, segurança de dados, embroideryviewer.xyz",
"seo.url": "https://embroideryviewer.xyz/mobile-app/privacy-policy",
"seo.image": "https://embroideryviewer.xyz/og/privacy-policy.png"
}

View file

@ -9,7 +9,6 @@
"selected": "Arquivos selecionados",
"rejected": "Arquivos recusados",
"stitches": "Pontos",
"dimensions": "Dimensões (x, y)",
"download": "Baixar imagem",
"warning.copyright": "Não carregue material protegido por direitos autorais que você não possui ou sobre os quais não tenha direitos.",
"seo.title": "🧵 Visualizador Online Gratuito de Arquivos de Bordado Rápido, Privado e Sem Cadastro",

View file

@ -4,8 +4,4 @@ function appVersion() {
return APP_VERSION;
}
function isDevelopment() {
return import.meta.env.MODE === 'development';
}
export { appVersion, isDevelopment };
export { appVersion };

View file

@ -1,51 +1,17 @@
import { browser } from '$app/environment';
import { loadTranslations, setLocale, setRoute } from '$lib/translations';
import { SUPPORTED_LOCALES } from '$lib/translations';
import { setLocale, setRoute } from '$lib/translations';
/**
* Type guard that checks if a string is a supported locale.
* @param {string | null} locale
* @returns {locale is "en-US" | "pt-BR"}
* @typedef {Object} LayoutData
* @property {string} route
* @property {string} language
*/
function isSupportedLocale(locale) {
// @ts-ignore
return locale !== null && Object.values(SUPPORTED_LOCALES).includes(locale);
}
/**
* Client-side load function to initialize translations based on localStorage or server fallback.
*
* This function runs in the browser and:
* - Prioritizes locale from localStorage (if valid)
* - Falls back to the server-provided fallbackLanguage (from Accept-Language)
* - Initializes translations and routing
*
* @type {import('@sveltejs/kit').Load}
*/
export const load = async ({ data, url }) => {
/** @type {string} */
const route = url.pathname;
/** @type {import('@sveltejs/kit').Load<LayoutData>} */
export const load = async ({ data }) => {
const { route, language } = data ?? {};
/** @type {"en-US" | "pt-BR"} */
let language;
if (route) await setRoute(route);
if (language) await setLocale(language);
if (browser) {
/**
* Locale stored in the browser, if any.
* @type {string | null}
*/
const stored = localStorage.getItem('locale');
if (isSupportedLocale(stored)) {
language = stored; // Type narrowed here
} else {
language = data?.fallbackLanguage ?? SUPPORTED_LOCALES.EN_US;
}
await loadTranslations(language, route);
await setLocale(language);
await setRoute(route);
}
return {};
return data ?? {};
};

View file

@ -1,44 +1,54 @@
import { parse } from 'accept-language-parser';
import { loadTranslations, setLocale, setRoute } from '$lib/translations';
import { SUPPORTED_LOCALES } from '$lib/translations';
/**
* A Set of all supported locale codes for quick validation.
* A set of all supported locale codes, used to validate and match against
* user preferences from cookies or Accept-Language headers. We're using a
* Set for better performance in lookup.
*
* Example values: "en-US", "pt-BR"
* @type {Set<string>}
*/
const SUPPORTED_LOCALE_SET = new Set(Object.values(SUPPORTED_LOCALES));
/**
* Extracts the best matching locale from an Accept-Language HTTP header.
*
* @param {string | null} header - The Accept-Language header value.
* @returns {string | null} - A supported locale string like "en-US", or null if none matched.
* Returns a valid locale from cookies, or null if not valid/found.
* @param {{ get: (cookies: string) => any; }} cookies
*/
function localeFromHeader(header) {
if (!header) return null;
const parsed = parse(header);
for (const { code, region } of parsed) {
const locale = region ? `${code}-${region}` : code;
if (SUPPORTED_LOCALE_SET.has(locale)) return locale;
}
return null;
function localeFromCookies(cookies) {
const locale = cookies.get('locale');
return locale && SUPPORTED_LOCALE_SET.has(locale) ? locale : null;
}
/**
* Server-side load function that returns the initial route and fallback language.
* The language is inferred from the Accept-Language header.
* `localStorage` will take precedence on the client.
*
* @type {import('@sveltejs/kit').ServerLoad}
* Parses the Accept-Language header and returns the best matching locale.
* @param {string | null | undefined} header
*/
export async function load({ url, request }) {
/** @type {string} */
function localeFromHeader(header) {
if (!header) return null;
const parsedLanguages = parse(header);
for (const { code, region } of parsedLanguages) {
const locale = region ? `${code}-${region}` : code;
if (SUPPORTED_LOCALE_SET.has(locale)) {
return locale;
}
}
return null;
}
/** @type {import('@sveltejs/kit').ServerLoad}*/
export async function load({ url, request, cookies }) {
const cookieLocale = localeFromCookies(cookies);
const headerLocale = localeFromHeader(request.headers.get('accept-language'));
const language = cookieLocale || headerLocale || SUPPORTED_LOCALES.EN_US;
const route = url.pathname;
/** @type {string} */
const fallbackLanguage =
localeFromHeader(request.headers.get('accept-language')) ||
SUPPORTED_LOCALES.EN_US;
await loadTranslations(language, route);
setLocale(language);
setRoute(route);
return { fallbackLanguage, route };
return { language, route };
}

View file

@ -1,34 +1,18 @@
<script>
import { browser } from '$app/environment';
import { onMount } from 'svelte';
import Header from '$lib/components/Header.svelte';
import Footer from '$lib/components/Footer.svelte';
import Analytics from '$lib/components/Analytics.svelte';
let mounted = false;
if (browser) {
onMount(() => {
mounted = true;
});
}
</script>
<Analytics />
{#if mounted}
<Header />
<main>
<slot />
</main>
<Footer />
{/if}
<Header />
<main>
<slot />
</main>
<Footer />
<style>
main {
flex: 1; /* This pushes footer to bottom */
padding: 0;
padding: 20px;
min-height: 90vh;
}
</style>

View file

@ -1,4 +1,3 @@
/** @type {import('./$types').PageLoad} */
export function load() {
return {

View file

@ -1,34 +0,0 @@
import { EMAIL_ACCESS_KEY, EMAIL_BASE_URL } from '$env/static/private';
/** @type {import('./$types').Actions} */
export const actions = {
default: async ({ request }) => {
const formData = await request.formData();
console.log(formData);
const response = await fetch(`${EMAIL_BASE_URL}/submit`, {
method: 'POST',
body: JSON.stringify({
accessKey: EMAIL_ACCESS_KEY,
subject: 'Contato - Embroidery Viewer Beta Testers!',
name: formData.get('name'),
email: formData.get('email'),
}),
headers: { 'Content-Type': 'application/json' }
});
const json = await response.json();
if (json.error === undefined) {
return {
message: "home.banner.feedback.success",
textColor: 'green'
};
} else {
return {
message: 'home.banner.feedback.error',
textColor: 'red'
};
}
}
};

View file

@ -1,84 +1,15 @@
<script>
// @ts-nocheck
import { applyAction, enhance } from '$app/forms';
import { invalidateAll } from '$app/navigation';
import { t, locale, SUPPORTED_LOCALES } from '$lib/translations';
import Seo from '$lib/components/Seo.svelte';
import appScreenshot from '$lib/assets/app-with-frame.png';
import appScreenshotPt from '$lib/assets/app-with-frame-pt.png';
import { browser } from '$app/environment';
import { t } from '$lib/translations';
/** @type {import('./$types').PageProps} */
let { data } = $props();
/**
* @type {{ textColor: String; message: String; } | null}
*/
let feedbackMessage = $state(null);
/**
* @type {boolean}
*/
let loading = $state(false);
const metadata = data.metadata;
const resetFeedback = () => {
if (feedbackMessage) feedbackMessage = null
}
</script>
<Seo {...metadata} />
<div class="beta-section">
<div class="beta-content">
<div class="beta-image">
<img src={$locale === SUPPORTED_LOCALES.EN_US ? appScreenshot : appScreenshotPt} alt="Embroidery Viewer App Screenshot" />
</div>
<div class="beta-text">
<h1>{$t("home.banner.title")}</h1>
<p class="lead">
{@html $t('home.banner.subtitle')}
</p>
<p>
{$t('home.banner.description')}
</p>
<form
action="/"
method="POST"
class="beta-form"
use:enhance={() => {
loading = true;
return async ({ result }) => {
loading = false;
feedbackMessage = result.data;
if (result.type === 'success') await invalidateAll();
applyAction(result);
};
}}>
<label for="name">{$t("home.banner.name")}</label>
<input type="text" name="name" id="name" oninput={resetFeedback} required />
<label for="email">{$t("home.banner.email")}</label>
<input type="email" name="email" id="email" oninput={resetFeedback} required />
<button type="submit">{$t(loading ? 'home.banner.cta.loading' : 'home.banner.cta') }</button>
{#if feedbackMessage !== null}
<p style="margin-top: 1rem; color: {feedbackMessage.textColor}">
{$t(feedbackMessage.message)}
</p>
{/if}
</form>
</div>
</div>
</div>
<div class="home-container">
<section aria-labelledby="main-title">
<h1 id="main-title">{$t('home.main.title')}</h1>
@ -120,152 +51,14 @@
</div>
<style>
.beta-form {
background: white;
color: #000;
padding: 1.5rem;
border-radius: 10px;
max-width: 400px;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
}
.beta-form label {
display: block;
margin-top: 1rem;
font-weight: bold;
}
.beta-form input {
width: 100%;
padding: 0.6rem;
margin-top: 0.3rem;
border: 1px solid #ccc;
border-radius: 6px;
}
.beta-form button {
margin-top: 1.5rem;
width: 100%;
background-color: #05345f;
color: white;
border: none;
padding: 0.8rem;
font-size: 1rem;
border-radius: 6px;
cursor: pointer;
}
.beta-form button:disabled {
background-color: #7aa3c1;
cursor: wait;
}
.beta-form button:hover:enabled {
background-color: #042b4f;
}
.home-container {
margin: 0 auto;
width: 70%;
padding-top: 20px;
}
@media (max-width: 768px) {
.home-container {
width: 90%;
}
}
.beta-section {
background-color: #05345f;
color: white;
padding: 3rem 1rem;
display: flex;
justify-content: center;
}
.beta-content {
display: flex;
flex-wrap: wrap;
gap: 2rem;
max-width: 1000px;
align-items: center;
width: 100%;
}
.beta-image img {
max-width: 300px;
border-radius: 1rem;
}
.beta-text {
flex: 1;
min-width: 280px;
}
.beta-text h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: white;
}
.beta-text .lead {
font-size: 1.2rem;
margin-bottom: 1rem;
font-weight: 500;
}
.beta-text p {
margin-bottom: 1rem;
line-height: 1.5;
}
.beta-form {
background: white;
color: #000;
padding: 1.5rem;
border-radius: 10px;
max-width: 400px;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
}
.beta-form label {
display: block;
margin-top: 1rem;
font-weight: bold;
}
.beta-form input {
width: 100%;
padding: 0.6rem;
margin-top: 0.3rem;
border: 1px solid #ccc;
border-radius: 6px;
}
.beta-form button {
margin-top: 1.5rem;
width: 100%;
background-color: #05345f;
color: white;
border: none;
padding: 0.8rem;
font-size: 1rem;
border-radius: 6px;
cursor: pointer;
}
.beta-form button:hover {
background-color: #042b4f;
}
@media (max-width: 768px) {
.beta-content {
flex-direction: column;
align-items: center;
}
.beta-form {
margin: 0 auto;
width: 100%;
}
}
</style>

View file

@ -30,7 +30,7 @@
@media (max-width: 768px) {
section {
width: 90%;
width: 100%;
}
}
</style>

View file

@ -164,7 +164,7 @@
@media (max-width: 768px) {
.donate-container {
width: 90%;
width: 100%;
}
.donation-options {

View file

@ -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',
},
};
}

View file

@ -1,34 +0,0 @@
<script>
import { t } from '$lib/translations';
import Seo from '$lib/components/Seo.svelte';
/** @type {import('./$types').PageProps} */
let { data } = $props();
const metadata = data.metadata;
</script>
<Seo {...metadata} />
<section aria-labelledby="privacy-policy-heading">
<h1 id="privacy-policy-heading">{$t('mobile.app.privacy.policy.title')}</h1>
<p><em>{$t('mobile.app.privacy.policy.last.update')}</em></p>
{@html $t('mobile.app.privacy.policy.content')}
</section>
<style>
section {
width: 70%;
margin: 0 auto;
}
h2 {
font-size: 17px;
}
@media (max-width: 768px) {
section {
width: 90%;
}
}
</style>

View file

@ -28,7 +28,7 @@
@media (max-width: 768px) {
section {
width: 90%;
width: 100%;
}
}
</style>

View file

@ -28,7 +28,7 @@
@media (max-width: 768px) {
section {
width: 90%;
width: 100%;
}
}
</style>

View file

@ -24,14 +24,7 @@
maxSize: 1000000,
};
/**
* Update the flag that indicates that the files were rendered.
*
* @param {SubmitEvent} e
*/
function onSubmit(e) {
e.preventDefault();
e.stopPropagation();
function onSubmit() {
areAcceptedFilesRendered = true;
}
@ -85,7 +78,11 @@
<Seo {...metadata} />
<form id="form" enctype="multipart/form-data" onsubmit={onSubmit}>
<form
id="form"
enctype="multipart/form-data"
on:submit|preventDefault|stopPropagation={onSubmit}
>
<div class="title-container">
<h2>{$t('viewer.title')}</h2>
</div>
@ -147,7 +144,7 @@
@media only screen and (max-device-width: 768px) {
#form {
width: 90%;
width: 100%;
}
}
</style>

View file

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

View file

@ -1,22 +1,5 @@
import adapter from '@sveltejs/adapter-node';
import adapter from '@sveltejs/adapter-auto';
/** @type {import('@sveltejs/kit').Config} */
const config = {
extensions: ['.svelte'],
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter(),
},
vite: {
server: {
port: 7280,
},
},
prerender: {
origin: 'https://embroideryviewer.xyz',
},
};
const config = { kit: { adapter: adapter() } };
export default config;

View file

@ -6,7 +6,4 @@ export default defineConfig({
define: {
APP_VERSION: JSON.stringify(process.env.npm_package_version),
},
build: {
minify: true,
},
});