feat: privacy updates (#144)

* fix: default to auto on empty PUB_VERTD_URL

* feat: privacy policy

* chore: vert self-hosting

* feat: view info submitted

for transparency, see the exact details that is sent automatically to the owner of the instance:
- job id
- convert from
- convert to
- ffmpeg stderr
- actual video file (if submitted)

* feat: vertd error details footer

* fix: sanitize translations

...my bad i realize this earlier

* refactor: remove jepgify

* fix: blob scheme

* fix: seo fixes

don't index static language urls (doesn't do anything) - robots.txt & sitemap.xml

* feat: translatable privacy policy

i forgor

also fix inconsistencies with link colours

* fix: privacy translation fixes

* fix: broken conflict merge

why does github's web conflict resolver not have highlighting
This commit is contained in:
Maya 2025-10-20 15:32:22 +03:00 committed by GitHub
parent e3e3372aad
commit b93a566928
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 462 additions and 308 deletions

View File

@ -21,6 +21,7 @@
"overlayscrollbars-svelte": "^0.5.5",
"p-queue": "^8.1.1",
"riff-file": "^1.0.3",
"sanitize-html": "^2.17.0",
"svelte-stripe": "^1.4.0",
"vert-wasm": "^0.0.2",
"vite-plugin-wasm": "^3.5.0",
@ -32,6 +33,7 @@
"@sveltejs/kit": "^2.42.2",
"@sveltejs/vite-plugin-svelte": "^4.0.4",
"@types/eslint": "^9.6.1",
"@types/sanitize-html": "^2.16.0",
"autoprefixer": "^10.4.21",
"css-select": "5.1.0",
"eslint": "^9.36.0",
@ -317,6 +319,10 @@
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@24.8.1", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q=="],
"@types/sanitize-html": ["@types/sanitize-html@2.16.0", "", { "dependencies": { "htmlparser2": "^8.0.0" } }, "sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.44.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/type-utils": "8.44.0", "@typescript-eslint/utils": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.44.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.44.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw=="],
@ -541,6 +547,8 @@
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="],
"human-id": ["human-id@4.1.1", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
@ -567,6 +575,8 @@
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="],
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
@ -671,6 +681,8 @@
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"parse-srcset": ["parse-srcset@1.0.2", "", {}, "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
@ -737,6 +749,8 @@
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
"sanitize-html": ["sanitize-html@2.17.0", "", { "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", "htmlparser2": "^8.0.0", "is-plain-object": "^5.0.0", "parse-srcset": "^1.0.2", "postcss": "^8.3.11" } }, "sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA=="],
"sass": ["sass@1.93.0", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-CQi5/AzCwiubU3dSqRDJ93RfOfg/hhpW1l6wCIvolmehfwgCI35R/0QDs1+R+Ygrl8jFawwwIojE2w47/mf94A=="],
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
@ -807,6 +821,8 @@
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
"undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
"unplugin": ["unplugin@2.3.10", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw=="],
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],

View File

@ -17,8 +17,7 @@
"subtitle": "Die Verarbeitung aller Bild-, Audio- und Dokumentdateien findet auf deinem Gerät statt. Videos werden auf unseren blitzschnellen Servern konvertiert. Kein Dateigrößenlimit, keine Werbung und vollständig Open-Source.",
"uploader": {
"text": "Dateien hier ablegen oder klicken zum {action}",
"convert": "Konvertieren",
"jpegify": "Jpegifizieren"
"convert": "Konvertieren"
},
"cards": {
"title": "VERT unterstützt...",
@ -190,7 +189,7 @@
"notable_contributors": "Besonders erwähnenswerte Mitwirkende",
"notable_description": "Wir möchten diesen Personen für ihre wichtigen Beiträge zu VERT danken.",
"github_contributors": "GitHub-Mitwirkende",
"github_description": "Ein großes [jpegify_link]Dankeschön[/jpegify_link] an all diese Leute für ihre Hilfe! [github_link]Möchtest du auch helfen?[/github_link]",
"github_description": "Ein großes Dankeschön an all diese Leute für ihre Hilfe! [github_link]Möchtest du auch helfen?[/github_link]",
"no_contributors": "Scheint, als hätte noch niemand beigetragen... [contribute_link]sei der Erste, der beiträgt![/contribute_link]",
"libraries": "Bibliotheken",
"libraries_description": "Ein großes Dankeschön an FFmpeg (Audio, Video), ImageMagick (Bilder) und Pandoc (Dokumente) für die Pflege solch exzellenter Bibliotheken über so viele Jahre. VERT verlässt sich auf sie, um dir deine Konvertierungen zu ermöglichen.",
@ -215,12 +214,5 @@
"no_audio": "Kein Audiostream gefunden.",
"invalid_rate": "Ungültige Abtastrate angegeben: {rate}Hz"
}
},
"jpegify": {
"title": "GEHEIMES JPEGIFIZIEREN!!!",
"subtitle": "(psst... sag es niemandem!)",
"button": "JPEGIFIZIEREN {compression}%!!!",
"download": "Herunterladen",
"delete": "Löschen"
}
}

View File

@ -17,8 +17,7 @@
"subtitle": "Όλη η επεξεργασία εικόνων, ήχου και εγγράφων γίνεται στη συσκευή σας. Τα βίντεο μετατρέπονται στους κεραυνοβόλα γρήγορους διακομιστές μας. Χωρίς όριο μεγέθους αρχείου, χωρίς διαφημίσεις και εντελώς ανοιχτού κώδικα.",
"uploader": {
"text": "Σύρετε ή κάντε κλικ για {action}",
"convert": "μετατροπή",
"jpegify": "μετατροπή σε jpeg"
"convert": "μετατροπή"
},
"cards": {
"title": "Το VERT υποστηρίζει...",
@ -216,7 +215,7 @@
"notable_contributors": "Αξιόλογοι συνεισφέροντες",
"notable_description": "Θα θέλαμε να ευχαριστήσουμε αυτά τα άτομα για τις σημαντικές συνεισφορές τους στο VERT.",
"github_contributors": "Συνεισφέροντες στο GitHub",
"github_description": "Μεγάλες [jpegify_link]ευχαριστίες[/jpegify_link] σε όλα αυτά τα άτομα που βοήθησαν! [github_link]Θέλετε να βοηθήσετε κι εσείς;[/github_link]",
"github_description": "Μεγάλες ευχαριστίες σε όλα αυτά τα άτομα που βοήθησαν! [github_link]Θέλετε να βοηθήσετε κι εσείς;[/github_link]",
"no_contributors": "Φαίνεται ότι κανείς δεν έχει συνεισφέρει ακόμα... [contribute_link]γίνετε ο πρώτος που θα συνεισφέρει![/contribute_link]",
"libraries": "Βιβλιοθήκες",
"libraries_description": "Μεγάλες ευχαριστίες στα FFmpeg (ήχος, βίντεο), ImageMagick (εικόνες) και Pandoc (έγγραφα) που διατηρούν τέτοιες εξαιρετικές βιβλιοθήκες για τόσα χρόνια. Το VERT βασίζεται σε αυτές για να σας παρέχει τις μετατροπές σας.",
@ -241,12 +240,5 @@
"no_audio": "Δεν βρέθηκε ροή ήχου.",
"invalid_rate": "Καθορίστηκε μη έγκυρος ρυθμός δειγματοληψίας: {rate}Hz"
}
},
"jpegify": {
"title": "ΜΥΣΤΙΚΟ JPEGIFY!!!",
"subtitle": "(σσσ... μην το πεις σε κανέναν!)",
"button": "JPEGIFY {compression}%!!!",
"download": "Λήψη",
"delete": "Διαγραφή"
}
}

View File

@ -10,15 +10,15 @@
"footer": {
"copyright": "© {year} VERT.",
"source_code": "Source code",
"discord_server": "Discord server"
"discord_server": "Discord server",
"privacy_policy": "Privacy policy"
},
"upload": {
"title": "The file converter you'll love.",
"subtitle": "All image, audio, and document processing is done on your device. Videos are converted on our lightning-fast servers. No file size limit, no ads, and completely open source.",
"uploader": {
"text": "Drop or click to {action}",
"convert": "convert",
"jpegify": "jpegify"
"convert": "convert"
},
"cards": {
"title": "VERT supports...",
@ -79,11 +79,20 @@
"errors": {
"cant_convert": "We can't convert this file.",
"vertd_server": "what are you doing..? you're supposed to run the vertd server!",
"vertd_generic_view": "View error details",
"vertd_generic_body": "An error occurred whilst whilst trying convert your video. Would you like to submit this video to the developers to help fix this bug? Only your video file will be sent. No identifiers will be uploaded.",
"vertd_generic_title": "Video conversion error",
"vertd_generic_yes": "Submit video",
"vertd_generic_no": "Don't submit",
"vertd_failed_to_keep": "Failed to keep the video on the server: {error}",
"vertd_details": "View error details",
"vertd_details_body": "If you press submit, <b>your video will also be attached</b> alongside the error log which is always reported to us for review. The following information is the log that we automatically receive:",
"vertd_details_footer": "This information will only be used for troubleshooting purposes and will never be shared. View our [privacy_link]privacy policy[/privacy_link] for more details.",
"vertd_details_job_id": "<b>Job ID:</b> {jobId}",
"vertd_details_from": "<b>From format:</b> {from}",
"vertd_details_to": "<b>To format:</b> {to}",
"vertd_details_error_message": "<b>Error message:</b> [view_link]View error logs[/view_link]",
"vertd_details_close": "Close",
"unsupported_format": "Only image, video, audio, and document files are supported",
"vertd_not_found": "Could not find the vertd instance to start video conversion. Are you sure the instance URL is set correctly?",
"worker_downloading": "The {type} converter is currently being initialized, please wait a few moments.",
@ -216,7 +225,7 @@
"notable_contributors": "Notable contributors",
"notable_description": "We'd like to thank these people for their major contributions to VERT.",
"github_contributors": "GitHub contributors",
"github_description": "Big [jpegify_link]thanks[/jpegify_link] to all these people for helping out! [github_link]Want to help too?[/github_link]",
"github_description": "Big thanks to all these people for helping out! [github_link]Want to help too?[/github_link]",
"no_contributors": "Seems like no one has contributed yet... [contribute_link]be the first to contribute![/contribute_link]",
"libraries": "Libraries",
"libraries_description": "A big thanks to FFmpeg (audio, video), ImageMagick (images) and Pandoc (documents) for maintaining such excellent libraries for so many years. VERT relies on them to provide you with your conversions.",
@ -242,11 +251,38 @@
"invalid_rate": "Invalid sample rate specified: {rate}Hz"
}
},
"jpegify": {
"title": "SECRET JPEGIFY!!!",
"subtitle": "(shh... don't tell anyone!)",
"button": "JPEGIFY {compression}%!!!",
"download": "Download",
"delete": "Delete"
"privacy": {
"title": "Privacy Policy",
"summary": {
"title": "Summary",
"description": "VERT's privacy policy is very simple: we do not collect or store any data on you at all. We don't use cookies or trackers, analytics are completely private, and all conversions (except videos) happen locally on your browser. Videos are deleted after being downloaded, or an hour, unless explicitly given permission by you to be stored; it will only be used for the purpose of troubleshooting. VERT self-hosts a Coolify instance for hosting the website and vertd (for video conversion), and a Plausible instance for completely anonymous and aggregated analytics.<br/><br/>Note this may only apply to the official VERT instance at [vert_link]vert.sh[/vert_link]; third-party instances may handle your data differently."
},
"conversions": {
"title": "Conversions",
"description": "Most conversions (images, documents, audio) happen entirely locally on your device using WebAssembly versions of the relevant tools (e.g. ImageMagick, Pandoc, FFmpeg). This means your files never leave your device and we will never have access to them.<br/><br/>Video conversions are performed on our servers because they require more processing power and cannot be done very quickly on the browser yet. Videos you convert with VERT are deleted after being downloaded, or after one hour, unless you explicitly give permission for us to store them longer purely for troubleshooting purposes."
},
"conversion_errors": {
"title": "Conversion Errors",
"description": "When a video conversion fails, we may collect some anonymous data to help us diagnose the issue. This data may include:",
"list_job_id": "The job ID, which is the anonymized file name",
"list_format_from": "The format you converted from",
"list_format_to": "The format you converted to",
"list_stderr": "The FFmpeg stderr output of your job (error message)",
"list_video": "The actual video file (if given explicit permission)",
"footer": "This information is used solely for the purpose of diagnosing conversion issues. The actual video file will only ever be collected if you give us permission to do so, where it will only be used for troubleshooting."
},
"analytics": {
"title": "Analytics",
"description": "We self-host a Plausible instance for completely anonymous and aggregated analytics. Plausible does not use cookies and complies with all major privacy regulations (GDPR/CCPA/PECR). You can opt out of analytics in the \"Privacy & data\" section in [settings_link]settings[/settings_link] and read more about Plausible's privacy practices [plausible_link]here[/plausible_link]."
},
"local_storage": {
"title": "Local Storage",
"description": "We use your browser's local storage to save your settings, and your browser's session storage to temporarily store the GitHub contributors list for the \"About\" section to reduce repeated GitHub API requests. No personal data is stored or transmitted.<br/><br/>The WebAssembly versions of the conversion tools we use (FFmpeg, ImageMagick, Pandoc) are also stored locally on your browser when you first visit the website, so you don't need to redownload them each visit. No personal data is stored or transmitted. You may view or delete this data at any time in the \"Privacy & data\" section in [settings_link]settings[/settings_link]."
},
"contact": {
"title": "Contact",
"description": "For questions, email us at: [email_link]hello@vert.sh[/email_link]. If you are using a third-party instance of VERT, please contact the hoster of that instance instead."
},
"last_updated": "Last updated: 2025-10-19"
}
}

View File

@ -17,8 +17,7 @@
"subtitle": "Todo el procesamiento de imágenes, audio y documentos es hecho en tu dispositivo. Los vídeos son convertidos en nuestros servidores ultra rápidos. Sin límite de tamaño de archivo, sin anuncios y de código abierto.",
"uploader": {
"text": "Arrastra o haz clic para {action}",
"convert": "convertir",
"jpegify": "jpegificar"
"convert": "convertir"
},
"cards": {
"title": "VERT soporta...",
@ -210,7 +209,7 @@
"notable_contributors": "Colaboradores destacados",
"notable_description": "Queremos dar las gracias a las siguientes personas por sus importantes contribuciones a VERT.",
"github_contributors": "Contribuidores de GitHub",
"github_description": "¡Muchas [jpegify_link]gracias[/jpegify_link] a todos los que han contribuido! [github_link]¿Quieres contribuir también?[/github_link]",
"github_description": "¡Muchas gracias a todos los que han contribuido! [github_link]¿Quieres contribuir también?[/github_link]",
"no_contributors": "Parece que nadie ha contribuido todavía... [contribute_link]¡Sé el primero en hacerlo![/contribute_link]",
"libraries": "Librerías",
"libraries_description": "Muchas gracias a FFmpeg (audio, vídeo), ImageMagick (imágenes) y Pandoc (documentos) por mantener librerías excelentes por tantos años. VERT depende de ellas para proporcionar tus conversiones.",
@ -235,12 +234,5 @@
"no_audio": "No se encontró una pista de audio.",
"invalid_rate": "La tasa de muestreo especificada no es válida: {rate}Hz"
}
},
"jpegify": {
"title": "¡¡¡JPEGIFICADOR SECRETO!!!",
"subtitle": "(shh... ¡no se lo digas a nadie!)",
"button": "¡¡¡JPEGIFICAR {compression}%!!!",
"download": "Descargar",
"delete": "Eliminar"
}
}

View File

@ -17,8 +17,7 @@
"subtitle": "Tout le traitement des images, des fichiers audio et des documents s'effectue sur votre appareil. Les vidéos sont converties sur nos serveurs ultra-rapides. Aucune limite de taille de fichier, aucune publicité et entièrement open source.",
"uploader": {
"text": "Déposer ou cliquer pour {action}",
"convert": "convertir",
"jpegify": "jpegify"
"convert": "convertir"
},
"cards": {
"title": "VERT supports...",
@ -190,7 +189,7 @@
"notable_contributors": "Contributeurs notables",
"notable_description": "Nous tenons à remercier ces personnes pour leurs contributions majeures à VERT.",
"github_contributors": "Les contributeurs de GitHub",
"github_description": "Un grand [jpegify_link]merci[/jpegify_link] à toutes ces personnes pour leur aide ! [github_link]Vous voulez aussi aider ?[/github_link]",
"github_description": "Un grand merci à toutes ces personnes pour leur aide ! [github_link]Vous voulez aussi aider ?[/github_link]",
"no_contributors": "Il semble que personne n'ait encore contribué... [contribute_link]soyez le premier à contribuer ![/contribute_link]",
"libraries": "Bibliothèques",
"libraries_description": "un grand merci à FFmpeg (audio, video), ImageMagick (images) et Pandoc (documents) pour avoir maintenu d'aussi excellentes bibliothèques pendant tant d'années, VERT compte sur eux pour vous fournir vos conversions.",
@ -215,12 +214,5 @@
"no_audio": "Aucun flux audio détécté.",
"invalid_rate": "Taux d'échantillonnage spécifié non valide: {rate}Hz"
}
},
"jpegify": {
"title": "SECRET JPEGIFY!!!",
"subtitle": "(chut... ne le dis à personne !)",
"button": "JPEGIFY {compression}%!!!",
"download": "Télécharger",
"delete": "Supprimer"
}
}

View File

@ -17,8 +17,7 @@
"subtitle": "Cijelokupna obrada slika, zvuka i dokumenata se odvija na vašem uređaju. Videozapisi se pretvaraju na našim izrazito brzim serverima. Nema nikakvih ograničenja veličine niti reklama i potpuno je open source.",
"uploader": {
"text": "Ubaci ili klikni da {action}",
"convert": "pretvori",
"jpegify": "jpegiraj"
"convert": "pretvori"
},
"cards": {
"title": "VERT podržava...",
@ -190,7 +189,7 @@
"notable_contributors": "Značajni suradnici",
"notable_description": "Želimo zahvaliti ovim ljudima za njihove ogromne doprinose VERTu.",
"github_contributors": "GitHub suradnici",
"github_description": "Velike [jpegify_link]zahvale[/jpegify_link] svim ovim ljudima koji su nam pomogli! [github_link]Želiš nam i ti pomoći?[/github_link]",
"github_description": "Velike zahvale svim ovim ljudima koji su nam pomogli! [github_link]Želiš nam i ti pomoći?[/github_link]",
"no_contributors": "Čini se kako nitko nije još doprinio... [contribute_link]budite prvi koji će doprinjeti![/contribute_link]",
"libraries": "Biblioteke",
"libraries_description": "Velike zahvale prema FFmpeg (audio, video), ImageMagick (slike) i Pandoc (dokumenti) što su održavali tako odlične biblioteke svih ovih godina. VERT se oslanja na njih da bi Vam pružili pretvorbu.",
@ -214,12 +213,5 @@
"no_audio": "Nije pronađen audio.",
"invalid_rate": "Upisan nevažeći sample rate: {rate}Hz!"
}
},
"jpegify": {
"title": "TAJNI JPEGIFY!!!",
"subtitle": "(shhhh... nemoj reći nikome!)",
"button": "JPEGIRAJ {compression}%!!!",
"download": "Preuzmi",
"delete": "Obriši"
}
}

View File

@ -17,8 +17,7 @@
"subtitle": "Tutta l'elaborazione di immagini, audio e documenti avviene sul tuo dispositivo. I video sono convertiti sui nostri server velocissimi. Nessun limite di dimensione, nessuna pubblicità e completamente open source.",
"uploader": {
"text": "Trascina o clicca per {action}",
"convert": "convertire",
"jpegify": "jpegificare"
"convert": "convertire"
},
"cards": {
"title": "VERT supporta...",
@ -216,7 +215,7 @@
"notable_contributors": "Contributori di rilievo",
"notable_description": "Vorremmo ringraziare queste persone per i loro importanti contributi a VERT.",
"github_contributors": "Contributori GitHub",
"github_description": "Un grande [jpegify_link]grazie[/jpegify_link] a tutte queste persone per aver dato una mano! [github_link]Vuoi aiutare anche tu?[/github_link]",
"github_description": "Un grande grazie a tutte queste persone per aver dato una mano! [github_link]Vuoi aiutare anche tu?[/github_link]",
"no_contributors": "Sembra che nessuno abbia ancora contribuito... [contribute_link]sii il primo a contribuire![/contribute_link]",
"libraries": "Librerie",
"libraries_description": "Un grande ringraziamento a FFmpeg (audio, video), ImageMagick (immagini) e Pandoc (documenti) per aver mantenuto librerie così eccellenti per così tanti anni. VERT si affida a loro per fornirti le tue conversioni.",
@ -241,12 +240,5 @@
"no_audio": "Nessuno *stream* audio trovato.",
"invalid_rate": "Frequenza di campionamento specificata non valida: {rate}Hz"
}
},
"jpegify": {
"title": "JPEGIFY SEGRETO!!!",
"subtitle": "(shh... non dirlo a nessuno!)",
"button": "JPEGIFY {compression}%!!!",
"download": "Scarica",
"delete": "Elimina"
}
}

View File

@ -17,8 +17,7 @@
"subtitle": "すべての画像・音声・ドキュメント処理はデバイス上で行われます。動画は超高速サーバーで変換されます。ファイルサイズ制限なし、広告なし、完全オープンソース。",
"uploader": {
"text": "ドロップまたはクリックして{action}",
"convert": "変換",
"jpegify": "JPEG化"
"convert": "変換"
},
"cards": {
"title": "VERTがサポートしている形式",
@ -210,7 +209,7 @@
"notable_contributors": "特筆すべき貢献者",
"notable_description": "VERTに大きく貢献してくださった方々に感謝します。",
"github_contributors": "GitHubの貢献者",
"github_description": "多くの方々に[jpegify_link]感謝[/jpegify_link]します![github_link]あなたも参加してみませんか?[/github_link]",
"github_description": "多くの方々に感謝します![github_link]あなたも参加してみませんか?[/github_link]",
"no_contributors": "まだ誰も貢献していないようです… [contribute_link]最初の貢献者になりましょう![/contribute_link]",
"libraries": "ライブラリ",
"libraries_description": "長年にわたり優れたライブラリを提供してくれているFFmpeg音声・動画、ImageMagick画像、Pandocドキュメントに感謝します。VERTはこれらに依存して動作しています。",
@ -235,12 +234,5 @@
"no_audio": "音声ストリームが見つかりません。",
"invalid_rate": "無効なサンプリングレートが指定されました: {rate}Hz"
}
},
"jpegify": {
"title": "秘密のJPEGIFY!!!",
"subtitle": "(しっ…誰にも言わないで!)",
"button": "JPEGIFY {compression}%!!!",
"download": "ダウンロード",
"delete": "削除"
}
}

View File

@ -17,8 +17,7 @@
"subtitle": "Tüm görüntü, ses ve belge işlemleri cihazınızda gerçekleştirilir. Videolar, ışık hızındaki sunucularımızda dönüştürülür. Dosya boyutu sınırı ve reklam yoktur. Tamamen açık kaynaklıdır.",
"uploader": {
"text": "{action} için sürükleyip bırakın veya dosya seçin",
"convert": "dönüştürmek",
"jpegify": "jpegify"
"convert": "dönüştürmek"
},
"cards": {
"title": "VERT'in desteklediği formatlar...",
@ -211,7 +210,7 @@
"notable_contributors": "Önemli katılımcılar",
"notable_description": "VERT'e sağladıkları büyük katkılardan dolayı bu kişilere teşekkür ederiz.",
"github_contributors": "GitHub katılımcıları",
"github_description": "Yardımcı olan herkese çok [jpegify_link]teşekkürler[/jpegify_link]! [github_link]Sen de yardım etmek ister misin?[/github_link]",
"github_description": "Yardımcı olan herkese çok teşekkürler! [github_link]Sen de yardım etmek ister misin?[/github_link]",
"no_contributors": "Henüz kimse katkıda bulunmamış gibi görünüyor... [contribute_link]ilk katkıda bulunan sen ol![/contribute_link]",
"libraries": "Kütüphaneler",
"libraries_description": "Bu mükemmel kütüphaneleri yıllardır geliştirdikleri için FFmpeg (ses, video), ImageMagick (görseller) ve Pandoc (belgeler)'a çok teşekkür ederiz. VERT, dönüştürme işlemleri için bu kütüphaneleri kullanmaktadır.",
@ -236,12 +235,5 @@
"no_audio": "Ses akışı bulunamadı.",
"invalid_rate": "Geçersiz örnekleme hızı: {hız}Hz"
}
},
"jpegify": {
"title": "GİZLİ JPEGIFY!!!",
"subtitle": "(şşş... kimseye söyleme!)",
"button": "JPEGIFY {compression}%!!!",
"download": "İndir",
"delete": "Sil"
}
}

View File

@ -18,6 +18,7 @@
"@sveltejs/kit": "^2.42.2",
"@sveltejs/vite-plugin-svelte": "^4.0.4",
"@types/eslint": "^9.6.1",
"@types/sanitize-html": "^2.16.0",
"autoprefixer": "^10.4.21",
"css-select": "5.1.0",
"eslint": "^9.36.0",
@ -54,6 +55,7 @@
"overlayscrollbars-svelte": "^0.5.5",
"p-queue": "^8.1.1",
"riff-file": "^1.0.3",
"sanitize-html": "^2.17.0",
"svelte-stripe": "^1.4.0",
"vert-wasm": "^0.0.2",
"vite-plugin-wasm": "^3.5.0"

View File

@ -3,19 +3,13 @@
import { removeDialog } from "$lib/store/DialogProvider";
import { BanIcon, CheckIcon, InfoIcon, TriangleAlert } from "lucide-svelte";
import { quintOut } from "svelte/easing";
import type { Dialog as DialogType } from "$lib/store/DialogProvider";
type Props = {
id: number;
title: string;
message: string;
buttons: {
text: string;
action: () => void;
}[];
type: "success" | "error" | "info" | "warning";
};
type Props = DialogType;
let { id, title, message, buttons, type }: Props = $props();
let props: Props = $props();
const { id, title, message, buttons, type } = props;
const additional = "additional" in props ? props.additional : undefined;
const colors = {
success: "purple",
@ -59,7 +53,14 @@
</div>
</div>
<div class="flex flex-col gap-1 w-full">
<p class="text-sm font-normal text-muted">{message}</p>
{#if typeof message === "string"}
<p class="text-sm font-normal text-muted whitespace-pre-wrap">{message}</p>
{:else}
{@const MessageComponent = message}
<div class="text-sm font-normal text-muted">
<MessageComponent {id} {title} {type} {buttons} {additional} />
</div>
{/if}
</div>
<div class="flex flex-row items-center gap-4 w-full">
{#each buttons as { text, action }, i}

View File

@ -11,10 +11,9 @@
type Props = {
class?: string;
jpegify?: boolean;
};
const { class: classList, jpegify }: Props = $props();
const { class: classList }: Props = $props();
let uploaderButton = $state<HTMLButtonElement>();
let fileInput = $state<HTMLInputElement>();
@ -43,13 +42,9 @@
const handleFileChange = (e: Event) => {
if (!fileInput) return;
if (page.url.pathname !== "/jpegify/") {
const oldLength = files.files.length;
files.add(fileInput.files);
if (oldLength !== files.files.length) goto("/convert");
} else {
files.add(fileInput.files);
}
const oldLength = files.files.length;
files.add(fileInput.files);
if (oldLength !== files.files.length) goto("/convert");
};
onMount(() => {
@ -100,9 +95,7 @@
</div>
<h2 class="text-center text-2xl font-semibold mt-4">
{m["upload.uploader.text"]({
action: jpegify
? m["upload.uploader.jpegify"]()
: m["upload.uploader.convert"](),
action: m["upload.uploader.convert"]()
})}
</h2>
</Panel>

View File

@ -2,6 +2,10 @@
export interface VertdErrorProps {
jobId: string;
auth: string;
from?: string;
to?: string;
errorMessage?: string;
fileName?: string;
}
</script>
@ -10,6 +14,8 @@
import { m } from "$lib/paraglide/messages";
import { ToastManager, type ToastProps } from "$lib/toast/index.svelte";
import { addDialog } from "$lib/store/DialogProvider";
import VertdErrorDetails from "./VertdErrorDetails.svelte";
const toast: ToastProps<VertdErrorProps> = $props();
@ -52,22 +58,50 @@
ToastManager.remove(toast.id);
};
const showDetails = () => {
addDialog(
m["convert.errors.vertd_details"](),
VertdErrorDetails as any,
[
{
text: "Close",
action: () => {},
},
],
"info",
{
jobId: toast.additional.jobId || "Unknown",
from: toast.additional.from || "Unknown",
to: toast.additional.to || "Unknown",
errorMessage: toast.additional.errorMessage || "Unknown error",
},
);
};
</script>
<div class="flex flex-col gap-4">
<p class="text-black">{m["convert.errors.vertd_generic_body"]()}</p>
<div class="flex gap-4">
<div class="flex flex-col gap-2">
<button
onclick={submit}
class="btn rounded-lg h-fit py-2 w-full bg-accent-red-alt text-white"
onclick={showDetails}
class="btn rounded-lg h-fit py-2 w-full bg-accent-blue text-black"
disabled={submitting}
>{m["convert.errors.vertd_generic_yes"]()}</button
>
<button
onclick={remove}
class="btn rounded-lg h-fit py-2 w-full"
disabled={submitting}
>{m["convert.errors.vertd_generic_no"]()}</button
>{m["convert.errors.vertd_generic_view"]()}</button
>
<div class="flex gap-4">
<button
onclick={submit}
class="btn rounded-lg h-fit py-2 w-full bg-accent-red-alt text-white"
disabled={submitting}
>{m["convert.errors.vertd_generic_yes"]()}</button
>
<button
onclick={remove}
class="btn rounded-lg h-fit py-2 w-full"
disabled={submitting}
>{m["convert.errors.vertd_generic_no"]()}</button
>
</div>
</div>
</div>

View File

@ -0,0 +1,64 @@
<script lang="ts">
import { m } from "$lib/paraglide/messages";
import type { DialogProps } from "$lib/store/DialogProvider";
import { link, sanitize } from "$lib/store/index.svelte";
interface VertdErrorDetailsProps {
jobId: string;
from: string;
to: string;
errorMessage: string;
}
type Props = DialogProps<VertdErrorDetailsProps>;
let { additional }: Props = $props();
</script>
<div class="flex flex-col gap-2">
<p>{@html sanitize(m["convert.errors.vertd_details_body"]())}</p>
<p>
<span class="text-black dynadark:text-white">
{@html sanitize(m["convert.errors.vertd_details_job_id"]({
jobId: additional.jobId,
}))}
</span>
</p>
<p>
<span class="text-black dynadark:text-white">
{@html sanitize(m["convert.errors.vertd_details_from"]({
from: additional.from,
}))}
</span>
</p>
<p>
<span class="text-black dynadark:text-white">
{@html sanitize(m["convert.errors.vertd_details_to"]({ to: additional.to }))}
</span>
</p>
<p>
<span class="text-black dynadark:text-white">
{@html sanitize(link(
["view_link"],
m["convert.errors.vertd_details_error_message"](),
[
URL.createObjectURL(
new Blob([additional.errorMessage], {
type: "text/plain",
}),
),
],
[true],
["text-blue-500 font-normal"],
))}
</span>
</p>
<p>
{@html sanitize(link(
["privacy_link"],
m["convert.errors.vertd_details_footer"](),
"/privacy",
[true],
))}
</p>
</div>

View File

@ -26,9 +26,9 @@
easing: quintOut,
}}
>
{#each dialogList as { id, title, message, buttons, type }, i}
{#each dialogList as dialog, i}
{#if i === 0}
<Dialog {id} {title} {message} {buttons} {type} />
<Dialog {...dialog} />
{/if}
{/each}
</div>

View File

@ -5,6 +5,7 @@
const items = $derived([
[m["footer.source_code"](), GITHUB_URL_VERT],
[m["footer.discord_server"](), DISCORD_URL],
[m["footer.privacy_policy"](), "/privacy"],
]);
const year = new Date().getFullYear();

View File

@ -37,7 +37,7 @@
at: 25,
},
{
matcher: (path) => path === "/jpegify/",
matcher: (path) => path === "/privacy/",
color: "var(--bg-gradient-red-from)",
at: 100,
},

View File

@ -377,6 +377,9 @@ export class VertdConverter extends Converter {
additional: {
jobId: uploadRes.id,
auth: uploadRes.auth,
from: input.from,
to: to,
errorMessage: msg.data.message,
},
});
}

View File

@ -6,7 +6,7 @@
GITHUB_URL_VERT,
} from "$lib/consts";
import { m } from "$lib/paraglide/messages";
import { link } from "$lib/store/index.svelte";
import { link, sanitize } from "$lib/store/index.svelte";
let { mainContribs, notableContribs, ghContribs } = $props();
</script>
@ -101,23 +101,24 @@
</h2>
{#if ghContribs && ghContribs.length > 0}
<p class="text-base text-muted font-normal">
{@html link(
["jpegify_link", "github_link"],
m["about.credits.github_description"](),
["/jpegify", GITHUB_URL_VERT],
[false, true],
[
"text-black dynadark:text-white",
"text-blue-500 font-normal hover:underline",
],
{@html sanitize(
link(
"github_link",
m["about.credits.github_description"](),
GITHUB_URL_VERT,
true,
),
)}
</p>
{:else}
<p class="text-base text-muted font-normal italic">
{@html link(
"contribute_link",
m["about.credits.no_contributors"](),
GITHUB_URL_VERT,
{@html sanitize(
link(
"contribute_link",
m["about.credits.no_contributors"](),
GITHUB_URL_VERT,
true,
),
)}
</p>
{/if}
@ -131,12 +132,12 @@
{/each}
</div>
{/if}
<h2 class="mt-2 -mb-2">{m["about.credits.libraries"]()}</h2>
<p class="font-normal">
{m["about.credits.libraries_description"]()}
</p>
</div>
<h2 class="mt-2 -mb-2">{m["about.credits.libraries"]()}</h2>
<p class="font-normal">
{m["about.credits.libraries_description"]()}
</p>
{/if}
</div></Panel
>
</div>
</Panel>

View File

@ -5,7 +5,7 @@
import { DISCORD_URL } from "$lib/consts";
import { error } from "$lib/logger";
import { m } from "$lib/paraglide/messages";
import { link } from "$lib/store/index.svelte";
import { link, sanitize } from "$lib/store/index.svelte";
import { ToastManager } from "$lib/toast/index.svelte";
let copied = false;
@ -48,11 +48,12 @@
</a>
</div>
<p class="text-muted">
{@html link(
{@html sanitize(link(
"discord_link",
m["about.sponsors.description"](),
DISCORD_URL,
)}
true
))}
<span class="inline-block mx-[2px] relative top-[2px]">
<button
id="email"

View File

@ -2,6 +2,7 @@
import Panel from "$lib/components/visual/Panel.svelte";
import { MessageCircleQuestionIcon } from "lucide-svelte";
import { m } from "$lib/paraglide/messages";
import { sanitize } from "$lib/store/index.svelte";
</script>
<Panel class="flex flex-col gap-3 p-6">
@ -14,6 +15,6 @@
{m["about.why.title"]()}
</h2>
<p class="text-lg font-normal">
{@html m["about.why.description"]()}
{@html sanitize(m["about.why.description"]())}
</p>
</Panel>

View File

@ -17,7 +17,7 @@
import { m } from "$lib/paraglide/messages";
import Dropdown from "$lib/components/functional/Dropdown.svelte";
import FancyInput from "$lib/components/functional/FancyInput.svelte";
import { effects } from "$lib/store/index.svelte";
import { effects, sanitize } from "$lib/store/index.svelte";
import FormatDropdown from "$lib/components/functional/FormatDropdown.svelte";
import { categories } from "$lib/converters";
import clsx from "clsx";
@ -43,7 +43,7 @@
{m["settings.conversion.filename_format"]()}
</p>
<p class="text-sm text-muted font-normal">
{@html m["settings.conversion.filename_description"]()}
{@html sanitize(m["settings.conversion.filename_description"]())}
</p>
</div>
<FancyTextInput

View File

@ -10,7 +10,7 @@
import type { ISettings } from "./index.svelte";
import { effects } from "$lib/store/index.svelte";
import { m } from "$lib/paraglide/messages";
import { link } from "$lib/store/index.svelte";
import { link, sanitize } from "$lib/store/index.svelte";
import { swManager, type CacheInfo } from "$lib/sw/register";
import { onMount } from "svelte";
import { error } from "$lib/logger";

View File

@ -7,7 +7,7 @@
import Dropdown from "$lib/components/functional/Dropdown.svelte";
import { vertdLoaded } from "$lib/store/index.svelte";
import { m } from "$lib/paraglide/messages";
import { link } from "$lib/store/index.svelte";
import { link, sanitize } from "$lib/store/index.svelte";
import { VertdInstance, type VertdInner } from "./vertdSettings.svelte";
let vertdCommit = $state<string | null>(null);
@ -73,14 +73,14 @@
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-4">
<p class="text-sm text-muted font-normal">
{@html m["settings.vertd.description"]()}
{@html sanitize(m["settings.vertd.description"]())}
</p>
<p class="text-sm text-muted font-normal">
{@html link(
{@html sanitize(link(
"vertd_link",
m["settings.vertd.hosting_info"](),
GITHUB_URL_VERTD,
)}
))}
</p>
<div class="flex flex-col gap-2">
<p class="text-base font-bold">

View File

@ -1,17 +1,39 @@
import type { Component } from "svelte";
import { writable } from "svelte/store";
type DialogType = "success" | "error" | "info" | "warning";
export interface Dialog {
type BaseDialog = {
id: number;
title: string;
message: string;
buttons: {
text: string;
action: () => void;
}[];
type: DialogType;
}
};
export type StringDialog = BaseDialog & {
message: string;
};
export type ComponentDialog<T = unknown> = BaseDialog & {
message: Component<DialogProps<T>>;
additional: T;
};
export type Dialog<T = unknown> = StringDialog | ComponentDialog<T>;
export type DialogProps<T = unknown> = {
id: number;
title: string;
type: DialogType;
buttons: {
text: string;
action: () => void;
}[];
additional: T;
};
const dialogs = writable<Dialog[]>([]);
@ -19,20 +41,33 @@ let dialogId = 0;
function addDialog(
title: string,
message: string,
buttons: Dialog["buttons"],
message: string | Component<DialogProps>,
buttons: BaseDialog["buttons"],
type: DialogType,
) {
additional?: unknown,
): number {
const id = dialogId++;
const newDialog: Dialog = {
id,
title,
message,
buttons,
type,
};
dialogs.update((currentDialogs) => [...currentDialogs, newDialog]);
if (typeof message === "string") {
const newDialog: StringDialog = {
id,
title,
message,
buttons,
type,
};
dialogs.update((currentDialogs) => [...currentDialogs, newDialog]);
} else {
const newDialog: ComponentDialog = {
id,
title,
message,
buttons,
type,
additional,
};
dialogs.update((currentDialogs) => [...currentDialogs, newDialog]);
}
return id;
}

View File

@ -8,6 +8,7 @@ import { addDialog } from "./DialogProvider";
import PQueue from "p-queue";
import { getLocale, setLocale } from "$lib/paraglide/runtime";
import { m } from "$lib/paraglide/messages";
import sanitizeHtml from "sanitize-html";
class Files {
public files = $state<VertFile[]>([]);
@ -373,3 +374,17 @@ export function link(
return result;
}
export function sanitize(
html: string,
allowedTags: string[] = ["a", "b", "code", "br"],
): string {
return sanitizeHtml(html, {
allowedTags: allowedTags,
allowedAttributes: {
a: ["href", "target", "rel", "class"],
"*": ["class"],
},
allowedSchemes: ["http", "https", "mailto", "blob"],
});
}

View File

@ -46,13 +46,9 @@
const dropFiles = (e: DragEvent) => {
e.preventDefault();
dropping.set(false);
if (page.url.pathname !== "/jpegify/") {
const oldLength = files.files.length;
files.add(e.dataTransfer?.files);
if (oldLength !== files.files.length) goto("/convert");
} else {
files.add(e.dataTransfer?.files);
}
const oldLength = files.files.length;
files.add(e.dataTransfer?.files);
if (oldLength !== files.files.length) goto("/convert");
};
const handleDrag = (e: DragEvent, drag: boolean) => {
@ -64,14 +60,9 @@
const clipboardData = e.clipboardData;
if (!clipboardData || !clipboardData.files.length) return;
e.preventDefault();
if (page.url.pathname !== "/jpegify/") {
const oldLength = files.files.length;
files.add(clipboardData.files);
if (oldLength !== files.files.length) goto("/convert");
} else {
files.add(clipboardData.files);
}
const oldLength = files.files.length;
files.add(clipboardData.files);
if (oldLength !== files.files.length) goto("/convert");
};
onMount(() => {
@ -156,6 +147,7 @@
/>
<meta property="twitter:image" content={featuredImage} />
<link rel="manifest" href="/manifest.json" />
<link rel="canonical" href="https://vert.sh/" />
{#if enablePlausible}
<script
defer

View File

@ -11,6 +11,7 @@
import "overlayscrollbars/overlayscrollbars.css";
import { onMount } from "svelte";
import type { WorkerStatus } from "$lib/converters/converter.svelte";
import { sanitize } from "$lib/store/index.svelte";
import { DISABLE_ALL_EXTERNAL_REQUESTS } from "$lib/consts";
const getSupportedFormats = (name: string) =>
@ -230,9 +231,9 @@
</p>
{/if}
<p>
{@html m["upload.cards.status.text"]({
{@html sanitize(m["upload.cards.status.text"]({
status: getStatusText(s.status),
})}
}))}
</p>
<div
class="flex flex-col items-center relative"

View File

@ -1,118 +0,0 @@
<script lang="ts">
import { flip } from "$lib/animation";
import Uploader from "$lib/components/functional/Uploader.svelte";
import Panel from "$lib/components/visual/Panel.svelte";
import { files } from "$lib/store/index.svelte";
import { quintOut } from "svelte/easing";
import { blur } from "svelte/transition";
import { m } from "$lib/paraglide/messages";
const images = $derived(
files.files.filter((f) =>
f.converters.map((c) => c.name).includes("imagemagick"),
),
);
let forcedBlobURLs = $state<Map<string, string>>(new Map());
const jpegify = () => {
const imgs = [...images];
imgs.map(async (f, i) => {
f.to = ".jpeg";
const result = await f.convert(compression);
if (!result) return;
forcedBlobURLs.set(f.id, URL.createObjectURL(result.file));
forcedBlobURLs = new Map([...forcedBlobURLs]);
});
};
let compressionInverted = $state(10);
const compression = $derived(100 - compressionInverted);
const processing = $derived(images.map((f) => f.processing).includes(true));
</script>
<svelte:head>
<meta name="robots" content="noindex" />
</svelte:head>
<div class="mx-auto w-full max-w-[778px] flex flex-col gap-8">
<h1 class="text-5xl text-center">{m["jpegify.title"]()}</h1>
<p class="text-muted text-center -mt-4 font-normal italic">
{m["jpegify.subtitle"]()}
</p>
<Uploader class="w-full h-64" jpegify={true} />
<input
type="range"
min="1"
max="100"
step="1"
class="w-full h-2 bg-panel rounded-lg appearance-none cursor-pointer"
bind:value={compressionInverted}
disabled={processing}
/>
<button
onclick={jpegify}
disabled={processing}
class="btn bg-accent text-black rounded-2xl text-2xl w-full mx-auto"
>{m["jpegify.button"]({ compression: compressionInverted })}</button
>
<div class="flex flex-wrap flex-row justify-center gap-4">
{#each images as file, i (file.id)}
<div
class="max-w-full w-full h-96"
animate:flip={{ duration: 400, easing: quintOut }}
transition:blur={{
duration: 400,
amount: 8,
easing: quintOut,
}}
>
<Panel class="w-full h-full flex flex-col gap-4 relative z-0">
<div
class="relative rounded-xl flex-grow overflow-hidden flex items-center justify-center"
>
<img
src={forcedBlobURLs.get(file.id) ||
file.result?.blobUrl ||
file.blobUrl}
alt={file.name}
class="h-full relative"
/>
<img
src={forcedBlobURLs.get(file.id) ||
file.result?.blobUrl ||
file.blobUrl}
alt={file.name}
class="h-full absolute top-0 left-0 w-full object-cover blur-2xl -z-10"
/>
</div>
<div class="flex-shrink-0 flex items-center gap-4 w-full">
<button
onclick={() => {
file?.download();
}}
disabled={!!!file.result}
class="btn bg-accent text-black rounded-2xl text-2xl w-full mx-auto"
>
{m["jpegify.download"]()}
</button>
<button
onclick={() => {
URL.revokeObjectURL(
forcedBlobURLs.get(file.id) || "",
);
forcedBlobURLs.delete(file.id);
files.files = files.files.filter(
(f) => f.id !== file.id,
);
}}
class="btn border-accent-red border-2 bg-transparent text-black dynadark:text-white rounded-2xl text-2xl w-full mx-auto"
>
{m["jpegify.delete"]()}
</button>
</div>
</Panel>
</div>
{/each}
</div>
</div>

View File

@ -0,0 +1,93 @@
<script lang="ts">
import { m } from "$lib/paraglide/messages";
import { link, sanitize } from "$lib/store/index.svelte";
import { ShieldCheckIcon } from "lucide-svelte";
</script>
<div class="flex flex-col h-full items-center">
<h1 class="hidden md:block text-[40px] tracking-tight leading-[72px] mb-6">
<ShieldCheckIcon size="40" class="inline-block -mt-2 mr-2" />
{m["privacy.title"]()}
</h1>
<div
class="w-full max-w-[1280px] flex flex-col md:flex-row gap-4 p-4 md:px-4 md:py-0"
>
<div class="bg-panel rounded-2xl p-6 shadow-panel text-lg font-normal">
<h2 class="text-2xl mb-3">{m["privacy.summary.title"]()}</h2>
<p class="mb-4">
{@html sanitize(
link(
["vert_link"],
m["privacy.summary.description"](),
["https://vert.sh"],
[true],
),
)}
</p>
<h2 class="text-2xl mb-3">{m["privacy.conversions.title"]()}</h2>
<p class="mb-4">
{@html sanitize(m["privacy.conversions.description"]())}
</p>
<h2 class="text-2xl mb-3">
{m["privacy.conversion_errors.title"]()}
</h2>
<div class="mb-4">
{m["privacy.conversion_errors.description"]()}
<ul class="list-disc list-inside mt-2 mb-2">
<li>{m["privacy.conversion_errors.list_job_id"]()}</li>
<li>{m["privacy.conversion_errors.list_format_from"]()}</li>
<li>{m["privacy.conversion_errors.list_format_to"]()}</li>
<li>{m["privacy.conversion_errors.list_stderr"]()}</li>
<li>{m["privacy.conversion_errors.list_video"]()}</li>
</ul>
{m["privacy.conversion_errors.footer"]()}
</div>
<h3 class="text-xl mt-4 mb-2">{m["privacy.analytics.title"]()}</h3>
<p class="mb-4">
{@html sanitize(
link(
["settings_link", "plausible_link"],
m["privacy.analytics.description"](),
[
"/settings",
"https://plausible.io/privacy-focused-web-analytics",
],
[false, true],
),
)}
</p>
<h3 class="text-xl mt-4 mb-2">
{m["privacy.local_storage.title"]()}
</h3>
<p class="mb-4">
{@html sanitize(
link(
["settings_link"],
m["privacy.local_storage.description"](),
["/settings"],
[false],
),
)}
</p>
<h3 class="text-xl mt-4 mb-2">{m["privacy.contact.title"]()}</h3>
<p class="mb-0">
{@html sanitize(
link(
["email_link"],
m["privacy.contact.description"](),
["mailto:hello@vert.sh"],
[false],
),
)}
</p>
<p class="text-sm text-muted mt-6">{m["privacy.last_updated"]()}</p>
</div>
</div>
</div>

15
static/robots.txt Normal file
View File

@ -0,0 +1,15 @@
User-agent: *
Allow: /
# language urls (doesn't actually change language when visiting)
# should prob be auto-generated?
Disallow: /es/
Disallow: /fr/
Disallow: /de/
Disallow: /it/
Disallow: /hr/
Disallow: /tr/
Disallow: /ja/
Disallow: /el/
Sitemap: https://vert.sh/sitemap.xml

32
static/sitemap.xml Normal file
View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url>
<loc>https://vert.sh/</loc>
<lastmod>2025-10-17T19:23:05+00:00</lastmod>
<priority>1.00</priority>
</url>
<url>
<loc>https://vert.sh/convert/</loc>
<lastmod>2025-10-17T19:23:05+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://vert.sh/settings/</loc>
<lastmod>2025-10-17T19:23:05+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://vert.sh/about/</loc>
<lastmod>2025-10-17T19:23:05+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://vert.sh/privacy/</loc>
<lastmod>2025-10-17T19:23:05+00:00</lastmod>
<priority>0.80</priority>
</url>
</urlset>