From c4d44269779dd8973348f99397b4410a19d707cd Mon Sep 17 00:00:00 2001 From: TheFrostBunny Date: Thu, 29 Jan 2026 15:08:09 +0100 Subject: [PATCH 001/118] feat(i18n): add Norwegian language support --- messages/no.json | 222 ++++++++++++++++++++++++++++++++++ project.inlang/settings.json | 1 + src/lib/store/index.svelte.ts | 1 + 3 files changed, 224 insertions(+) create mode 100644 messages/no.json diff --git a/messages/no.json b/messages/no.json new file mode 100644 index 00000000..befe23e8 --- /dev/null +++ b/messages/no.json @@ -0,0 +1,222 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "navbar": { + "upload": "Last opp", + "convert": "Konverter", + "settings": "Innstillinger", + "about": "Om", + "toggle_theme": "Bytt tema" + }, + "footer": { + "copyright": "© {year} VERT.", + "source_code": "Kildekode", + "discord_server": "Discord-server", + "privacy_policy": "Personvern" + }, + "upload": { + "title": "Filkonvertereren du vil elske.", + "subtitle": "All bilde-, lyd- og dokumentbehandling skjer på enheten din. Videoer konverteres på våre lynraske servere. Ingen filstørrelsesgrense, ingen annonser, og helt åpen kildekode.", + "uploader": { + "text": "Slipp eller klikk for å {action}", + "convert": "konvertere" + }, + "cards": { + "title": "VERT støtter...", + "images": "Bilder", + "audio": "Lyd", + "documents": "Dokumenter", + "video": "Video", + "video_server_processing": "Serverstøttet", + "local_supported": "Lokal støtte", + "status": { + "text": "Status: {status}", + "ready": "klar", + "not_ready": "ikke klar", + "not_initialized": "ikke initialisert", + "downloading": "laster ned...", + "initializing": "initialiserer...", + "unknown": "ukjent status" + }, + "supported_formats": "Støttede formater:" + }, + "tooltip": { + "partial_support": "Dette formatet kan bare konverteres som {direction}.", + "direction_input": "input (fra)", + "direction_output": "output (til)", + "video_server_processing": "Videoer lastes opp til en server for behandling som standard, lær hvordan du setter det opp lokalt her." + } + }, + "convert": { + "panel": { + "convert_all": "Konverter alle", + "download_all": "Last ned alle som .zip", + "remove_all": "Fjern alle filer", + "set_all_to": "Sett alle til", + "na": "I/T" + }, + "dropdown": { + "audio": "Lyd", + "video": "Video", + "doc": "Dokument", + "image": "Bilde", + "placeholder": "Søk format" + }, + "tooltips": { + "unknown_file": "Ukjent filtype", + "audio_file": "Lydfil", + "video_file": "Videofil", + "document_file": "Dokumentfil", + "image_file": "Bildefil", + "convert_file": "Konverter denne filen", + "download_file": "Last ned denne filen" + }, + "errors": { + "cant_convert": "Vi kan ikke konvertere denne filen.", + "vertd_server": "hva gjør du..? du skal kjøre vertd-serveren!", + "unsupported_format": "Kun bilde-, video-, lyd- og dokumentfiler støttes", + "vertd_not_found": "Kunne ikke finne vertd-instansen for å starte videokonvertering. Er du sikker på at instans-URL-en er satt riktig?", + "worker_downloading": "{type}-konverteren initialiseres for øyeblikket, vennligst vent noen øyeblikk.", + "worker_error": "{type}-konverteren fikk en feil under initialisering, prøv igjen senere.", + "worker_timeout": "{type}-konverteren tar lengre tid enn forventet å initialisere, vennligst vent litt til eller oppdater siden.", + "audio": "lyd", + "doc": "dokument", + "image": "bilde" + } + }, + "settings": { + "title": "Innstillinger", + "errors": { + "save_failed": "Kunne ikke lagre innstillinger!" + }, + "appearance": { + "title": "Utseende", + "brightness_theme": "Lysstyrketema", + "brightness_description": "Vil du ha et solfylt smell, eller en rolig ensom natt?", + "light": "Lyst", + "dark": "Mørkt", + "effect_settings": "Effektinnstillinger", + "effect_description": "Vil du ha fancy effekter, eller en mer statisk opplevelse?", + "enable": "Aktiver", + "disable": "Deaktiver" + }, + "conversion": { + "title": "Konvertering", + "advanced_settings": "Avanserte innstillinger", + "filename_format": "Filnavnformat", + "filename_description": "Dette bestemmer navnet på filen ved nedlasting, uten filendelsen. Du kan bruke følgende maler i formatet, som vil bli erstattet med relevant informasjon: %name% for det opprinnelige filnavnet, %extension% for den opprinnelige filendelsen, og %date% for en dato når filen ble konvertert.", + "placeholder": "VERT_%name%", + "default_format": "Standard konverteringsformat", + "default_format_enable": "Aktiver", + "default_format_disable": "Deaktiver", + "default_format_description": "Dette endrer standardformatet som velges når du laster opp en fil av denne filtypen.", + "default_format_image": "Bilder", + "default_format_video": "Videoer", + "default_format_audio": "Lyd", + "default_format_document": "Dokumenter", + "metadata": "Filmetadata", + "metadata_description": "Dette endrer om metadata (EXIF, sanginfo, osv.) på den opprinnelige filen beholdes i konverterte filer.", + "keep": "Behold", + "remove": "Fjern", + "quality": "Konverteringskvalitet", + "quality_description": "Dette endrer standard utgangskvalitet for konverterte filer (i sin kategori). Høyere verdier kan føre til lengre konverteringstid og filstørrelse.", + "quality_video": "Dette endrer standard utgangskvalitet for konverterte videofiler. Høyere verdier kan føre til lengre konverteringstid og filstørrelse.", + "quality_audio": "Lyd (kbps)", + "quality_images": "Bilde (%)", + "rate": "Samplingsfrekvens (Hz)" + }, + "vertd": { + "title": "Videokonvertering", + "status": "status:", + "loading": "laster...", + "available": "tilgjengelig, commit id {commitId}", + "unavailable": "utilgjengelig (er url-en riktig?)", + "description": "vertd-prosjektet er en server-wrapper for FFmpeg. Dette lar deg konvertere videoer gjennom VERTs webgrensesnitt, samtidig som du kan bruke kraften til GPU-en din for å gjøre det så raskt som mulig.", + "hosting_info": "Vi hoster en offentlig instans for din bekvemmelighet, men det er ganske enkelt å hoste din egen på PC-en eller serveren din hvis du vet hva du gjør. Du kan laste ned server-binærfiler [vertd_link]her[/vertd_link] - prosessen med å sette dette opp vil bli enklere i fremtiden, så følg med!", + "instance": "Instans", + "url_placeholder": "Eksempel: http://localhost:24153", + "conversion_speed": "Konverteringshastighet", + "speed_description": "Dette beskriver avveiningen mellom hastighet og kvalitet. Raskere hastigheter gir lavere kvalitet, men får jobben gjort raskere.", + "speeds": { + "very_slow": "Veldig treg", + "slower": "Tregere", + "slow": "Treg", + "medium": "Middels", + "fast": "Rask", + "ultra_fast": "Ekstremt rask" + } + }, + "privacy": { + "title": "Personvern og data", + "plausible_title": "Plausible-analyse", + "plausible_description": "Vi bruker [plausible_link]Plausible[/plausible_link], et personvernfokusert analyseverktøy, for å samle helt anonyme statistikker. All data er anonymisert og aggregert, og ingen identifiserbar informasjon blir noen gang sendt eller lagret. Du kan se analysen [analytics_link]her[/analytics_link] og velge å melde deg ut nedenfor.", + "opt_in": "Meld inn", + "opt_out": "Meld ut" + }, + "language": { + "title": "Språk", + "description": "Velg ønsket språk for VERT-grensesnittet." + } + }, + "about": { + "title": "Om", + "why": { + "title": "Hvorfor VERT?", + "description": "Filkonverterere har alltid skuffet oss. De er stygge, fulle av annonser, og viktigst av alt; trege. Vi bestemte oss for å løse dette en gang for alle ved å lage et alternativ som løser alle disse problemene, og mer.

Alle ikke-video-filer konverteres helt på enheten; dette betyr at det ikke er noen forsinkelse mellom sending og mottak av filer fra en server, og vi får aldri tilgang til filene du konverterer.

Videofiler lastes opp til vår lynraske RTX 4000 Ada-server. Videoene dine blir liggende der i en time hvis du ikke konverterer dem. Hvis du konverterer filen, blir videoen liggende på serveren i en time, eller til den lastes ned. Filen blir deretter slettet fra serveren vår." + }, + "sponsors": { + "title": "Sponsorer", + "description": "Vil du støtte oss? Kontakt en utvikler i [discord_link]Discord[/discord_link]-serveren, eller send en e-post til", + "email_copied": "E-post kopiert til utklippstavlen!" + }, + "resources": { + "title": "Ressurser", + "discord": "Discord", + "source": "Kilde", + "email": "E-post" + }, + "donate": { + "title": "Doner til VERT", + "description": "Med din støtte kan vi fortsette å vedlikeholde og forbedre VERT.", + "one_time": "Engangs", + "monthly": "Månedlig", + "custom": "Egendefinert", + "pay_now": "Betal nå", + "donate_amount": "Doner ${amount} USD", + "thank_you": "Takk for donasjonen!", + "payment_failed": "Betaling mislyktes: {message}{period} Du er ikke belastet.", + "donation_error": "En feil oppstod under behandlingen av donasjonen. Prøv igjen senere.", + "payment_error": "Feil ved henting av betalingsdetaljer. Prøv igjen senere." + }, + "credits": { + "title": "Kreditering", + "contact_team": "Hvis du vil kontakte utviklingsteamet, bruk e-posten som finnes på \"Ressurser\"-kortet.", + "notable_contributors": "Merkbare bidragsytere", + "notable_description": "Vi vil takke disse personene for deres store bidrag til VERT.", + "github_contributors": "GitHub-bidragsytere", + "github_description": "Stor takk til alle disse for å hjelpe til! [github_link]Vil du også hjelpe?[/github_link]", + "no_contributors": "Ser ut som ingen har bidratt ennå... [contribute_link]bli den første til å bidra![/contribute_link]", + "libraries": "Biblioteker", + "libraries_description": "En stor takk til FFmpeg (lyd, video), ImageMagick (bilder) og Pandoc (dokumenter) for å vedlikeholde slike utmerkede biblioteker i så mange år. VERT er avhengig av dem for å gi deg konverteringene dine.", + "roles": { + "lead_developer": "Hovedutvikler; konverteringsbackend, UI-implementering", + "developer": "Utvikler; UI-implementering", + "designer": "Designer; UX, merkevarebygging, markedsføring", + "docker_ci": "Vedlikehold av Docker & CI-støtte", + "former_cofounder": "Tidligere medgründer & designer" + } + }, + "errors": { + "github_contributors": "Feil ved henting av GitHub-bidragsytere" + } + }, + "workers": { + "errors": { + "general": "Feil ved konvertering av {file}: {message}", + "cancel": "Feil ved avbrytelse av konvertering for {file}: {message}", + "magick": "Feil i Magick-arbeider, bildekovertering fungerer kanskje ikke som forventet.", + "ffmpeg": "Feil ved lasting av FFmpeg, noen funksjoner fungerer kanskje ikke som forventet.", + "no_audio": "Ingen lydstrøm funnet.", + "invalid_rate": "Ugyldig samplingsfrekvens angitt: {rate}Hz" + } + } +} \ No newline at end of file diff --git a/project.inlang/settings.json b/project.inlang/settings.json index 9e09cb91..2e726bc7 100644 --- a/project.inlang/settings.json +++ b/project.inlang/settings.json @@ -12,6 +12,7 @@ "tr", "ja", "ko", + "no", "el", "id", "zh-Hans", diff --git a/src/lib/store/index.svelte.ts b/src/lib/store/index.svelte.ts index 318c6bda..11851e4d 100644 --- a/src/lib/store/index.svelte.ts +++ b/src/lib/store/index.svelte.ts @@ -450,6 +450,7 @@ export const availableLocales = { tr: "Türkçe", ja: "日本語", ko: "한국어", + no: "Norsk", el: "Ελληνικά", "zh-Hans": "简体中文", "zh-Hant": "繁體中文", From 1a7ea051e630a0f6c51639bd6a092f3883c8affe Mon Sep 17 00:00:00 2001 From: wx Date: Thu, 5 Feb 2026 12:38:42 +0100 Subject: [PATCH 002/118] feat: add czech language --- messages/cs.json | 325 ++++++++++++++++++++++++++++++++++ project.inlang/settings.json | 1 + src/lib/store/index.svelte.ts | 1 + 3 files changed, 327 insertions(+) create mode 100644 messages/cs.json diff --git a/messages/cs.json b/messages/cs.json new file mode 100644 index 00000000..7333315f --- /dev/null +++ b/messages/cs.json @@ -0,0 +1,325 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "navbar": { + "upload": "Nahrát", + "convert": "Konvertovat", + "settings": "Nastavení", + "about": "O aplikaci", + "toggle_theme": "Přepnout vzhled" + }, + "footer": { + "copyright": "© {year} VERT.", + "source_code": "Zdrojový kód", + "discord_server": "Discord server", + "privacy_policy": "Zásady ochrany osobních údajů" + }, + "upload": { + "title": "Konvertor souborů, který si zamilujete.", + "subtitle": "Veškeré zpracování obrázků, audia a dokumentů probíhá na vašem zařízení. Videa jsou konvertována na našich bleskově rychlých serverech. Žádné omezení velikosti souborů, bez reklam a kompletně open-source.", + "uploader": { + "text": "Přetáhněte soubor nebo klikněte pro {action}", + "convert": "konvertování" + }, + "cards": { + "title": "VERT podporuje...", + "images": "Obrázky", + "audio": "Audio", + "documents": "Dokumenty", + "video": "Video", + "video_server_processing": "Podporováno na serveru", + "local_supported": "Lokálně podporováno", + "status": { + "text": "Stav: {status}", + "ready": "připraveno", + "not_ready": "není připraveno", + "not_initialized": "nespuštěno", + "downloading": "stahování...", + "initializing": "spouštění...", + "unknown": "neznámý stav" + }, + "supported_formats": "Podporované formáty:" + }, + "tooltip": { + "partial_support": "Tento formát lze konvertovat pouze jako {direction}.", + "direction_input": "vstup (z)", + "direction_output": "výstup (do)", + "video_server_processing": "Video se standardně nahrává na server ke zpracování. Zjistěte, jak to nastavit lokálně zde." + } + }, + "convert": { + "archive_file": { + "extract": "Rozbalit archiv", + "extracting": "Detekován archiv: {filename}", + "extracted": "Rozbaleno {extract_count} souborů z {filename}. {ignore_count} položek bylo ignorováno.", + "detected": "Detekovány {type} soubory v {filename}.", + "audio": "audio", + "video": "video", + "doc": "dokument", + "image": "obrázek", + "extract_error": "Chyba při rozbalování {filename}: {error}" + }, + "large_file_warning": "Kvůli omezením prohlížeče/zařízení je konverze videa na audio pro tento soubor zakázána, protože je větší než {limit} GB. Doporučujeme použít Firefox nebo Safari pro soubory této velikosti, které mají méně omezení.", + "external_warning": { + "title": "Upozornění na externí server", + "text": "Pokud se rozhodnete konvertovat do video formátu, tyto soubory budou nahrány na externí server ke konverzi. Chcete pokračovat?", + "yes": "Ano", + "no": "Ne" + }, + "panel": { + "convert_all": "Konvertovat vše", + "download_all": "Stáhnout vše jako .zip", + "remove_all": "Odebrat všechny soubory", + "set_all_to": "Nastavit vše na", + "na": "Nedostupné" + }, + "dropdown": { + "audio": "Audio", + "video": "Video", + "doc": "Dokument", + "image": "Obrázek", + "placeholder": "Hledat formát", + "no_formats": "Žádné formáty k dispozici", + "no_results": "Žádné formáty neodpovídají vašemu hledání" + }, + "tooltips": { + "unknown_file": "Neznámý typ souboru", + "audio_file": "Audio soubor", + "video_file": "Video soubor", + "document_file": "Dokument", + "image_file": "Obrázek", + "convert_file": "Konvertovat tento soubor", + "download_file": "Stáhnout tento soubor" + }, + "errors": { + "cant_convert": "Tento soubor nelze konvertovat.", + "vertd_server": "co to děláte...? měli byste spustit vertd server!", + "vertd_generic_view": "Zobrazit podrobnosti chyby", + "vertd_generic_body": "Při pokusu o konverzi vašeho videa došlo k chybě. Chcete odeslat toto video vývojářům, abyste pomohli opravit tuto chybu? Bude odeslán pouze váš video soubor. Žádné identifikátory nebudou nahrány.", + "vertd_generic_title": "Chyba při konverzi videa", + "vertd_generic_yes": "Odeslat video", + "vertd_generic_no": "Neposílat", + "vertd_failed_to_keep": "Selhalo uchování videa na serveru: {error}", + "vertd_details": "Zobrazit podrobnosti chyby", + "vertd_details_body": "Pokud stisknete odeslat, vaše video bude také připojeno spolu s protokolem chyb, který je nám vždy hlášen ke kontrole. Následující informace jsou protokol, který automaticky obdržíme:", + "vertd_details_footer": "Tyto informace budou použity pouze pro účely řešení problémů a nebudou nikdy sdíleny. Prohlédněte si naše [privacy_link]zásady ochrany osobních údajů[/privacy_link] pro více informací.", + "vertd_details_job_id": "ID úlohy: {jobId}", + "vertd_details_from": "Z formátu: {from}", + "vertd_details_to": "Do formátu: {to}", + "vertd_details_error_message": "Chybová zpráva: [view_link]Zobrazit protokol chyb[/view_link]", + "vertd_details_close": "Zavřít", + "vertd_ratelimit": "Vaše video '{filename}' se nepodařilo konvertovat několikrát. Aby nedošlo k přetížení serveru, další pokusy o konverzi tohoto souboru byly dočasně zablokovány. Zkuste to prosím později.", + "unsupported_format": "Podporovány jsou pouze soubory obrázků, videa, audia a dokumentů", + "format_output_only": "Tento formát lze v současné době použít pouze jako výstup (konvertovat do), nikoli jako vstup.", + "vertd_not_found": "Nelze najít instanci vertd pro zahájení konverze videa. Jste si jisti, že je URL instance nastavena správně?", + "worker_downloading": "Konvertor {type} se právě inicializuje, počkejte prosím chvíli.", + "worker_error": "Konvertor {type} měl při inicializaci chybu, zkuste to prosím později.", + "worker_timeout": "Inicializace konvertoru {type} trvá déle než obvykle, počkejte prosím ještě chvíli nebo obnovte stránku.", + "audio": "audia", + "doc": "dokumentů", + "image": "obrázků" + } + }, + "settings": { + "title": "Nastavení", + "errors": { + "save_failed": "Selhalo uložení nastavení!" + }, + "appearance": { + "title": "Vzhled", + "brightness_theme": "Barevný motiv", + "brightness_description": "Chcete slunečný blesk, nebo tiché osamělé noci?", + "light": "Světlý", + "dark": "Tmavý", + "effect_settings": "Nastavení efektů", + "effect_description": "Chcete efektní efekty, nebo statičtější prostředí?", + "enable": "Zapnout", + "disable": "Vypnout" + }, + "conversion": { + "title": "Konverze", + "advanced_settings": "Pokročilá nastavení", + "filename_format": "Formát názvu souboru", + "filename_description": "Toto určí název souboru při stažení, bez přípony souboru. Do formátu můžete vložit následující šablony, které budou nahrazeny příslušnými informacemi: %name% pro původní název souboru, %extension% pro původní příponu souboru a %date% pro datum, kdy byl soubor konvertován.", + "placeholder": "VERT_%name%", + "default_format": "Výchozí formát konverze", + "default_format_enable": "Zapnout", + "default_format_disable": "Vypnout", + "default_format_description": "Toto změní výchozí formát vybraný při nahrání souboru tohoto typu.", + "default_format_image": "Obrázky", + "default_format_video": "Videa", + "default_format_audio": "Audio", + "default_format_document": "Dokumenty", + "metadata": "Metadata souboru", + "metadata_description": "Toto mění, zda budou metadata (EXIF, informace o skladbě atd.) z původního souboru zachována v konvertovaných souborech.", + "keep": "Zachovat", + "remove": "Odebrat", + "quality": "Kvalita konverze", + "quality_description": "Toto mění výchozí výstupní kvalitu konvertovaných souborů (v jejich kategorii). Vyšší hodnoty mohou vést k delšímu času konverze a větší velikosti souboru.", + "quality_video": "Toto mění výchozí výstupní kvalitu konvertovaných video souborů. Vyšší hodnoty mohou vést k delšímu času konverze a větší velikosti souboru.", + "quality_audio": "Audio (kbps)", + "quality_images": "Obrázek (%)", + "rate": "Vzorkovací frekvence (Hz)" + }, + "vertd": { + "title": "Konverze videa", + "status": "stav:", + "loading": "načítání...", + "available": "dostupné, commit id {commitId}", + "unavailable": "nedostupné (je URL správná?)", + "description": "Projekt vertd je serverový obal pro FFmpeg. To vám umožňuje konvertovat videa prostřednictvím pohodlného webového rozhraní VERT a zároveň využívat sílu vašeho GPU, abyste to udělali co nejrychleji.", + "hosting_info": "Pro vaše pohodlí hostujeme veřejnou instanci, ale pokud víte, co děláte, je docela snadné si ji hostovat sami na vašem PC nebo serveru. Serverové binárky si můžete stáhnout [vertd_link]zde[/vertd_link] - proces nastavení bude v budoucnu jednodušší, takže zůstaňte naladěni!", + "instance": "Instance", + "url_placeholder": "Příklad: http://localhost:24153", + "conversion_speed": "Rychlost konverze", + "speed_description": "Toto popisuje kompromis mezi rychlostí a kvalitou. Vyšší rychlosti povedou k nižší kvalitě, ale práci udělají rychleji.", + "speeds": { + "very_slow": "Velmi pomalá", + "slower": "Pomalejší", + "slow": "Pomalá", + "medium": "Střední", + "fast": "Rychlá", + "ultra_fast": "Ultra rychlá" + }, + "auto_instance": "Automaticky (doporučeno)", + "eu_instance": "Falkenstein, Německo", + "us_instance": "Washington, USA", + "custom_instance": "Vlastní" + }, + "privacy": { + "title": "Soukromí a data", + "plausible_title": "Plausible analytika", + "plausible_description": "Používáme [plausible_link]Plausible[/plausible_link], nástroj pro analytiku zaměřený na ochranu soukromí, ke shromažďování zcela anonymních statistik. Všechna data jsou anonymizována a agregována a nikdy nejsou odesílány nebo ukládány žádné identifikovatelné informace. Analytiku můžete zobrazit [analytics_link]zde[/analytics_link] a níže se můžete rozhodnout odhlásit.", + "opt_in": "Souhlasím", + "opt_out": "Odhlásit se", + "cache_title": "Správa mezipaměti", + "cache_description": "Ukládáme soubory konvertoru do mezipaměti vašeho prohlížeče, abyste je nemuseli stahovat pokaždé znovu, což zlepšuje výkon a snižuje využití dat.", + "refresh_cache": "Obnovit mezipaměť", + "clear_cache": "Vymazat mezipaměť", + "files_cached": "{size} ({count} souborů)", + "loading_cache": "Načítání...", + "total_size": "Celková velikost", + "files_cached_label": "Souborů v mezipaměti", + "cache_cleared": "Mezipaměť byla úspěšně vymazána!", + "cache_clear_error": "Selhalo vymazání mezipaměti.", + "site_data_title": "Správa dat webu", + "site_data_description": "Vymažte všechna data webu včetně nastavení a souborů v mezipaměti, resetujte VERT do výchozího stavu a znovu načtěte stránku.", + "clear_all_data": "Vymazat všechna data webu", + "clear_all_data_confirm_title": "Vymazat všechna data webu?", + "clear_all_data_confirm": "Toto resetuje všechna nastavení a mezipaměť a poté znovu načte stránku. Tuto akci nelze vrátit zpět.", + "clear_all_data_cancel": "Zrušit", + "all_data_cleared": "Všechna data webu byla vymazána! Načítání stránky...", + "all_data_clear_error": "Selhalo vymazání všech dat webu." + }, + "language": { + "title": "Jazyk", + "description": "Vyberte preferovaný jazyk pro rozhraní VERT." + } + }, + "about": { + "title": "O aplikaci", + "why": { + "title": "Proč VERT?", + "description": "Konvertory souborů nás vždy zklamaly. Jsou ošklivé, plné reklam a především; pomalé. Rozhodli jsme se tento problém vyřešit jednou provždy vytvořením alternativy, která řeší všechny tyto problémy a ještě víc.

Všechny ne-video soubory jsou konvertovány kompletně na vašem zařízení; to znamená, že není žádné zpoždění mezi odesláním a přijetím souborů ze serveru a my nikdy nemůžeme špehovat soubory, které konvertujete.

Video soubory jsou nahrány na náš bleskově rychlý server s RTX 4000 Ada. Vaše videa tam zůstanou hodinu, pokud je nekonvertujete. Pokud soubor konvertujete, video zůstane na serveru hodinu nebo dokud nebude staženo. Poté bude soubor z našeho serveru smazán." + }, + "sponsors": { + "title": "Sponzoři", + "description": "Chcete nás podpořit? Kontaktujte vývojáře na [discord_link]Discord[/discord_link] serveru nebo pošlete e-mail na", + "email_copied": "E-mail zkopírován do schránky!" + }, + "resources": { + "title": "Zdroje", + "discord": "Discord", + "source": "Zdrojový kód", + "email": "E-mail" + }, + "donate": { + "title": "Přispějte projektu VERT", + "description": "S vaší podporou můžeme pokračovat v údržbě a vylepšování VERT.", + "one_time": "Jednorázově", + "monthly": "Měsíčně", + "custom": "Vlastní částka", + "pay_now": "Zaplatit nyní", + "donate_amount": "Přispět ${amount} USD", + "thank_you": "Děkujeme za váš příspěvek!", + "payment_failed": "Platba selhala: {message}{period} Nebyli jste účtováni.", + "donation_error": "Při zpracování vašeho příspěvku došlo k chybě. Zkuste to prosím později.", + "payment_error": "Chyba při načítání platebních údajů. Zkuste to prosím později.", + "donation_notice_official": "Vaše příspěvky zde jdou oficiální instanci VERT (vert.sh) a pomáhají podporovat vývoj projektu.", + "donation_notice_unofficial": "Vaše příspěvky zde jdou provozovateli této instance VERT. Pokud chcete podpořit oficiální vývojáře VERT, navštivte prosím místo toho [official_link]vert.sh[/official_link]." + }, + "credits": { + "title": "Tvůrci", + "contact_team": "Pokud byste chtěli kontaktovat vývojový tým, použijte prosím e-mail uvedený na kartě \"Zdroje\".", + "notable_contributors": "Významní přispěvatelé", + "notable_description": "Chtěli bychom poděkovat těmto lidem za jejich významný přínos projektu VERT.", + "github_contributors": "Přispěvatelé na GitHubu", + "github_description": "Velké díky všem těmto lidem za pomoc! [github_link]Chcete pomoci také?[/github_link]", + "no_contributors": "Zdá se, že zatím nikdo nepřispěl... [contribute_link]buďte první, kdo přispěje![/contribute_link]", + "libraries": "Knihovny", + "libraries_description": "Velké díky FFmpeg (audio, video), ImageMagick (obrázky) a Pandoc (dokumenty) za udržování tak vynikajících knihoven po tolik let. VERT na nich spoléhá, aby vám mohl poskytovat vaše konverze.", + "roles": { + "lead_developer": "Vedoucí vývojář; backend konverze, implementace UI", + "developer": "Vývojář; implementace UI", + "designer": "Designer; UX, branding, marketing", + "docker_ci": "Údržba podpory Docker a CI", + "former_cofounder": "Bývalý spoluzakladatel a designer" + } + }, + "errors": { + "github_contributors": "Chyba při načítání přispěvatelů z GitHubu" + } + }, + "workers": { + "errors": { + "general": "Chyba při konverzi {file}: {message}", + "cancel": "Chyba při zrušení konverze {file}: {message}", + "magick": "Chyba v Magick workeru, konverze obrázků nemusí fungovat podle očekávání.", + "ffmpeg": "Chyba při načítání FFmpeg, některé funkce nemusí fungovat podle očekávání.", + "pandoc": "Chyba při načítání Pandoc workeru, konverze dokumentů nemusí fungovat podle očekávání.", + "no_audio": "Nebyl nalezen žádný audio stream.", + "invalid_rate": "Byla zadána neplatná vzorkovací frekvence: {rate}Hz", + "file_too_large": "Tento soubor překračuje limit prohlížeče/zařízení {limit} GB. Zkuste Firefox nebo Safari pro konverzi tohoto velkého souboru, které obvykle mají vyšší limity." + } + }, + "privacy": { + "title": "Zásady ochrany osobních údajů", + "summary": { + "title": "Shrnutí", + "description": "Zásady ochrany osobních údajů VERT jsou velmi jednoduché: vůbec neshromažďujeme ani neukládáme žádná data o vás. Nepoužíváme cookies ani sledovací nástroje, analytika je zcela soukromá a všechny konverze (kromě videí) probíhají lokálně ve vašem prohlížeči. Videa jsou smazána po stažení nebo po hodině, pokud nám výslovně nedáte svolení k jejich uložení; budou použita pouze pro účely řešení problémů. VERT samostatně hostuje instanci Coolify pro hostování webové stránky a vertd (pro konverzi videa) a instanci Plausible pro zcela anonymní a agregovanou analytiku. K zpracování příspěvků používáme Stripe, který může shromažďovat některá data používaná pro prevenci podvodů.

Poznámka: toto se může vztahovat pouze na oficiální instanci VERT na [vert_link]vert.sh[/vert_link]; instance třetích stran mohou s vašimi daty zacházet odlišně." + }, + "conversions": { + "title": "Konverze", + "description": "Většina konverzí (obrázky, dokumenty, audio) probíhá zcela lokálně na vašem zařízení pomocí WebAssembly verzí příslušných nástrojů (např. ImageMagick, Pandoc, FFmpeg). To znamená, že vaše soubory nikdy neopustí vaše zařízení a my k nim nikdy nebudeme mít přístup.

Konverze videí se provádějí na našich serverech, protože vyžadují větší výpočetní výkon a v prohlížeči je zatím nelze provádět velmi rychle. Videa, která konvertujete pomocí VERT, jsou smazána po stažení nebo po jedné hodině, pokud nám výslovně nedáte svolení k jejich uložení na delší dobu čistě pro účely řešení problémů." + }, + "donations": { + "title": "Příspěvky", + "description": "Na stránce [about_link]o aplikaci[/about_link] používáme Stripe ke shromažďování příspěvků. Stripe může shromažďovat určité informace o platbě a zařízení pro prevenci podvodů, jak je popsáno v [stripe_link]jejich dokumentaci o pokročilé detekci podvodů[/stripe_link]. Externí síťové požadavky na Stripe jsou odloženy a jsou provedeny až poté, co kliknete na tlačítko k platbě." + }, + "conversion_errors": { + "title": "Chyby konverze", + "description": "Když se konverze videa nezdaří, můžeme shromažďovat některá anonymní data, která nám pomohou diagnostikovat problém. Tato data mohou zahrnovat:", + "list_job_id": "ID úlohy, což je anonymizovaný název souboru", + "list_format_from": "Formát, ze kterého jste konvertovali", + "list_format_to": "Formát, do kterého jste konvertovali", + "list_stderr": "Výstup FFmpeg stderr z vaší úlohy (chybová zpráva)", + "list_video": "Skutečný video soubor (pokud je uděleno výslovné svolení)", + "footer": "Tyto informace jsou používány pouze pro účely diagnostiky problémů s konverzí. Skutečný video soubor bude shromážděn pouze tehdy, pokud nám k tomu udělíte svolení, kde bude použit pouze pro řešení problémů." + }, + "analytics": { + "title": "Analytika", + "description": "Samostatně hostujeme instanci Plausible pro zcela anonymní a agregovanou analytiku. Plausible nepoužívá cookies a dodržuje všechny hlavní předpisy o ochraně osobních údajů (GDPR/CCPA/PECR). Z analytiky se můžete odhlásit v sekci \"Soukromí a data\" v [settings_link]nastavení[/settings_link] a přečíst si více o postupech ochrany soukromí Plausible [plausible_link]zde[/plausible_link]." + }, + "local_storage": { + "title": "Lokální úložiště", + "description": "Používáme lokální úložiště vašeho prohlížeče k uložení vašich nastavení a úložiště relace vašeho prohlížeče k dočasnému uložení seznamu přispěvatelů z GitHubu pro sekci \"O aplikaci\", abychom snížili opakované požadavky na GitHub API. Žádná osobní data nejsou ukládána ani přenášena.

WebAssembly verze konverzních nástrojů, které používáme (FFmpeg, ImageMagick, Pandoc), jsou také uloženy lokálně ve vašem prohlížeči při první návštěvě webové stránky, takže je nemusíte stahovat při každé návštěvě. Žádná osobní data nejsou ukládána ani přenášena. Tato data můžete kdykoli zobrazit nebo smazat v sekci \"Soukromí a data\" v [settings_link]nastavení[/settings_link]." + }, + "contact": { + "title": "Kontakt", + "description": "Pro dotazy nám napište e-mail na: [email_link]hello@vert.sh[/email_link]. Pokud používáte instanci VERT třetí strany, kontaktujte prosím místo toho hostitele této instance." + }, + "last_updated": "Naposledy aktualizováno: 29. 10. 2025" + }, + "toast": { + "insecure_context": "Navštěvujete VERT v nezabezpečeném kontextu (např. přístup přes HTTP místo HTTPS). Některé funkce nemusí fungovat podle očekávání." + } +} diff --git a/project.inlang/settings.json b/project.inlang/settings.json index 9e09cb91..16787d39 100644 --- a/project.inlang/settings.json +++ b/project.inlang/settings.json @@ -2,6 +2,7 @@ "$schema": "https://inlang.com/schema/project-settings", "baseLocale": "en", "locales": [ + "cs", "en", "es", "fr", diff --git a/src/lib/store/index.svelte.ts b/src/lib/store/index.svelte.ts index 318c6bda..dced5850 100644 --- a/src/lib/store/index.svelte.ts +++ b/src/lib/store/index.svelte.ts @@ -439,6 +439,7 @@ export const effects = writable(true); export const theme = writable<"light" | "dark">("light"); export const locale = writable(getLocale()); export const availableLocales = { + cs: "Čeština", en: "English", es: "Español", fr: "Français", From 6d1974ae88d28f3f33624fc51e0673854ce123ae Mon Sep 17 00:00:00 2001 From: TheFrostBunny Date: Sun, 8 Feb 2026 22:19:36 +0100 Subject: [PATCH 003/118] feat(i18n): update Norwegian language translations --- messages/no.json | 543 ++++++++++++++++++++++++++++------------------- 1 file changed, 323 insertions(+), 220 deletions(-) diff --git a/messages/no.json b/messages/no.json index befe23e8..48df23de 100644 --- a/messages/no.json +++ b/messages/no.json @@ -1,222 +1,325 @@ { - "$schema": "https://inlang.com/schema/inlang-message-format", - "navbar": { - "upload": "Last opp", - "convert": "Konverter", - "settings": "Innstillinger", - "about": "Om", - "toggle_theme": "Bytt tema" - }, - "footer": { - "copyright": "© {year} VERT.", - "source_code": "Kildekode", - "discord_server": "Discord-server", - "privacy_policy": "Personvern" - }, - "upload": { - "title": "Filkonvertereren du vil elske.", - "subtitle": "All bilde-, lyd- og dokumentbehandling skjer på enheten din. Videoer konverteres på våre lynraske servere. Ingen filstørrelsesgrense, ingen annonser, og helt åpen kildekode.", - "uploader": { - "text": "Slipp eller klikk for å {action}", - "convert": "konvertere" - }, - "cards": { - "title": "VERT støtter...", - "images": "Bilder", - "audio": "Lyd", - "documents": "Dokumenter", - "video": "Video", - "video_server_processing": "Serverstøttet", - "local_supported": "Lokal støtte", - "status": { - "text": "Status: {status}", - "ready": "klar", - "not_ready": "ikke klar", - "not_initialized": "ikke initialisert", - "downloading": "laster ned...", - "initializing": "initialiserer...", - "unknown": "ukjent status" - }, - "supported_formats": "Støttede formater:" - }, - "tooltip": { - "partial_support": "Dette formatet kan bare konverteres som {direction}.", - "direction_input": "input (fra)", - "direction_output": "output (til)", - "video_server_processing": "Videoer lastes opp til en server for behandling som standard, lær hvordan du setter det opp lokalt her." - } - }, - "convert": { - "panel": { - "convert_all": "Konverter alle", - "download_all": "Last ned alle som .zip", - "remove_all": "Fjern alle filer", - "set_all_to": "Sett alle til", - "na": "I/T" - }, - "dropdown": { - "audio": "Lyd", - "video": "Video", - "doc": "Dokument", - "image": "Bilde", - "placeholder": "Søk format" - }, - "tooltips": { - "unknown_file": "Ukjent filtype", - "audio_file": "Lydfil", - "video_file": "Videofil", - "document_file": "Dokumentfil", - "image_file": "Bildefil", - "convert_file": "Konverter denne filen", - "download_file": "Last ned denne filen" - }, - "errors": { - "cant_convert": "Vi kan ikke konvertere denne filen.", - "vertd_server": "hva gjør du..? du skal kjøre vertd-serveren!", - "unsupported_format": "Kun bilde-, video-, lyd- og dokumentfiler støttes", - "vertd_not_found": "Kunne ikke finne vertd-instansen for å starte videokonvertering. Er du sikker på at instans-URL-en er satt riktig?", - "worker_downloading": "{type}-konverteren initialiseres for øyeblikket, vennligst vent noen øyeblikk.", - "worker_error": "{type}-konverteren fikk en feil under initialisering, prøv igjen senere.", - "worker_timeout": "{type}-konverteren tar lengre tid enn forventet å initialisere, vennligst vent litt til eller oppdater siden.", - "audio": "lyd", - "doc": "dokument", - "image": "bilde" - } - }, - "settings": { - "title": "Innstillinger", - "errors": { - "save_failed": "Kunne ikke lagre innstillinger!" - }, - "appearance": { - "title": "Utseende", - "brightness_theme": "Lysstyrketema", - "brightness_description": "Vil du ha et solfylt smell, eller en rolig ensom natt?", - "light": "Lyst", - "dark": "Mørkt", - "effect_settings": "Effektinnstillinger", - "effect_description": "Vil du ha fancy effekter, eller en mer statisk opplevelse?", - "enable": "Aktiver", - "disable": "Deaktiver" - }, - "conversion": { - "title": "Konvertering", - "advanced_settings": "Avanserte innstillinger", - "filename_format": "Filnavnformat", - "filename_description": "Dette bestemmer navnet på filen ved nedlasting, uten filendelsen. Du kan bruke følgende maler i formatet, som vil bli erstattet med relevant informasjon: %name% for det opprinnelige filnavnet, %extension% for den opprinnelige filendelsen, og %date% for en dato når filen ble konvertert.", - "placeholder": "VERT_%name%", - "default_format": "Standard konverteringsformat", - "default_format_enable": "Aktiver", - "default_format_disable": "Deaktiver", - "default_format_description": "Dette endrer standardformatet som velges når du laster opp en fil av denne filtypen.", - "default_format_image": "Bilder", - "default_format_video": "Videoer", - "default_format_audio": "Lyd", - "default_format_document": "Dokumenter", - "metadata": "Filmetadata", - "metadata_description": "Dette endrer om metadata (EXIF, sanginfo, osv.) på den opprinnelige filen beholdes i konverterte filer.", - "keep": "Behold", - "remove": "Fjern", - "quality": "Konverteringskvalitet", - "quality_description": "Dette endrer standard utgangskvalitet for konverterte filer (i sin kategori). Høyere verdier kan føre til lengre konverteringstid og filstørrelse.", - "quality_video": "Dette endrer standard utgangskvalitet for konverterte videofiler. Høyere verdier kan føre til lengre konverteringstid og filstørrelse.", - "quality_audio": "Lyd (kbps)", - "quality_images": "Bilde (%)", - "rate": "Samplingsfrekvens (Hz)" - }, - "vertd": { - "title": "Videokonvertering", - "status": "status:", - "loading": "laster...", - "available": "tilgjengelig, commit id {commitId}", - "unavailable": "utilgjengelig (er url-en riktig?)", - "description": "vertd-prosjektet er en server-wrapper for FFmpeg. Dette lar deg konvertere videoer gjennom VERTs webgrensesnitt, samtidig som du kan bruke kraften til GPU-en din for å gjøre det så raskt som mulig.", - "hosting_info": "Vi hoster en offentlig instans for din bekvemmelighet, men det er ganske enkelt å hoste din egen på PC-en eller serveren din hvis du vet hva du gjør. Du kan laste ned server-binærfiler [vertd_link]her[/vertd_link] - prosessen med å sette dette opp vil bli enklere i fremtiden, så følg med!", - "instance": "Instans", - "url_placeholder": "Eksempel: http://localhost:24153", - "conversion_speed": "Konverteringshastighet", - "speed_description": "Dette beskriver avveiningen mellom hastighet og kvalitet. Raskere hastigheter gir lavere kvalitet, men får jobben gjort raskere.", - "speeds": { - "very_slow": "Veldig treg", - "slower": "Tregere", - "slow": "Treg", - "medium": "Middels", - "fast": "Rask", - "ultra_fast": "Ekstremt rask" - } - }, - "privacy": { - "title": "Personvern og data", - "plausible_title": "Plausible-analyse", - "plausible_description": "Vi bruker [plausible_link]Plausible[/plausible_link], et personvernfokusert analyseverktøy, for å samle helt anonyme statistikker. All data er anonymisert og aggregert, og ingen identifiserbar informasjon blir noen gang sendt eller lagret. Du kan se analysen [analytics_link]her[/analytics_link] og velge å melde deg ut nedenfor.", - "opt_in": "Meld inn", - "opt_out": "Meld ut" - }, - "language": { - "title": "Språk", - "description": "Velg ønsket språk for VERT-grensesnittet." - } - }, - "about": { - "title": "Om", - "why": { - "title": "Hvorfor VERT?", - "description": "Filkonverterere har alltid skuffet oss. De er stygge, fulle av annonser, og viktigst av alt; trege. Vi bestemte oss for å løse dette en gang for alle ved å lage et alternativ som løser alle disse problemene, og mer.

Alle ikke-video-filer konverteres helt på enheten; dette betyr at det ikke er noen forsinkelse mellom sending og mottak av filer fra en server, og vi får aldri tilgang til filene du konverterer.

Videofiler lastes opp til vår lynraske RTX 4000 Ada-server. Videoene dine blir liggende der i en time hvis du ikke konverterer dem. Hvis du konverterer filen, blir videoen liggende på serveren i en time, eller til den lastes ned. Filen blir deretter slettet fra serveren vår." - }, - "sponsors": { - "title": "Sponsorer", - "description": "Vil du støtte oss? Kontakt en utvikler i [discord_link]Discord[/discord_link]-serveren, eller send en e-post til", - "email_copied": "E-post kopiert til utklippstavlen!" - }, - "resources": { - "title": "Ressurser", - "discord": "Discord", - "source": "Kilde", - "email": "E-post" - }, - "donate": { - "title": "Doner til VERT", - "description": "Med din støtte kan vi fortsette å vedlikeholde og forbedre VERT.", - "one_time": "Engangs", - "monthly": "Månedlig", - "custom": "Egendefinert", - "pay_now": "Betal nå", - "donate_amount": "Doner ${amount} USD", - "thank_you": "Takk for donasjonen!", - "payment_failed": "Betaling mislyktes: {message}{period} Du er ikke belastet.", - "donation_error": "En feil oppstod under behandlingen av donasjonen. Prøv igjen senere.", - "payment_error": "Feil ved henting av betalingsdetaljer. Prøv igjen senere." - }, - "credits": { - "title": "Kreditering", - "contact_team": "Hvis du vil kontakte utviklingsteamet, bruk e-posten som finnes på \"Ressurser\"-kortet.", - "notable_contributors": "Merkbare bidragsytere", - "notable_description": "Vi vil takke disse personene for deres store bidrag til VERT.", - "github_contributors": "GitHub-bidragsytere", - "github_description": "Stor takk til alle disse for å hjelpe til! [github_link]Vil du også hjelpe?[/github_link]", - "no_contributors": "Ser ut som ingen har bidratt ennå... [contribute_link]bli den første til å bidra![/contribute_link]", - "libraries": "Biblioteker", - "libraries_description": "En stor takk til FFmpeg (lyd, video), ImageMagick (bilder) og Pandoc (dokumenter) for å vedlikeholde slike utmerkede biblioteker i så mange år. VERT er avhengig av dem for å gi deg konverteringene dine.", - "roles": { - "lead_developer": "Hovedutvikler; konverteringsbackend, UI-implementering", - "developer": "Utvikler; UI-implementering", - "designer": "Designer; UX, merkevarebygging, markedsføring", - "docker_ci": "Vedlikehold av Docker & CI-støtte", - "former_cofounder": "Tidligere medgründer & designer" - } - }, - "errors": { - "github_contributors": "Feil ved henting av GitHub-bidragsytere" - } - }, - "workers": { - "errors": { - "general": "Feil ved konvertering av {file}: {message}", - "cancel": "Feil ved avbrytelse av konvertering for {file}: {message}", - "magick": "Feil i Magick-arbeider, bildekovertering fungerer kanskje ikke som forventet.", - "ffmpeg": "Feil ved lasting av FFmpeg, noen funksjoner fungerer kanskje ikke som forventet.", - "no_audio": "Ingen lydstrøm funnet.", - "invalid_rate": "Ugyldig samplingsfrekvens angitt: {rate}Hz" - } - } + "$schema": "https://inlang.com/schema/inlang-message-format", + "navbar": { + "upload": "Last opp", + "convert": "Konverter", + "settings": "Innstillinger", + "about": "Om", + "toggle_theme": "Bytt tema" + }, + "footer": { + "copyright": "© {year} VERT.", + "source_code": "Kildekode", + "discord_server": "Discord-server", + "privacy_policy": "Personvern" + }, + "upload": { + "title": "Filkonvertereren du vil elske.", + "subtitle": "All bilde-, lyd- og dokumentbehandling skjer på enheten din. Videoer konverteres på våre lynraske servere. Ingen filstørrelsesgrense, ingen annonser, og helt åpen kildekode.", + "uploader": { + "text": "Slipp eller klikk for å {action}", + "convert": "konvertere" + }, + "cards": { + "title": "VERT støtter...", + "images": "Bilder", + "audio": "Lyd", + "documents": "Dokumenter", + "video": "Video", + "video_server_processing": "Serverstøttet", + "local_supported": "Lokal støtte", + "status": { + "text": "Status: {status}", + "ready": "klar", + "not_ready": "ikke klar", + "not_initialized": "ikke initialisert", + "downloading": "laster ned...", + "initializing": "initialiserer...", + "unknown": "ukjent status" + }, + "supported_formats": "Støttede formater:" + }, + "tooltip": { + "partial_support": "Dette formatet kan bare konverteres som {direction}.", + "direction_input": "input (fra)", + "direction_output": "output (til)", + "video_server_processing": "Videoer lastes opp til en server for behandling som standard, lær hvordan du setter det opp lokalt her." + } + }, + "convert": { + "archive_file": { + "extract": "Pakk ut arkiv", + "extracting": "Arkiv oppdaget: {filename}", + "extracted": "Pakket ut {extract_count} filer fra {filename}. {ignore_count} elementer ble ignorert.", + "detected": "Oppdaget {type}-filer i {filename}.", + "audio": "lyd", + "video": "video", + "doc": "dokument", + "image": "bilde", + "extract_error": "Feil ved utpakking av {filename}: {error}" + }, + "large_file_warning": "På grunn av begrensninger i nettleser/enhet er konvertering fra video til lyd deaktivert for denne filen fordi den er større enn {limit}GB. Vi anbefaler å bruke Firefox eller Safari for filer av denne størrelsen, da de har færre begrensninger.", + "external_warning": { + "title": "Advarsel om ekstern server", + "text": "Hvis du velger å konvertere til et videoformat, vil disse filene bli lastet opp til en ekstern server for konvertering. Vil du fortsette?", + "yes": "Ja", + "no": "Nei" + }, + "panel": { + "convert_all": "Konverter alle", + "download_all": "Last ned alle som .zip", + "remove_all": "Fjern alle filer", + "set_all_to": "Sett alle til", + "na": "I/T" + }, + "dropdown": { + "audio": "Lyd", + "video": "Video", + "doc": "Dokument", + "image": "Bilde", + "placeholder": "Søk format", + "no_formats": "Ingen formater tilgjengelig", + "no_results": "Ingen formater samsvarer med søket ditt" + }, + "tooltips": { + "unknown_file": "Ukjent filtype", + "audio_file": "Lydfil", + "video_file": "Videofil", + "document_file": "Dokumentfil", + "image_file": "Bildefil", + "convert_file": "Konverter denne filen", + "download_file": "Last ned denne filen" + }, + "errors": { + "cant_convert": "Vi kan ikke konvertere denne filen.", + "vertd_server": "hva gjør du..? du skal kjøre vertd-serveren!", + "vertd_generic_view": "Vis feildetaljer", + "vertd_generic_body": "En feil oppstod under konvertering av videoen din. Vil du sende denne videoen til utviklerne for å hjelpe med å fikse feilen? Kun videofilen din vil bli sendt. Ingen identifiserende informasjon lastes opp.", + "vertd_generic_title": "Feil ved videokonvertering", + "vertd_generic_yes": "Send video", + "vertd_generic_no": "Ikke send", + "vertd_failed_to_keep": "Kunne ikke beholde videoen på serveren: {error}", + "vertd_details": "Vis feildetaljer", + "vertd_details_body": "Hvis du trykker send, vil videoen din også bli vedlagt sammen med feilloggen som alltid rapporteres til oss for gjennomgang. Følgende informasjon er loggen vi automatisk mottar:", + "vertd_details_footer": "Denne informasjonen brukes kun til feilsøking og vil aldri bli delt. Se vår [privacy_link]personvernerklæring[/privacy_link] for mer informasjon.", + "vertd_details_job_id": "Jobb-ID: {jobId}", + "vertd_details_from": "Fra format: {from}", + "vertd_details_to": "Til format: {to}", + "vertd_details_error_message": "Feilmelding: [view_link]Vis feillogger[/view_link]", + "vertd_details_close": "Lukk", + "vertd_ratelimit": "Videoen din, '{filename}', har feilet å konvertere flere ganger. For å forhindre overbelastning av serveren er videre konverteringsforsøk for denne filen midlertidig blokkert. Prøv igjen senere.", + "unsupported_format": "Kun bilde-, video-, lyd- og dokumentfiler støttes", + "format_output_only": "Dette formatet kan for øyeblikket kun brukes som utdata (konverteres til), ikke som input.", + "vertd_not_found": "Kunne ikke finne vertd-instansen for å starte videokonvertering. Er du sikker på at instans-URL-en er satt riktig?", + "worker_downloading": "{type}-konverteren initialiseres for øyeblikket, vennligst vent noen øyeblikk.", + "worker_error": "{type}-konverteren fikk en feil under initialisering, prøv igjen senere.", + "worker_timeout": "{type}-konverteren tar lengre tid enn forventet å initialisere, vennligst vent litt til eller oppdater siden.", + "audio": "lyd", + "doc": "dokument", + "image": "bilde" + } + }, + "settings": { + "title": "Innstillinger", + "errors": { + "save_failed": "Kunne ikke lagre innstillinger!" + }, + "appearance": { + "title": "Utseende", + "brightness_theme": "Lysstyrketema", + "brightness_description": "Vil du ha et solfylt smell, eller en rolig ensom natt?", + "light": "Lyst", + "dark": "Mørkt", + "effect_settings": "Effektinnstillinger", + "effect_description": "Vil du ha fancy effekter, eller en mer statisk opplevelse?", + "enable": "Aktiver", + "disable": "Deaktiver" + }, + "conversion": { + "title": "Konvertering", + "advanced_settings": "Avanserte innstillinger", + "filename_format": "Filnavnformat", + "filename_description": "Dette bestemmer navnet på filen ved nedlasting, uten filendelsen. Du kan bruke følgende maler i formatet, som vil bli erstattet med relevant informasjon: %name% for det opprinnelige filnavnet, %extension% for den opprinnelige filendelsen, og %date% for en dato når filen ble konvertert.", + "placeholder": "VERT_%name%", + "default_format": "Standard konverteringsformat", + "default_format_enable": "Aktiver", + "default_format_disable": "Deaktiver", + "default_format_description": "Dette endrer standardformatet som velges når du laster opp en fil av denne filtypen.", + "default_format_image": "Bilder", + "default_format_video": "Videoer", + "default_format_audio": "Lyd", + "default_format_document": "Dokumenter", + "metadata": "Filmetadata", + "metadata_description": "Dette endrer om metadata (EXIF, sanginfo, osv.) på den opprinnelige filen beholdes i konverterte filer.", + "keep": "Behold", + "remove": "Fjern", + "quality": "Konverteringskvalitet", + "quality_description": "Dette endrer standard utgangskvalitet for konverterte filer (i sin kategori). Høyere verdier kan føre til lengre konverteringstid og filstørrelse.", + "quality_video": "Dette endrer standard utgangskvalitet for konverterte videofiler. Høyere verdier kan føre til lengre konverteringstid og filstørrelse.", + "quality_audio": "Lyd (kbps)", + "quality_images": "Bilde (%)", + "rate": "Samplingsfrekvens (Hz)" + }, + "vertd": { + "title": "Videokonvertering", + "status": "status:", + "loading": "laster...", + "available": "tilgjengelig, commit id {commitId}", + "unavailable": "utilgjengelig (er url-en riktig?)", + "description": "vertd-prosjektet er en server-wrapper for FFmpeg. Dette lar deg konvertere videoer gjennom VERTs webgrensesnitt, samtidig som du kan bruke kraften til GPU-en din for å gjøre det så raskt som mulig.", + "hosting_info": "Vi hoster en offentlig instans for din bekvemmelighet, men det er ganske enkelt å hoste din egen på PC-en eller serveren din hvis du vet hva du gjør. Du kan laste ned server-binærfiler [vertd_link]her[/vertd_link] - prosessen med å sette dette opp vil bli enklere i fremtiden, så følg med!", + "instance": "Instans", + "url_placeholder": "Eksempel: http://localhost:24153", + "conversion_speed": "Konverteringshastighet", + "speed_description": "Dette beskriver avveiningen mellom hastighet og kvalitet. Raskere hastigheter gir lavere kvalitet, men får jobben gjort raskere.", + "speeds": { + "very_slow": "Veldig treg", + "slower": "Tregere", + "slow": "Treg", + "medium": "Middels", + "fast": "Rask", + "ultra_fast": "Ekstremt rask" + }, + "auto_instance": "Auto (anbefalt)", + "eu_instance": "Falkenstein, Tyskland", + "us_instance": "Washington, USA", + "custom_instance": "Egendefinert" + }, + "privacy": { + "title": "Personvern og data", + "plausible_title": "Plausible-analyse", + "plausible_description": "Vi bruker [plausible_link]Plausible[/plausible_link], et personvernfokusert analyseverktøy, for å samle helt anonyme statistikker. All data er anonymisert og aggregert, og ingen identifiserbar informasjon blir noen gang sendt eller lagret. Du kan se analysen [analytics_link]her[/analytics_link] og velge å melde deg ut nedenfor.", + "opt_in": "Meld inn", + "opt_out": "Meld ut", + "cache_title": "Cache-håndtering", + "cache_description": "Vi cacher konverteringsfilene i nettleseren din slik at du ikke trenger å laste dem ned på nytt hver gang, noe som forbedrer ytelsen og reduserer databruk.", + "refresh_cache": "Oppdater cache", + "clear_cache": "Tøm cache", + "files_cached": "{size} ({count} filer)", + "loading_cache": "Laster...", + "total_size": "Total størrelse", + "files_cached_label": "Filer i cache", + "cache_cleared": "Cache tømt!", + "cache_clear_error": "Kunne ikke tømme cache.", + "site_data_title": "Nettsteddata-håndtering", + "site_data_description": "Tøm alle nettsteddata inkludert innstillinger og cachede filer, tilbakestill VERT til standard og last inn siden på nytt.", + "clear_all_data": "Tøm alle nettsteddata", + "clear_all_data_confirm_title": "Tøm alle nettsteddata?", + "clear_all_data_confirm": "Dette vil tilbakestille alle innstillinger og cache, og deretter laste inn siden på nytt. Denne handlingen kan ikke angres.", + "clear_all_data_cancel": "Avbryt", + "all_data_cleared": "Alle nettsteddata er tømt! Laster inn siden på nytt...", + "all_data_clear_error": "Kunne ikke tømme alle nettsteddata." + }, + "language": { + "title": "Språk", + "description": "Velg ønsket språk for VERT-grensesnittet." + } + }, + "about": { + "title": "Om", + "why": { + "title": "Hvorfor VERT?", + "description": "Filkonverterere har alltid skuffet oss. De er stygge, fulle av annonser, og viktigst av alt; trege. Vi bestemte oss for å løse dette en gang for alle ved å lage et alternativ som løser alle disse problemene, og mer.

Alle ikke-video-filer konverteres helt på enheten; dette betyr at det ikke er noen forsinkelse mellom sending og mottak av filer fra en server, og vi får aldri tilgang til filene du konverterer.

Videofiler lastes opp til vår lynraske RTX 4000 Ada-server. Videoene dine blir liggende der i en time hvis du ikke konverterer dem. Hvis du konverterer filen, blir videoen liggende på serveren i en time, eller til den lastes ned. Filen blir deretter slettet fra serveren vår." + }, + "sponsors": { + "title": "Sponsorer", + "description": "Vil du støtte oss? Kontakt en utvikler i [discord_link]Discord[/discord_link]-serveren, eller send en e-post til", + "email_copied": "E-post kopiert til utklippstavlen!" + }, + "resources": { + "title": "Ressurser", + "discord": "Discord", + "source": "Kilde", + "email": "E-post" + }, + "donate": { + "title": "Doner til VERT", + "description": "Med din støtte kan vi fortsette å vedlikeholde og forbedre VERT.", + "one_time": "Engangs", + "monthly": "Månedlig", + "custom": "Egendefinert", + "pay_now": "Betal nå", + "donate_amount": "Doner ${amount} USD", + "thank_you": "Takk for donasjonen!", + "payment_failed": "Betaling mislyktes: {message}{period} Du er ikke belastet.", + "donation_error": "En feil oppstod under behandlingen av donasjonen. Prøv igjen senere.", + "payment_error": "Feil ved henting av betalingsdetaljer. Prøv igjen senere.", + "donation_notice_official": "Donasjoner her går til den offisielle VERT-instansen (vert.sh) og hjelper til med å støtte utviklingen av prosjektet.", + "donation_notice_unofficial": "Donasjoner her går til operatøren av denne VERT-instansen. Hvis du ønsker å støtte de offisielle VERT-utviklerne, besøk [official_link]vert.sh[/official_link] i stedet." + }, + "credits": { + "title": "Kreditering", + "contact_team": "Hvis du vil kontakte utviklingsteamet, bruk e-posten som finnes på \"Ressurser\"-kortet.", + "notable_contributors": "Merkbare bidragsytere", + "notable_description": "Vi vil takke disse personene for deres store bidrag til VERT.", + "github_contributors": "GitHub-bidragsytere", + "github_description": "Stor takk til alle disse for å hjelpe til! [github_link]Vil du også hjelpe?[/github_link]", + "no_contributors": "Ser ut som ingen har bidratt ennå... [contribute_link]bli den første til å bidra![/contribute_link]", + "libraries": "Biblioteker", + "libraries_description": "En stor takk til FFmpeg (lyd, video), ImageMagick (bilder) og Pandoc (dokumenter) for å vedlikeholde slike utmerkede biblioteker i så mange år. VERT er avhengig av dem for å gi deg konverteringene dine.", + "roles": { + "lead_developer": "Hovedutvikler; konverteringsbackend, UI-implementering", + "developer": "Utvikler; UI-implementering", + "designer": "Designer; UX, merkevarebygging, markedsføring", + "docker_ci": "Vedlikehold av Docker & CI-støtte", + "former_cofounder": "Tidligere medgründer & designer" + } + }, + "errors": { + "github_contributors": "Feil ved henting av GitHub-bidragsytere" + } + }, + "workers": { + "errors": { + "general": "Feil ved konvertering av {file}: {message}", + "cancel": "Feil ved avbrytelse av konvertering for {file}: {message}", + "magick": "Feil i Magick-arbeider, bildekovertering fungerer kanskje ikke som forventet.", + "ffmpeg": "Feil ved lasting av FFmpeg, noen funksjoner fungerer kanskje ikke som forventet.", + "pandoc": "Feil ved lasting av Pandoc-arbeider, dokumentkonvertering fungerer kanskje ikke som forventet.", + "no_audio": "Ingen lydstrøm funnet.", + "invalid_rate": "Ugyldig samplingsfrekvens angitt: {rate}Hz", + "file_too_large": "Denne filen overskrider {limit}GB-grensen for nettleser/enhet. Prøv Firefox eller Safari for å konvertere store filer, da de vanligvis har høyere grenser." + } + }, + "privacy": { + "title": "Personvern", + "summary": { + "title": "Sammendrag", + "description": "VERTs personvernerklæring er veldig enkel: vi samler eller lagrer ingen data om deg i det hele tatt. Vi bruker ikke informasjonskapsler eller sporere, og all konvertering (bortsett fra video) skjer lokalt i nettleseren din. Videoer slettes etter nedlasting eller etter én time, med mindre du eksplisitt gir tillatelse til å lagre dem; de vil kun bli brukt til feilsøking. VERT hoster selv en Coolify-instans for å hoste nettstedet og vertd (for videokonvertering), og en Plausible-instans for helt anonym og aggregert analyse.

Merk at dette kun gjelder den offisielle VERT-instansen på [vert_link]vert.sh[/vert_link]; tredjepartsinstanser kan håndtere dataene dine annerledes." + }, + "conversions": { + "title": "Konverteringer", + "description": "De fleste konverteringer (bilder, dokumenter, lyd) skjer helt lokalt på enheten din ved hjelp av WebAssembly-versjoner av relevante verktøy (f.eks. ImageMagick, Pandoc, FFmpeg). Dette betyr at filene dine aldri forlater enheten din, og vi får aldri tilgang til dem.

Videokonverteringer utføres på våre servere fordi de krever mer prosessorkraft og ikke kan gjøres raskt nok i nettleseren ennå. Videoer du konverterer med VERT slettes etter nedlasting eller etter én time, med mindre du eksplisitt gir tillatelse til å lagre dem lenger kun for feilsøking." + }, + "donations": { + "title": "Donasjoner", + "description": "Vi bruker Stripe på [about_link]om[/about_link]-siden for å samle inn donasjoner. Stripe kan samle inn visse opplysninger om betalingen og enheten for å forhindre svindel, som beskrevet i [stripe_link]deres dokumentasjon om avansert svindeldeteksjon[/stripe_link]. Eksterne nettverksforespørsler til Stripe utsettes, og gjøres kun etter at du klikker på betal-knappen." + }, + "conversion_errors": { + "title": "Konverteringsfeil", + "description": "Når en videokonvertering feiler, kan vi samle inn noe anonym data for å hjelpe oss med å diagnostisere problemet. Denne dataen kan inkludere:", + "list_job_id": "Jobb-ID, som er det anonymiserte filnavnet", + "list_format_from": "Formatet du konverterte fra", + "list_format_to": "Formatet du konverterte til", + "list_stderr": "FFmpeg stderr-utdata fra jobben din (feilmelding)", + "list_video": "Selve videofilen (hvis eksplisitt tillatelse er gitt)", + "footer": "Denne informasjonen brukes kun til å diagnostisere konverteringsproblemer. Selve videofilen vil kun bli samlet inn hvis du gir oss tillatelse, og kun brukes til feilsøking." + }, + "analytics": { + "title": "Analyse", + "description": "Vi hoster en Plausible-instans for helt anonym og aggregert analyse. Plausible bruker ikke informasjonskapsler og overholder alle viktige personvernregler (GDPR/CCPA/PECR). Du kan melde deg ut av analyse i \"Personvern og data\"-seksjonen i [settings_link]innstillinger[/settings_link] og lese mer om Plausibles personvernpraksis [plausible_link]her[/plausible_link]." + }, + "local_storage": { + "title": "Lokal lagring", + "description": "Vi bruker nettleserens lokale lagring for å lagre innstillingene dine, og nettleserens sesjonslagring for midlertidig å lagre GitHub-bidragsyterlisten for \"Om\"-seksjonen for å redusere gjentatte GitHub API-forespørsler. Ingen personlige data lagres eller overføres.

WebAssembly-versjonene av konverteringsverktøyene vi bruker (FFmpeg, ImageMagick, Pandoc) lagres også lokalt i nettleseren din når du først besøker nettstedet, slik at du ikke trenger å laste dem ned på nytt hver gang. Ingen personlige data lagres eller overføres. Du kan se eller slette denne dataen når som helst i \"Personvern og data\"-seksjonen i [settings_link]innstillinger[/settings_link]." + }, + "contact": { + "title": "Kontakt", + "description": "For spørsmål, send oss en e-post på: [email_link]hello@vert.sh[/email_link]. Hvis du bruker en tredjepartsinstans av VERT, kontakt verten for den instansen i stedet." + }, + "last_updated": "Sist oppdatert: 2025-10-29" + }, + "toast": { + "insecure_context": "Du besøker VERT i en usikker kontekst (f.eks. tilgang via HTTP i stedet for HTTPS). Noen funksjoner kan ikke fungere som forventet." + } } \ No newline at end of file From 5ef8b4f34e2d7f66c3610ab7a49ac396aaba6d5c Mon Sep 17 00:00:00 2001 From: Maya Date: Mon, 9 Feb 2026 18:57:46 +0300 Subject: [PATCH 004/118] fix: og tags fix? --- src/routes/+layout.svelte | 5 ++--- {src/lib/assets => static}/VERT_Feature.webp | Bin static/banner.png | Bin 7963 -> 0 bytes 3 files changed, 2 insertions(+), 3 deletions(-) rename {src/lib/assets => static}/VERT_Feature.webp (100%) delete mode 100644 static/banner.png diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 5b6e6699..dc3e78ae 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -6,7 +6,6 @@ import { DISABLE_ALL_EXTERNAL_REQUESTS, VERT_NAME } from "$lib/util/consts.js"; import * as Layout from "$lib/components/layout"; import * as Navbar from "$lib/components/layout/Navbar"; - import featuredImage from "$lib/assets/VERT_Feature.webp"; import { Settings } from "$lib/sections/settings/index.svelte"; import { files, @@ -144,7 +143,7 @@ property="og:description" content="With VERT, you can quickly convert any image, video, audio, and document file. No ads, no tracking, open source, and all processing (other than video) is done on your device." /> - + @@ -156,7 +155,7 @@ property="twitter:description" content="With VERT, you can quickly convert any image, video, audio, and document file. No ads, no tracking, open source, and all processing (other than video) is done on your device." /> - + {#if enablePlausible} diff --git a/src/lib/assets/VERT_Feature.webp b/static/VERT_Feature.webp similarity index 100% rename from src/lib/assets/VERT_Feature.webp rename to static/VERT_Feature.webp diff --git a/static/banner.png b/static/banner.png deleted file mode 100644 index bb114667f13204d89c6ce18a972ecda5d3ddbc35..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7963 zcmZWucRZGF)IZO%_sB?kGO{uo$a=~aGE4T3o(Nfqx{Xi@KM_K9Rw|KX z38_Sd_m%MMesLe>nO9udqx;h#r05s@N3>ys&eb{a_ zA44CRM*3!&=u@AlyHc`v+bYgm&2uFA%|ia10a0(cXu(3#k9wmM-L?pL>h(9W>4wu)ixyRPT!kwS8#2MYO1~Ryeg=oFSa|`F0s%fKhq`c zvUXsgreBU*<|Chy?hS)`+rWKI3{8PFJLC$$FRrU`%q(zZVeF+xqYMk4w32UlWFsI& zd!$9dy~*~jNoAYFcwjD?s^1wioD9wFJc*Xl#0jzUKaAV;GJP=$!nNc(l2 zbw?Gwy0r^1*s5qH$>1j!AJw?yj={CVCvfy_-a29Q5r}%$6tC3$Z#ApIVm0wBD(3O# z-H6(J>4<<0ewP~1-C31?uN|&Lgh?9(q(1=O0K zhPELqcn-Tgw~Fwoo1%tE4h(EX?#`8oYrZC(yUg1uhNT?d?cs0mocAw~O+oqJg>q7u zaIRdVtL)Ugg12o4JljyAaJSb^;Zt_{F<7(z{Mq86`rp8g z9(&|s5EQ50KJIUCvirQcVlEyNKT!aiKZck-@S;I2oWdBjYy`0$^8Zg)dPH~ju3!l_ z$w{#Qk1;|RDHqB;9_>p;?d05`hQ+*ta-W7znLas^L4GKB=OPVUp!#em%Jmn<FSlE)*?8~iuSwU0zzTP@cHzJ5_1P~^JW!GUG#HTWO#bR%V7cnB?<^~* zc3&+Ekgi5ch9lCC%JI$-`$Bd-8l!@jbS&wI+JDzuS((=rP97__EgMptpeH)C|5CAx zo1jt%!;tbE!H8r1)q2->0nKNBNmtM0DIyg`*Wey?pDaY1F(9T>MUD0M&N|B1Y8h=v zh`6?M@wI@0SU?4Eg8}8TqXq-qa8hMe`soo7U!6b1Xu7>Dnan2!RPf&o3`vWbX4+8s zygW@|Kw9(|meZ02x`%Mf+AD9O{+SoD%NXK(s}gbB?)sRJ_O)GjQtp0GhEL-$V@&4UP!OjRnt!EjRC5mN0U}(0m35X`L zAbQ6@z`tBtB_cuXhK(YLlZu326xZb30p-2MuTaCgFY(eo|C3_H6-40mr&%ueK#OYE z5T}CXmB(z%ntMa@=tW#VGm1YY!d-bxpA_w1n{G7+ZOdirxR~u&+YCM(^g8t`9K&+$ zcVDB!4%{3bXTya`l?Yj`8V#i|3Pd|?JdFzfr}&#mbVi!M&E9M-R#}+xGCzH7hXUo- z+FzF=iNDCQX+?HfPM{%osph2}^V+Sy%XrEo++|)BVaI2+$7m$VYN{f`Ff5&8d{Hug zDe`x>xPCL)F}W7Me!9|emj9wCH8#1+HA^9hel##m|-Un zvLV(nsaCUv7i8JXpU^Ynh+w!IGFgRE?6MbYIQ;4un$OpT6%#Oh6mv?wkkf(_uoPKx!_sC^wXtoKVONLZxp|lC z7SEqBo6}>n%GjaZWWixaWkos+{MbKXu+qU2YGWM{To?AN>{QEjS(h|C^$@ z2tqZVO)uAlHt<2Gr{%jj*S^db&gw<2x5_TOXqz84Xv(YYntyI^tb3qq3N0Y({2`m0 z#_C&Vx<;|BDLDHllvG)23g64F^50F0Un`+u_wJexPndJ498lT487a?uXf|i_Qw)6g z8llkrdv%RYdf+c)k{bs6&+x)`4youbd4&Rq$6g;(r8&tn&Voug<0cUPyK$?WO$#nb{V_( zkxH(Sp|SH>%`pGb&H}C7)~XwV95%(c} zKny-K-8gu*VxIb_c%;_u-^!AAph{iaTOW`Is+m09PyeaXc+a#|Ha-$~dam%n&DkI0 zp%&Pza-IDEEAB1_o^kJ!As~5%7~`luiM}{4L_wP?(PYD(oo(ma!u)m+WoTHeXYl$;UpHr(tQ~=+j?)6tJ~9o zZU&a*=#|dnQHvK_dV=+0RHMa{%-E5sE`>(g1{2W1%LQ{2AS>x6HwS&L`ALF8>(hSQ zNnH%fkc%c2g~nkU1|3{)$o)DZDSErH%c7j^9vZg7JFpyll8O}jm;(}y5)h4S&zt`B znEX*ir!SKdp%6{RQf%1{*J@epV3>L=fA@07%PPLzohpsVSmNV#b>7QR(X>YE+f`3b zK$>zs-mDx6^9u255m-zyAi|Bh7*PnK&%m;*lTiE3KRVn76WTXD$Q5a{!NM$1cv>n4 z=5{xJVNJo3ObQ@gLBA)x-}d5~QqRr7^Y10E0cm40hZb?l<-+I^Ed|!NP>@c)U%P#l zti2_5v$FTS_?4n1q1EV1S$SEf>|cri%O>1I~Q zYWsn_&N9qXYb>XzPT%On}i|)6710WqJP)&_c0#w>F{`cAeICSw&RFr!#y$tAFts^ zO7CV=P}~0neY|-jDl@5!O5Q$?9Z2KVrqq-e?@_w!=Di@WlnwDI%15-6qLN=(c=qE2 z^I^>t3K$U|`}BFf?7w^?6ANSHi+4%fk?$|r3LY?P3M7(pUkVxOQ~30^wl0thGJ3wV zL`&;g%lG~;Z}qOHsB}I#v063C-OsB%)T~jlA|4rOib057YDZvkxgd)}c!@Wv1&D8D zp^ZP+UCMHt)1PI`_sW|rD-4pe5-X3Bx%)9Ek#DnJ2HN}>;7QvTiwgaj>5zjb6T9vs zH=Hnt@BAJK(0c16s5i~ed~D z`+J|el?DZ#F0RiN`q}3#36lk;OWYWiAHTE)f)o^}AmgD@Z5)Q=ugl&qH$$_uUd>k8 zN4;Ex1JU9?(>Nlxkk)s)g$NAk`)(X^fc?hE6yH+x2|+zqDq(6I7^jxtD6Ohf9(E%` zlgr%j;V2cHadFik|Cq}i9@eUelufi|01b!D(1UQr_Y9A_OKhVP#DC@n=cFp>bLOF1 zS7I0=?`6+81F^U^jt;3BQZ2nzkuWihD)S9it!@OjP!T4-b5iHzrQJBp?l+5tTY6GK zSj-1?Y?i{3IoX|@`u?BcVPTu=r&RZ5xBKt29Hg-n;RaQIBZdS|9Fb}9nce=GhTE$z zS`?J2;CU?SFOPZ7srzm$YC$=Py}@o7_ONh2uvh|l|L2_Dv$&JyRn`w~EjW3{G5zBt zX3J57T&Edc!BJ{J_mrI(Q5q1ZI*jXlJ6%_*EO`0ytv<|O#a_YICdYI~#LxG#%H_;{@(|RO5n;o4>ojY9?4xyTkXu4>87Sqc3rjeLn z%b-GyCH^wW(mG<$WU{V_82*EAFnU6xWN((1GHYo!kEPc>=t#BuJ^Wg4!S`F3rwpVw zyN6|>q-2-p^Lc*W%#zldwTjqfO@qjE8HuC}oq=(py8J(SMR-5tnCG-~MZa3!<)Od| zbef+&DE#1K+}ehAA&t4V`?o_d=~spO^#0U$bTJw_FQ6f!4w@ zLunN&Jh^Tx{o?WY=-K`1XI1+dD4*>0_psoQ2flCQa}uqm7{8h%>T~CEYGWxi$HUjs zY@GNqvz2y`9pFgTN?f5G_e!116wBR%ibSyTHl3136ErtcAgQSc}2T;Yp{fJKD~3FldsM8?T1lV$G+u^bv*?InwRP;X7Cz zDnd0nf^XNm9(#YsL{+`bt-<<)9AT6j`kAQroV-J(a49v%bVttvGj3zKPL8c#cVbND zrlkXhl5pray%L^El)Z3Lp#wVcB*TGSR%M_k=HQz3qnq1%z( zA1~*C#LB{)(&eAkX-8Nh94sb`ma(!YbWgnGZpc1m;7)Y+*?N2Guu7qE$&^Fcu>OmW zTY5UgKozp|8}y`LDF2I)%KeS@ewsgaEWLK$q z?Ace^B1ac{rp+R;SsRC6Zdt#84vDxC)Yqd_qz!?qY=>UGoms50*0|8<1;X9a~?6WUp zyiOa3QepE2LphcE`anR=)3&csGt?qShevX&PX@M_KovR8>D7jgRLD|i)7N>(D+HToY__X=^2ZbJdM3L2w6>4Iji1r z^%&1moW&uwDuz((z-2>eHK}mruRl-U?_HZvE$tI z@V3YJ+6Bfu&*^t1Kk&D<1=GQ(_EiRZeq}2&wXyhH8pi{WcX4@e*Pu0cE_-GaRG~-K zGp&#n-A!lg^D}Vj#FvIQr{ToRVtCdu`XvoXkyT3Fn zATn>Tq@&FINepCh$I8rNNv9OWN4N}onpE14 zELpB|B%`MfC);0NywDsQw%WJ|Az-m9r{Xvfhmi^4hm3U=&M)+eQD5EqY(t)V*4_FL zm53o#4$~mXmj)#FBtOt-|DlY8+_&)$C#|B&t;`0+XBhl{vd2Zs0JiQsz4N#8cu0M^ zt8{gjj+#Z5QV^=%s`6ZXD6Jw)1_vv!Gr^ z>|9k8$IJ6nNLQAugz#u#k^mD*4ti?-?=ye;z=K-tsz@C1(AVtkUo3!kWj-HaqpKy)_={XQ&vKy@p ze5bXzPZ(P#+Hqy+I(xLmq%S;#KrG4cdOw~D2va3yA87UTsVV1Ur>4Fg*Sol`#Ea%C zs+%LCO;>oe&qNun-1^KlZTc2Vij-AOm)E-tx9m}9t~f;gd&_}5xxPa5rlIdHhxs;h#qd>m`JA^G!>m zK1|E8_(cihCf|Tg!E!c3D9}VZ7#t_S&EujLV*h9;_ z@jI=9<;=;7Js-rlWx?7t023DKRDuv8Q) zwYz|pXJP4;<2W3Geey2meybgi-e(hN+aLDKZK>fM4h^aY`fkhGAr{RT`a^z17;U9_ zG37QFwzGyIkSOK!c}^o-DvL5<_nGiWo>E|EPBy&<^LAq(aKON&_P*GaE>@GRhjA{njlm(N^?4}j(Di*s z_tPf&3Bl&$fM-hAamL)O-_oCURYaRo6OgMoCg4pmin)ndw)0l(F+a%08PIn}2Zju&Mee^AMZPd^sU1Vx zB07~jC`LzChgE7|Nm@%@PSpa5m(V=D@s5O}PzV#{g8pR9W5&m=#4*HHrr6?)r&AqU z%E$nYIN;)O^zAme^f9?r<)xvO&REiWLA^1<89oy2BNJx0fnL@%$f=z1t}M!uLWia? zwuj;w?-2I%?ny_~0D7m!Q+7Va_n^1S(te|4Lr_lFt1340#63Lm=POmnUkOTi;AUWy zKxRzIa6xFbFP7xLC*AyV5;kS(fHj85)z2S|hTQ)CmvDuV(|yTCpzNkUy~Na*iPlM0 z6z~$Ld#)x?djAxr6bGX7*ZL!NqF%-lW!g7Ru$p|A7(~#W9A@! zbo-b>JNI(BZN6Gf@ua#X#><+dZtAA*XCXa_CbuJ6oXZE|UQNu6QKBlaB+Ej#RdIq* z18#?c_hKKPe)s>zZY#74TKUB9UMe*P(>c`C0@`!3{3Sw$9Wl~u^hiJV(PHge7lHhL zYk%DG3;Nls2+P+_U?~rM3T2X7cq#r2X!`z==t+Hu`q}@;GH7IWS@0?J)gTjm=e$>| zBeyq1l=*%0w)q9*zyu43mh=&IUwz7Xy4;`6=5OecFf$#sA8hyXc7dGL?(Ic|&Jng^ z6ps?xUOtWMNlUCHSi_JyZjW9Kr1@hE{`WQ4S5Y4CPxxSn@};3n`R|ZkPcmFsJADYl zGX3eW3CfI7#uL?3ihnL$>Y-X=D`}zG>-1NMQhuQ2H&ipogrrTkHF$ulp*3h7R#RyW9@pPV6 zY^>|eoFE{u67wh$*9=4f0X=jCjE)vP#sE1J3!^ox4Kgg6{U4(ZE4SG8?Q{AQTJ~S< zM>7&KLoU_w!u+1RsRh8$Dr?b$-uGn*W56q-_agee>km5rQWEh1-%ZhveZN=%`ivrJ zz{wY?`b5!Bkq^-SP&hF5SN9_3y8wJmjX{aq4d*41Bnsem|F7NaSmb>w zsXt!gaZgf(1Y8w^#V=*&iz6{$FmxDJYqxBu0BnTl>@DJE^t1#i5eKX%I<#FF0G^pA z$x~w>>$+YJ-1{50Bf2{SW#;QSAT# From 1c5cd9ea17dd2ed24062e271cc2cb3f8448d4a58 Mon Sep 17 00:00:00 2001 From: Maya Date: Mon, 9 Feb 2026 21:22:52 +0300 Subject: [PATCH 005/118] feat: job retry message --- messages/en.json | 1 + src/lib/converters/vertd.svelte.ts | 30 +++++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/messages/en.json b/messages/en.json index ff2d4065..220d5743 100644 --- a/messages/en.json +++ b/messages/en.json @@ -108,6 +108,7 @@ "vertd_details_error_message": "Error message: [view_link]View error logs[/view_link]", "vertd_details_close": "Close", "vertd_ratelimit": "Your video, '{filename}', has failed to convert a few times. To prevent server overload, further conversion attempts for this file have been temporarily blocked. Please try again later.", + "vertd_retry": "Retrying video conversion for {filename} with different settings due to failure. This may take longer than usual.", "unsupported_format": "Only image, video, audio, and document files are supported", "format_output_only": "This format can currently only be used as output (converted to), not as input.", "vertd_not_found": "Could not find the vertd instance to start video conversion. Are you sure the instance URL is set correctly?", diff --git a/src/lib/converters/vertd.svelte.ts b/src/lib/converters/vertd.svelte.ts index 839e513b..4003ac9c 100644 --- a/src/lib/converters/vertd.svelte.ts +++ b/src/lib/converters/vertd.svelte.ts @@ -6,6 +6,7 @@ import { VertdInstance } from "$lib/sections/settings/vertdSettings.svelte"; import { VertFile } from "$lib/types"; import { Converter, FormatInfo } from "./converter.svelte"; import { PUB_DISABLE_FAILURE_BLOCKS } from "$env/static/public"; +import { ToastManager } from "$lib/util/toast.svelte"; interface UploadResponse { id: string; @@ -124,6 +125,13 @@ interface JobCancelledMessage { }; } +interface JobRetriedMessage { + type: "jobRetried"; + data: { + jobId: string; + }; +} + interface FpsProgress { type: "fps"; data: number; @@ -142,6 +150,7 @@ type VertdMessage = | ProgressMessage | CancelJobMessage | JobCancelledMessage + | JobRetriedMessage | CompletedMessage; const progressEstimates = { @@ -370,7 +379,7 @@ export class VertdConverter extends Converter { ws.onopen = () => { const speed = Settings.instance.settings.vertdSpeed; const keepMetadata = Settings.instance.settings.metadata; - this.log("opened ws connection to vertd"); + this.log(`opened ws connection to vertd for file ${input.name}`); const msg: StartJobMessage = { type: "startJob", data: { @@ -382,12 +391,12 @@ export class VertdConverter extends Converter { }, }; ws.send(JSON.stringify(msg)); - this.log("sent startJob message"); + this.log(`sent startJob message for file ${input.name}`); }; ws.onmessage = async (e) => { const msg: VertdMessage = JSON.parse(e.data); - this.log(`received message ${msg.type}`); + this.log(`received message ${msg.type} for file ${input.name}`); switch (msg.type) { case "progressUpdate": { const data = msg.data; @@ -400,8 +409,19 @@ export class VertdConverter extends Converter { break; } + case "jobRetried": { + this.log(`job retrying for file ${input.name}`); + ToastManager.add({ + type: "error", + message: m["convert.errors.vertd_retry"]({ + filename: input.name, + }), + }); + break; + } + case "jobFinished": { - this.log("job finished"); + this.log(`job finished for file ${input.name}`); ws.close(); this.activeConversions.delete(input.id); const url = `${apiUrl}/api/download/${msg.data.jobId}/${uploadRes.auth}`; @@ -467,7 +487,7 @@ export class VertdConverter extends Converter { }, }; ws.send(JSON.stringify(cancelMsg)); - this.log("sent cancelJob message"); + this.log(`sent cancelJob message for file ${input.name}`); } ws.close(); From 9332c95721b9c67b451cc67681b939ddae19872c Mon Sep 17 00:00:00 2001 From: Maya Date: Wed, 11 Feb 2026 09:25:25 +0300 Subject: [PATCH 006/118] feat: apng & video to webp support --- src/lib/components/functional/FormatDropdown.svelte | 6 ++++++ src/lib/converters/vertd.svelte.ts | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/lib/components/functional/FormatDropdown.svelte b/src/lib/components/functional/FormatDropdown.svelte index 1f6845b5..d2c5b3a0 100644 --- a/src/lib/components/functional/FormatDropdown.svelte +++ b/src/lib/components/functional/FormatDropdown.svelte @@ -90,7 +90,13 @@ cat === rootCategory || categories[rootCategory!]?.canConvertTo?.includes(cat), ); + + // handle special cases if (from === ".gif") finalCategories.push("video"); + if (from === ".apng") { + //finalCategories.push("image"); // -- buggy, magick can't convert from or to apng properly + finalCategories = finalCategories.filter((cat) => cat !== "audio"); + } // filter out categories that can't handle large files (due to browser/device limitations) if (file && file.isLarge()) { diff --git a/src/lib/converters/vertd.svelte.ts b/src/lib/converters/vertd.svelte.ts index 4003ac9c..321fe8d0 100644 --- a/src/lib/converters/vertd.svelte.ts +++ b/src/lib/converters/vertd.svelte.ts @@ -264,6 +264,8 @@ export class VertdConverter extends Converter { new FormatInfo("wmv", true, true), new FormatInfo("mov", true, true), new FormatInfo("gif", true, true), + new FormatInfo("apng", true, true), + new FormatInfo("webp", false, true), // for some odd reasons, ffmpeg only supports encoding webp and not decoding -- https://trac.ffmpeg.org/ticket/4907 new FormatInfo("mts", true, true), new FormatInfo("ts", true, true), new FormatInfo("m2ts", true, true), @@ -379,7 +381,9 @@ export class VertdConverter extends Converter { ws.onopen = () => { const speed = Settings.instance.settings.vertdSpeed; const keepMetadata = Settings.instance.settings.metadata; - this.log(`opened ws connection to vertd for file ${input.name}`); + this.log( + `opened ws connection to vertd for file ${input.name}`, + ); const msg: StartJobMessage = { type: "startJob", data: { From 24e1e0c6674267c4a4082a6d2d287d7e0a129090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96FA?= <66040567+ofadev@users.noreply.github.com> Date: Wed, 11 Feb 2026 18:28:06 +0300 Subject: [PATCH 007/118] fix: update Turkish translations translated all missing sections that were introduced over the last few months to ensure full localization coverage. --- messages/tr.json | 109 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 98 insertions(+), 11 deletions(-) diff --git a/messages/tr.json b/messages/tr.json index ed2cac8d..72efe301 100644 --- a/messages/tr.json +++ b/messages/tr.json @@ -10,7 +10,8 @@ "footer": { "copyright": "© {year} VERT.", "source_code": "Kaynak kodu", - "discord_server": "Discord sunucusu" + "discord_server": "Discord sunucusu", + "privacy_policy": "Gizlilik politikası" }, "upload": { "title": "Sevdiğiniz dosya dönüştürücü.", @@ -46,6 +47,18 @@ } }, "convert": { + "archive_file": { + "extract": "Arşivi çıkar", + "extracting": "Arşiv algılandı: {filename}", + "extracted": "{filename} arşivinden {extract_count} dosya çıkarıldı. {ignore_count} öğe yoksayıldı.", + "detected": "{filename} içinde {type} dosyası algılandı.", + "audio": "ses", + "video": "video", + "doc": "belge", + "image": "görsel", + "extract_error": "{filename} çıkarılırken hata oluştu: {error}" + }, + "large_file_warning": "Tarayıcı / cihaz kısıtlamaları nedeniyle, bu dosya için videodan sese dönüştürme devre dışı bırakıldı çünkü {limit}GB'dan daha büyük. Bu boyuttaki dosyalar için daha az kısıtlamaya sahip olan Firefox veya Safari kullanmanızı öneririz.", "external_warning": { "title": "Harici sunucu uyarısı", "text": "Video formatına dönüştürmeyi seçerseniz, bu dosyalar dönüştürülmek üzere harici bir sunucuya yüklenecektir. Devam etmek istiyor musunuz?", @@ -64,7 +77,9 @@ "video": "Video", "doc": "Belge", "image": "Görsel", - "placeholder": "Format ara" + "placeholder": "Format ara", + "no_formats": "Kullanılabilir format yok", + "no_results": "Aramanızla eşleşen format bulunamadı" }, "tooltips": { "unknown_file": "Bilinmeyen dosya türü", @@ -78,7 +93,23 @@ "errors": { "cant_convert": "Bu dosyayı dönüştüremiyoruz.", "vertd_server": "Ne yapıyorsun..? vertd sunucusunu çalıştırman gerekiyordu!", + "vertd_generic_view": "Hata ayrıntılarını görüntüle", + "vertd_generic_body": "Videonuz dönüştürülmeye çalışılırken bir hata oluştu. Bu hatayı düzeltmeye yardımcı olmak için bu videoyu geliştiricilere göndermek ister misiniz? Yalnızca video dosyanız gönderilecektir. Hiçbir kimlik bilgisi yüklenmeyecektir.", + "vertd_generic_title": "Video dönüştürme hatası", + "vertd_generic_yes": "Videoyu gönder", + "vertd_generic_no": "Gönderme", + "vertd_failed_to_keep": "Video sunucuda tutulamadı: {error}", + "vertd_details": "Hata ayrıntılarını görüntüle", + "vertd_details_body": "Gönder'e basarsanız, incelememiz için bize bildirilen hata kaydıyla birlikte videonuz da eklenecektir. Aşağıdaki bilgiler otomatik olarak aldığımız hata kaydıdır:", + "vertd_details_footer": "Bu bilgiler yalnızca sorun giderme amacıyla kullanılacak ve asla paylaşılmayacaktır. Daha fazla ayrıntı için [privacy_link]gizlilik politikamıza[/privacy_link] göz atın.", + "vertd_details_job_id": "İş ID: {jobId}", + "vertd_details_from": "Kaynak format: {from}", + "vertd_details_to": "Hedef format: {to}", + "vertd_details_error_message": "Hata mesajı: [view_link]Hata kayıtlarını görüntüle[/view_link]", + "vertd_details_close": "Kapat", + "vertd_ratelimit": "'{filename}' videonuz birkaç kez dönüştürülemedi. Sunucu aşırı yüklenmesini önlemek için, bu dosya için daha fazla dönüştürme denemesi geçici olarak engellendi. Lütfen daha sonra tekrar deneyin.", "unsupported_format": "Yalnızca görüntü, video, ses ve belge dosyaları desteklenir.", + "format_output_only": "Bu format şu anda yalnızca çıktı (dönüştürülen format) olarak kullanılabilir, girdi olarak kullanılamaz.", "vertd_not_found": "Video dönüştürme işlemini başlatmak için vertd örneği bulunamadı. Sunucu URL’sinin doğru ayarlandığından emin misiniz?", "worker_downloading": "{type} dönüştürme işlemi şu anda başlatılıyor, lütfen birkaç saniye bekleyin.", "worker_error": "{type} dönüştürme işlemi başlatılırken bir hata oluştu, lütfen daha sonra tekrar deneyin.", @@ -111,6 +142,8 @@ "filename_description": "Bu ayar, dosya uzantısını etkilemeden indirilen dosyanın adını belirleyecektir. Aşağıdaki şablonları formata ekleyebilirsiniz, bunlar ilgili bilgilerle değiştirilecektir: orijinal dosya adı için %name%, orijinal dosya uzantısı için %extension% ve dosyanın dönüştürüldüğü tarihin tarih için %date%.", "placeholder": "VERT_%name%", "default_format": "Varsayılan dönüştürme formatı", + "default_format_enable": "Etkinleştir", + "default_format_disable": "Devre dışı bırak", "default_format_description": "Bu ayar, bu dosya türünde bir dosya yüklediğinizde seçili olan varsayılan formatı değiştirecektir.", "default_format_image": "Görsel", "default_format_video": "Video", @@ -166,7 +199,16 @@ "loading_cache": "Yükleniyor...", "total_size": "Toplam Boyut", "files_cached_label": "Önbelleğe Alınan Dosyalar", - "cache_cleared": "Önbellek başarıyla temizlendi." + "cache_cleared": "Önbellek başarıyla temizlendi!", + "cache_clear_error": "Önbellek temizlenemedi.", + "site_data_title": "Site verileri yönetimi", + "site_data_description": "Ayarlar ve önbelleğe alınmış dosyalar dahil olmak üzere tüm site verilerini temizleyerek VERT'i varsayılan durumuna sıfırlayın ve sayfayı yeniden yükleyin.", + "clear_all_data": "Tüm site verilerini temizle", + "clear_all_data_confirm_title": "Tüm site verileri temizlensin mi?", + "clear_all_data_confirm": "Bu işlem tüm ayarları ve önbelleği sıfırlayacak, ardından sayfayı yeniden yükleyecektir. Bu işlem geri alınamaz.", + "clear_all_data_cancel": "İptal", + "all_data_cleared": "Tüm site verileri temizlendi! Sayfa yeniden yükleniyor...", + "all_data_clear_error": "Tüm site verileri temizlenemedi." }, "language": { "title": "Dil", @@ -197,11 +239,13 @@ "monthly": "Aylık", "custom": "Özel", "pay_now": "Ödeme yap", - "donate_amount": "${amount} USD Bağış Yap", + "donate_amount": "{amount} USD Bağış Yap", "thank_you": "Bağışınız için teşekkür ederiz!", "payment_failed": "Ödeme başarısız: {message}{period} Kartınızdan para çekilmedi.", "donation_error": "Bağışınız işlenirken bir hata oluştu. Lütfen daha sonra tekrar deneyin.", - "payment_error": "Ödeme bilgileri alınırken hata oluştu. Lütfen daha sonra tekrar deneyin." + "payment_error": "Ödeme bilgileri alınırken hata oluştu. Lütfen daha sonra tekrar deneyin.", + "donation_notice_official": "Buradaki bağışlarınız resmi VERT örneğine (vert.sh) gider ve projenin gelişimini desteklemeye yardımcı olur.", + "donation_notice_unofficial": "Buradaki bağışlarınız bu VERT örneğinin operatörüne gider. Resmi VERT geliştiricilerini desteklemek istiyorsanız lütfen [official_link]vert.sh[/official_link] adresini ziyaret edin." }, "credits": { "title": "Katkıda bulunanlar", @@ -214,11 +258,11 @@ "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.", "roles": { - "lead_developer": "Lead developer; conversion backend, UI implementation", - "developer": "Developer; UI implementation", - "designer": "Designer; UX, branding, marketing", - "docker_ci": "Maintaining Docker & CI support", - "former_cofounder": "Former co-founder & designer" + "lead_developer": "Baş geliştirici; dönüştürme arka ucu, arayüz uygulaması", + "developer": "Geliştirici; arayüz uygulaması", + "designer": "Tasarımcı; UX, markalaşma, pazarlama", + "docker_ci": "Docker ve CI desteğini sürdürme", + "former_cofounder": "Eski kurucu ortak ve tasarımcı" } }, "errors": { @@ -231,8 +275,51 @@ "cancel": "{file} için dönüştürme işlemi iptal edilirken hata oluştu: {message}", "magick": "Magick işlemi sırasında hata oluştu, görsel dönüştürme işlemi beklendiği gibi çalışmayabilir.", "ffmpeg": "ffmpeg yüklenirken hata oluştu, bazı özellikler çalışmayabilir.", + "pandoc": "Pandoc işlemi sırasında hata oluştu, belge dönüştürme işlemi beklendiği gibi çalışmayabilir.", "no_audio": "Ses akışı bulunamadı.", - "invalid_rate": "Geçersiz örnekleme hızı: {rate}Hz" + "invalid_rate": "Geçersiz örnekleme hızı: {rate}Hz", + "file_too_large": "Bu dosya {limit}GB olan tarayıcı / cihaz sınırını aşıyor. Bu büyük dosyayı dönüştürmek için genellikle daha yüksek sınırlara sahip olan Firefox veya Safari'yi deneyin." } + }, + "privacy": { + "title": "Gizlilik Politikası", + "summary": { + "title": "Özet", + "description": "VERT'in gizlilik politikası oldukça basittir: hakkınızda hiçbir veri toplamıyoruz veya saklamıyoruz. Çerez veya takipçi kullanmıyoruz, analizler tamamen gizlidir ve tüm dönüştürmeler (videolar hariç) yerel olarak tarayıcınızda gerçekleşir. Videolar, sorun giderme amacıyla saklanması için tarafınızca açıkça izin verilmedikçe, indirildikten sonra veya bir saat içinde silinir; yalnızca sorun giderme amacıyla kullanılacaktır. VERT, web sitesini ve vertd'yi (video dönüştürme için) barındırmak için kendi bünyesinde bir Coolify örneği ve tamamen anonim ve toplu analizler için bir Plausible örneği barındırır. Bağışları işlemek için Stripe kullanıyoruz, bu da dolandırıcılığı önlemek için kullanılan bazı verileri toplayabilir.

Not: Bu durum yalnızca [vert_link]vert.sh[/vert_link] adresindeki resmi VERT örneği için geçerli olabilir; üçüncü taraf örnekleri verilerinizi farklı şekilde işleyebilir." + }, + "conversions": { + "title": "Dönüştürmeler", + "description": "Çoğu dönüştürme (görüntüler, belgeler, ses) ilgili araçların (örn. ImageMagick, Pandoc, FFmpeg) WebAssembly sürümleri kullanılarak tamamen yerel olarak cihazınızda gerçekleşir. Bu, dosyalarınızın asla cihazınızdan çıkmadığı ve onlara asla erişemeyeceğimiz anlamına gelir.

Video dönüştürmeleri sunucularımızda gerçekleştirilir çünkü daha fazla işlem gücü gerektirirler ve henüz tarayıcıda çok hızlı yapılamazlar. VERT ile dönüştürdüğünüz videolar, sorun giderme amacıyla saklamamız için açıkça izin vermediğiniz sürece indirildikten veya bir saat sonra silinir." + }, + "donations": { + "title": "Bağışlar", + "description": "Bağış toplamak için [about_link]hakkımızda[/about_link] sayfasında Stripe kullanıyoruz. Stripe, [gelişmiş dolandırıcılık tespiti hakkındaki belgelerinde] (https://stripe.com/docs/disputes/prevention/advanced-fraud-detection) açıklandığı gibi dolandırıcılığı önlemek için ödeme ve cihaz hakkında belirli bilgiler toplayabilir. Stripe'a yönelik harici ağ istekleri ertelenir ve yalnızca ödeme düğmesine tıkladıktan sonra yapılır." + }, + "conversion_errors": { + "title": "Dönüştürme Hataları", + "description": "Bir video dönüştürme işlemi başarısız olduğunda, sorunu teşhis etmemize yardımcı olması için bazı anonim veriler toplayabiliriz. Bu veriler şunları içerebilir:", + "list_job_id": "Anonimleştirilmiş dosya adı olan iş ID'si", + "list_format_from": "Dönüştürdüğünüz kaynak format", + "list_format_to": "Dönüştürdüğünüz hedef format", + "list_stderr": "İşinizin FFmpeg stderr çıktısı (hata mesajı)", + "list_video": "Gerçek video dosyası (açıkça izin verilmişse)", + "footer": "bu bilgiler yalnızca dönüştürme sorunlarını teşhis etmek amacıyla kullanılır. Gerçek video dosyası yalnızca bize izin verirseniz toplanır ve bu dosya yalnızca sorun giderme amacıyla kullanılır." + }, + "analytics": { + "title": "Analitik", + "description": "Tamamen anonim ve toplu analizler için kendi bünyemizde barındırdığımız bir Plausible örneği kullanıyoruz. Plausible çerez kullanmaz ve tüm ana gizlilik düzenlemelerine (GDPR/CCPA/PECR) uygundur. [settings_link]ayarlar[/settings_link] içindeki \"Gizlilik ve veriler\" bölümünden analizlerden çıkabilir ve Plausible'ın gizlilik uygulamaları hakkında daha fazla bilgiyi [plausible_link]buradan[/plausible_link] okuyabilirsiniz." + }, + "local_storage": { + "title": "Yerel Depolama", + "description": "Ayarlarınızı kaydetmek için tarayıcınızın yerel depolama alanını (local storage) ve tekrarlanan GitHub API isteklerini azaltmak için \"Hakkımızda\" bölümündeki GitHub katkıda bulunanlar listesini geçici olarak depolamak için tarayıcınızın oturum depolama alanını (session storage) kullanıyoruz. Hiçbir kişisel veri saklanmaz veya iletilmez.

Kullandığımız dönüştürme araçlarının (FFmpeg, ImageMagick, Pandoc) WebAssembly sürümleri de web sitesini ilk ziyaret ettiğinizde tarayıcınızda yerel olarak saklanır, böylece her ziyarette bunları yeniden indirmeniz gerekmez. Hiçbir kişisel veri saklanmaz veya iletilmez. Bu verileri istediğiniz zaman [settings_link]ayarlar[/settings_link] içindeki \"Gizlilik ve veriler\" bölümünden görüntüleyebilir veya silebilirsiniz." + }, + "contact": { + "title": "İletişim", + "description": "Sorularınız için bize e-posta gönderin: [email_link]hello@vert.sh[/email_link]. Üçüncü taraf bir VERT örneği kullanıyorsanız, lütfen bunun yerine o örneğin barındırıcısıyla iletişime geçin." + }, + "last_updated": "Son güncelleme: 29 Ekim 2025" + }, + "toast": { + "insecure_context": "VERT'i güvenli olmayan bir bağlantıda ziyaret ediyorsunuz (örneğin HTTPS yerine HTTP üzerinden erişim). Bazı özellikler beklendiği gibi çalışmayabilir." } } From 167991081a4ab4c745f9d3eb12ef75075a90d330 Mon Sep 17 00:00:00 2001 From: Maya Date: Wed, 11 Feb 2026 18:41:28 +0300 Subject: [PATCH 008/118] fix: fix file name conflicts pressing "download all" with multiple files of same name gets overwritten (e.g. when pasting screenshots/from clipboard) --- src/lib/converters/vertd.svelte.ts | 4 +- src/lib/store/index.svelte.ts | 67 ++++++++++++++++++++++++------ 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/lib/converters/vertd.svelte.ts b/src/lib/converters/vertd.svelte.ts index 321fe8d0..c74e8e99 100644 --- a/src/lib/converters/vertd.svelte.ts +++ b/src/lib/converters/vertd.svelte.ts @@ -265,7 +265,9 @@ export class VertdConverter extends Converter { new FormatInfo("mov", true, true), new FormatInfo("gif", true, true), new FormatInfo("apng", true, true), - new FormatInfo("webp", false, true), // for some odd reasons, ffmpeg only supports encoding webp and not decoding -- https://trac.ffmpeg.org/ticket/4907 + // for some odd reasons, ffmpeg only supports encoding webp and not decoding -- https://trac.ffmpeg.org/ticket/4907 + // honestly, we could probably convert webp to apng, then apng to (x) video format + new FormatInfo("webp", false, true), new FormatInfo("mts", true, true), new FormatInfo("ts", true, true), new FormatInfo("m2ts", true, true), diff --git a/src/lib/store/index.svelte.ts b/src/lib/store/index.svelte.ts index 318c6bda..b873a57d 100644 --- a/src/lib/store/index.svelte.ts +++ b/src/lib/store/index.svelte.ts @@ -126,9 +126,11 @@ class Files { // check if completely transparent const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - const isTransparent = Array.from(imageData.data).every((value, index) => { - return (index + 1) % 4 !== 0 || value === 0; - }); + const isTransparent = Array.from(imageData.data).every( + (value, index) => { + return (index + 1) % 4 !== 0 || value === 0; + }, + ); if (isTransparent) { canvas.remove(); return undefined; @@ -271,7 +273,9 @@ class Files { } const converter = converters .sort(byNative(format)) - .find((converter) => converter.formatStrings().includes(format)); + .find((converter) => + converter.formatStrings().includes(format), + ); if (!converter) { log(["files"], `no converter found for ${file.name}`); this.files.push(new VertFile(file, format)); @@ -312,7 +316,10 @@ class Files { action: () => { this.files = [ ...this.files.filter( - (f) => !f.converters.map((c) => c.name).includes("vertd"), + (f) => + !f.converters + .map((c) => c.name) + .includes("vertd"), ), ]; this._warningShown = false; @@ -321,7 +328,10 @@ class Files { { text: m["convert.external_warning.yes"](), action: () => { - localStorage.setItem("acceptedExternalWarning", "true"); + localStorage.setItem( + "acceptedExternalWarning", + "true", + ); this._warningShown = false; }, }, @@ -337,7 +347,14 @@ class Files { public add(file: VertFile[] | null | undefined): void; public add(file: FileList | null | undefined): void; public add( - file: VertFile | File | VertFile[] | File[] | FileList | null | undefined, + file: + | VertFile + | File + | VertFile[] + | File[] + | FileList + | null + | undefined, ) { if (!file) return; if (Array.isArray(file) || file instanceof FileList) { @@ -357,11 +374,13 @@ class Files { } public async downloadAll() { - if (files.files.length === 0) return; + if (this.files.length === 0) return; // eslint-disable-next-line @typescript-eslint/no-explicit-any const dlFiles: any[] = []; - for (let i = 0; i < files.files.length; i++) { - const file = files.files[i]; + const fileNames: string[] = []; + + for (let i = 0; i < this.files.length; i++) { + const file = this.files[i]; const result = file.result; if (!result) { @@ -372,12 +391,33 @@ class Files { let to = result.to; if (!to.startsWith(".")) to = `.${to}`; + fileNames.push(file.file.name.replace(/\.[^/.]+$/, "") + to); + } + + for (let i = 0; i < this.files.length; i++) { + const file = this.files[i]; + const result = file.result; + + if (!result) continue; + + let fileName = fileNames[i]; + + // check if this filename appears more than once + const isDuplicate = fileNames.filter((name) => name === fileName).length > 1; + if (isDuplicate) { + const nameParts = fileName.lastIndexOf("."); + const nameWithoutExt = fileName.substring(0, nameParts); + const ext = fileName.substring(nameParts); + fileName = `${nameWithoutExt} (${i + 1})${ext}`; + } + dlFiles.push({ - name: file.file.name.replace(/\.[^/.]+$/, "") + to, + name: fileName, lastModified: Date.now(), input: await result.file.arrayBuffer(), }); } + const { downloadZip } = await import("client-zip"); const blob = await downloadZip(dlFiles, "converted.zip").blob(); const url = URL.createObjectURL(blob); @@ -551,7 +591,10 @@ export const getMaxArrayBufferSize = (): number => { const cached = localStorage.getItem("maxArrayBufferSize"); if (cached) { const parsed = Number(cached); - log(["converters"], `using cached max ArrayBuffer size: ${parsed} bytes`); + log( + ["converters"], + `using cached max ArrayBuffer size: ${parsed} bytes`, + ); if (!isNaN(parsed) && parsed > 0) return parsed; } From ca8a03470b998a112f7ca97f6389f445101a35a3 Mon Sep 17 00:00:00 2001 From: Maya Date: Wed, 11 Feb 2026 19:07:05 +0300 Subject: [PATCH 009/118] feat: webp to video support adds support for animated webp to video by using middleware: webp -> gif -> video (vertd) --- .../functional/FormatDropdown.svelte | 2 +- src/lib/converters/vertd.svelte.ts | 28 +++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/lib/components/functional/FormatDropdown.svelte b/src/lib/components/functional/FormatDropdown.svelte index d2c5b3a0..d91d7371 100644 --- a/src/lib/components/functional/FormatDropdown.svelte +++ b/src/lib/components/functional/FormatDropdown.svelte @@ -92,7 +92,7 @@ ); // handle special cases - if (from === ".gif") finalCategories.push("video"); + if (from === ".gif" || from === ".webp") finalCategories.push("video"); if (from === ".apng") { //finalCategories.push("image"); // -- buggy, magick can't convert from or to apng properly finalCategories = finalCategories.filter((cat) => cat !== "audio"); diff --git a/src/lib/converters/vertd.svelte.ts b/src/lib/converters/vertd.svelte.ts index c74e8e99..02f9476f 100644 --- a/src/lib/converters/vertd.svelte.ts +++ b/src/lib/converters/vertd.svelte.ts @@ -7,6 +7,7 @@ import { VertFile } from "$lib/types"; import { Converter, FormatInfo } from "./converter.svelte"; import { PUB_DISABLE_FAILURE_BLOCKS } from "$env/static/public"; import { ToastManager } from "$lib/util/toast.svelte"; +import { converters } from "./index"; interface UploadResponse { id: string; @@ -265,9 +266,7 @@ export class VertdConverter extends Converter { new FormatInfo("mov", true, true), new FormatInfo("gif", true, true), new FormatInfo("apng", true, true), - // for some odd reasons, ffmpeg only supports encoding webp and not decoding -- https://trac.ffmpeg.org/ticket/4907 - // honestly, we could probably convert webp to apng, then apng to (x) video format - new FormatInfo("webp", false, true), + new FormatInfo("webp", true, true), new FormatInfo("mts", true, true), new FormatInfo("ts", true, true), new FormatInfo("m2ts", true, true), @@ -351,9 +350,28 @@ export class VertdConverter extends Converter { public async convert(input: VertFile, to: string): Promise { if (to.startsWith(".")) to = to.slice(1); + let fileUpload = input; + + // if converting animated webp to video, first convert to gif + // ffmpeg (in vertd) doesn't support decoding animated webp still.. while supporting encoding animated webp for some reason + // https://trac.ffmpeg.org/ticket/4907 + if (input.from === ".webp") { + this.log(`animated webp detected, converting to gif first`); + const magickConverter = converters.find((c) => c.name === "imagemagick"); + if (magickConverter) { + try { + fileUpload = await magickConverter.convert(input, ".gif", 100); + this.log(`successfully converted webp to gif`); + } catch (e) { + this.log(`failed to convert webp to gif: ${e}`); + throw e; + } + } + } + let hash: string; if (PUB_DISABLE_FAILURE_BLOCKS === "false") { - hash = await input.hash(); + hash = await fileUpload.hash(); if (this.blocked(hash)) { this.log(`conversion blocked for file ${input.name}`); @@ -365,7 +383,7 @@ export class VertdConverter extends Converter { } } - const uploadRes = await uploadFile(input); + const uploadRes = await uploadFile(fileUpload); const apiUrl = await VertdInstance.instance.url(); return new Promise((resolve, reject) => { From a4aaae6ed5d7a816d3f7816043378157091e96fa Mon Sep 17 00:00:00 2001 From: Kamiliusz Date: Wed, 11 Feb 2026 18:08:20 +0100 Subject: [PATCH 010/118] Add files via upload --- messages/pl.json | 325 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 messages/pl.json diff --git a/messages/pl.json b/messages/pl.json new file mode 100644 index 00000000..2a458618 --- /dev/null +++ b/messages/pl.json @@ -0,0 +1,325 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "navbar": { + "upload": "Prześlij", + "convert": "Konwertuj", + "settings": "Ustawienia", + "about": "O nas", + "toggle_theme": "Przełącz motyw" + }, + "footer": { + "copyright": "© {year} VERT.", + "source_code": "Kod źródłowy", + "discord_server": "Serwer Discord", + "privacy_policy": "Polityka prywatności" + }, + "upload": { + "title": "Konwerter plików, który pokochasz.", + "subtitle": "Wszystkie zdjęcia, pliki audio oraz dokumenty przetwarzane są na Twoim urządzeniu. Pliki wideo konwertowane są na naszych bardzo szybkich serwerach. Bez limitu rozmiaru plików, bez reklam, z otwartym kodem źródłowym.", + "uploader": { + "text": "Przeciągnij lub kliknij aby {action}", + "convert": "konwertować" + }, + "cards": { + "title": "VERT wspiera...", + "images": "Zdjęcia", + "audio": "Pliki audio", + "documents": "Dokumenty", + "video": "Wideo", + "video_server_processing": "Wspierane serwerowo", + "local_supported": "Wspierane lokalnie", + "status": { + "text": "Status: {status}", + "ready": "gotowy", + "not_ready": "niegotowy", + "not_initialized": "niezainicjowany", + "downloading": "pobieranie...", + "initializing": "inicjowanie...", + "unknown": "nieznany status" + }, + "supported_formats": "Wspierane formaty:" + }, + "tooltip": { + "partial_support": "Ten format może zostać przekonwertowany jedynie jako {direction}.", + "direction_input": "wejście (z)", + "direction_output": "wyjście (do)", + "video_server_processing": "Domyślnie, w celu przetworzenia, pliki wideo przesyłane są na serwer, dowiedz się jak ustawić lokalne przetwarzanie tutaj." + } + }, + "convert": { + "archive_file": { + "extract": "Wypakuj archiwum", + "extracting": "Wykryto archiwum: {filename}", + "extracted": "Wypakowanych plików z {filename}: {extract_count}. Zignorowanych plików: {ignore_count}.", + "detected": "Wykryto pliki {type} w {filename}.", + "audio": "audio", + "video": "wideo", + "doc": "dokumentowe", + "image": "zdjęciowe", + "extract_error": "Wystąpił błąd przy wypakowywaniu {filename}: {error}" + }, + "large_file_warning": "Ze względu na przeglądarkę / ograniczenia sprzętowe, konwersja wideo do audio jest niedostępna dla tego pliku, ponieważ przekracza on limit {limit}GB. Zalecamy używanie przeglądarki Firefox bądź Safari dla plików o takich rozmiarach, gdyż przeglądarki te mają mniej ograniczeń.", + "external_warning": { + "title": "Ostrzeżenie zewnętrznego serwera", + "text": "Jeśli wybierzesz konwersję na format wideo, pliki te zostaną przesłane na zewnętrzny serwer w celu konwersji. Czy chcesz kontynuować?", + "yes": "Tak", + "no": "Nie" + }, + "panel": { + "convert_all": "Konwertuj wszystko", + "download_all": "Pobierz wszystko jako .zip", + "remove_all": "Usuń wszystkie pliki", + "set_all_to": "Ustaw wszystkie jako", + "na": "Nie dotyczy" + }, + "dropdown": { + "audio": "Audio", + "video": "Wideo", + "doc": "Dokument", + "image": "Zdjęcie", + "placeholder": "Wyszukaj format", + "no_formats": "Formaty niedostępne", + "no_results": "Nie znaleziono wyszukiwanego formatu" + }, + "tooltips": { + "unknown_file": "Nieznany typ pliku", + "audio_file": "Plik audio", + "video_file": "Plik wideo", + "document_file": "Plik dokumentowy", + "image_file": "Plik obrazu", + "convert_file": "Konwertuj ten plik", + "download_file": "Pobierz ten plik" + }, + "errors": { + "cant_convert": "Nie możemy przekonwertować tego pliku.", + "vertd_server": "co robisz..? powinieneś uruchomić serwer vertd!", + "vertd_generic_view": "Wyświetl szczegóły błędu", + "vertd_generic_body": "Wystąpił błąd podczas próby konwersji Twojego wideo. Czy chcesz przesłać to wideo deweloperom w celu pomocy naprawy tego błędu? Przesłane zostanie jedynie wideo. Żadne identyfikatory nie zostaną przesłane.", + "vertd_generic_title": "Błąd konwersji wideo", + "vertd_generic_yes": "Prześlij wideo", + "vertd_generic_no": "Nie przesyłaj", + "vertd_failed_to_keep": "Nie udało się przechować wideo na serwerze: {error}", + "vertd_details": "Wyświetl szczegóły błędu", + "vertd_details_body": "Jeśli wciśniejsz przycisk prześlij, Twoje wideo również zostanie dołączone wraz z wpisem błędu, który zawsze jest do nas wysyłany do sprawdzenia. Następująca informacja jest wpisem, który automatycznie otrzymujemy:", + "vertd_details_footer": "Informacja ta użyta zostanie jedynie w celu rozwiązywania błędów oraz nigdy nie zostanie udostępniona. Zobacz naszą [privacy_link]politykę prywatności[/privacy_link] po więcej szczegółów.", + "vertd_details_job_id": "Job ID: {jobId}", + "vertd_details_from": "Z formatu: {from}", + "vertd_details_to": "Do formatu: {to}", + "vertd_details_error_message": "Wiadomość błędu: [view_link]Wyświetl wpis błędu[/view_link]", + "vertd_details_close": "Zamknij", + "vertd_ratelimit": "Nie udało się przekonwertować Twojego wideo '{filename}' kilkukrotnie. Aby zapobiec przeciążeniom serwera, dalsze próby konwersji tego pliku zostały chwilowo zablokowane. Spróbuj ponownie później.", + "unsupported_format": "Wspierane są jedynie pliki obrazów, wideo, audio oraz dokumentów", + "format_output_only": "Obecnie, ten format może być używany jedynie jako wyjście (konwersja do), nie jako wejście.", + "vertd_not_found": "Nie udało się znaleźć instancji vertd w celu uruchomienia konwersji wideo. Czy jesteś pewny, że instancja URL jest ustawiona poprawnie?", + "worker_downloading": "Konwerter {type} jest obecnie inicjowany, proszę czekać.", + "worker_error": "Konwerter {type} napotkał błąd podczas inicjacji, spróbuj ponownie później.", + "worker_timeout": "Konwerter {type} inicjowany jest dłużej niż oczekiwano, proszę odczekać kilka chwil lub odświeżyć stronę.", + "audio": "audio", + "doc": "dokumentów", + "image": "zdjęciowy" + } + }, + "settings": { + "title": "Ustawienia", + "errors": { + "save_failed": "Nie udało się zapisać ustawień!" + }, + "appearance": { + "title": "Wygląd", + "brightness_theme": "Motyw jasności", + "brightness_description": "Wolisz motyw jasny jak słońce, czy ciemny jak cicha, spokojna noc?", + "light": "Jasny", + "dark": "Ciemny", + "effect_settings": "Ustawienia efektów", + "effect_description": "Chciałbyś włączyć szalone efekty, czy preferujesz bardziej statyczne doświadczenie?", + "enable": "Włącz", + "disable": "Wyłącz" + }, + "conversion": { + "title": "Konwersja", + "advanced_settings": "Zaawansowane ustawienia", + "filename_format": "Format nazwy pliku", + "filename_description": "To ustawienie pozwoli ci określić nazwę pliku przy jego pobieraniu z wyłączeniem jego rozszerzenia. Możesz w formacie umieścić następujące wzorce, które następnie zostaną zamienioną na odpowiednią informację: %name% zostanie zamieniony na oryginalną nazwę pliku, %extension% zostanie zamieniony na oryginalne rozszerzenie pliku oraz %date% zamieniony zostanie na datę kiedy plik został przekonwertowany.", + "placeholder": "VERT_%name%", + "default_format": "Domyślny format konwersji", + "default_format_enable": "Włączony", + "default_format_disable": "Wyłączony", + "default_format_description": "To ustawienie zmieni domyślnie wybrany format przy przesyłaniu pliku danego typu.", + "default_format_image": "Zdjęcia", + "default_format_video": "Wideo", + "default_format_audio": "Audio", + "default_format_document": "Dokumenty", + "metadata": "Metadane pliku", + "metadata_description": "To ustawienie pozwola ci ustalić czy metadane (EXIF, informacje o piosence, itp.) oryginalnego pliku zostaną zachowane w przekonwertowanym pliku.", + "keep": "Zachowaj", + "remove": "Usuń", + "quality": "Jakość konwersji", + "quality_description": "To ustawienie pozwala ustalić domyślną jakość wyjściową przekonwertowanego pliku (w danej kategorii). Wyższe wartości mogą skutkować dłuższym czasem konwersji oraz większym rozmiarem pliku.", + "quality_video": "To ustawienie pozwala ustalić domyślną jakość wyjściową przekonwertowanego pliku wideo. Wyższe wartości mogą skutkować dłuższym czasem konwersji oraz większym rozmiarem pliku.", + "quality_audio": "Audio (kbps)", + "quality_images": "Zdjęcia (%)", + "rate": "Częstotliwość próbkowania (Hz)" + }, + "vertd": { + "title": "Konwersja wideo", + "status": "status:", + "loading": "ładowanie...", + "available": "dostępna, commit id {commitId}", + "unavailable": "niedostępna (czy url jest poprawny?)", + "description": "Projekt vertd jest serwerową nakładką na FFmpeg. To pozwala ci konwertować wideo za pomocą interfejsu sieciowego VERT, nadal będąc w stanie wykorzystywać moc swojego procesora graficznego (GPU) w celu jak najszybszej konwersji.", + "hosting_info": "Dla Twojej wygody hostujemy publiczną instancję, jednak ustawienie hosta na Twoim własnym komputerze lub serwerze jest stosunkowo proste jeśli wiesz co robisz. Możesz pobrać binaria serwera [vertd_link]tutaj[/vertd_link] - w przyszłości proces tej konfiguracji będzie łatwiejszy, więc czekajcie na więcej informacji!", + "instance": "Instancja", + "url_placeholder": "Przykładowo: http://localhost:24153", + "conversion_speed": "Prędkość konwersji", + "speed_description": "To opisuje kompromis pomiędzy prędkością a jakością. Większa prędkość może skutkować niższą jakością, ale wykona swoje zadanie szybciej", + "speeds": { + "very_slow": "Najwolniejsza", + "slower": "Wolniejsza", + "slow": "Wolna", + "medium": "Średnia", + "fast": "Szybka", + "ultra_fast": "Ultra szybka" + }, + "auto_instance": "Auto (zalecana)", + "eu_instance": "Falkenstein, Niemcy", + "us_instance": "Washington, USA", + "custom_instance": "Własna" + }, + "privacy": { + "title": "Prywatność i dane", + "plausible_title": "Analityka Plausible", + "plausible_description": "Używamy [plausible_link]Plausible[/plausible_link], skupionego na prywatności narzędzia analitycznego, w celu zebrania w pełni anonimowych statystyk. Wszystkie dane są anonimizowane i agregowane, a żadne wrażliwe informacje nigdy nie są przesyłane ani przechowywane. Możesz zobaczyć analitykę [analytics_link]tutaj[/analytics_link] oraz podjąć decyzję o zbieraniu danych poniżej.", + "opt_in": "Wyrażam zgodę", + "opt_out": "Rezygnuję", + "cache_title": "Zarządzanie pamięcią podręczną", + "cache_description": "W pamięci podręcznej przeglądarki przechowujemy pliki konwertera, abyś nie musiał ich ponownie pobierać za każdym razem, usprawniając wydajność i redukując wykorzystanie danych.", + "refresh_cache": "Odśwież pamięć podręczną", + "clear_cache": "Wyczyść pamięć podręczną", + "files_cached": "{size} ({count} plików)", + "loading_cache": "Ładowanie...", + "total_size": "Łączny rozmiar", + "files_cached_label": "Przechowywanych plików", + "cache_cleared": "Pomyślnie wyczyszczono pamięć podręczną!", + "cache_clear_error": "Nie udało się wyczyścić pamięci podręcznej.", + "site_data_title": "Zarządzanie danymi strony", + "site_data_description": "Wyczyść wszystkie dane strony wliczając ustawienia i przechowywane pliki, przywracając VERT do swojego domyślnego stanu oraz odświeżając stronę.", + "clear_all_data": "Wyczyść wszystkie dane strony", + "clear_all_data_confirm_title": "Wyczyścić wszystkie dane strony?", + "clear_all_data_confirm": "To zresetuje wszystkie ustawienia i przechowywane pliki, a następnie odświeży stronę. To działanie jest nieodwracalne.", + "clear_all_data_cancel": "Anuluj", + "all_data_cleared": "Wszystkie dane strony wyczyszczone! Odświeżanie strony...", + "all_data_clear_error": "Nie udało się wyczyścić wszystkich danych strony." + }, + "language": { + "title": "Język", + "description": "Wybierz swój preferowany język dla interfejsu VERT." + } + }, + "about": { + "title": "O nas", + "why": { + "title": "Dlaczego VERT?", + "description": "Konwertery plików od zawsze nas zawodziły. Są brzydkie, przepełnione reklamami, ale przede wszystkim, powolne. Postanowiliśmy rozwiązać ten problem raz na zawsze poprzez stworzenie alternatywy, która rozwiązuje wszystkie te problemy i nie tylko.

Wszystkie pliki nie będące wideo są w pełni konwertowane na urządzeniu; to oznacza brak opóźnienia pomiędzy przesyłaniem a odbieraniem plików z serwera, a my nie możemy podejrzeć plików, które konwertujesz.

Pliki wideo przesyłane zostają na nasz bardzo szybki serwer RTX 4000 Ada. Twoje pliki wideo przechowywane są tam przez godzinę jeśli ich nie przekonwertujesz. Jeżeli je przekonwertujesz, wideo zostanie na serwerze przez godzinę lub do momentu jego pobrania. Po tym czasie, plik zostanie usunięty z naszego serwera." + }, + "sponsors": { + "title": "Sponsorzy", + "description": "Chcesz nas wesprzeć? Skontaktuj się z deweloperem na serwerze [discord_link]Discord[/discord_link], lub wyślij email do", + "email_copied": "Email skopiowany do schowka!" + }, + "resources": { + "title": "Zasoby", + "discord": "Discord", + "source": "Źródło", + "email": "Email" + }, + "donate": { + "title": "Wesprzyj VERT", + "description": "Z waszym wsparciem możemy utrzymywać i rozwijać VERT.", + "one_time": "Jednorazowo", + "monthly": "Miesięcznie", + "custom": "Własna kwota", + "pay_now": "Zapłać teraz", + "donate_amount": "Dotacja w kwocie ${amount} USD", + "thank_you": "Dziękujemy za Twoje wsparcie!", + "payment_failed": "Płatność nieudana: {message}{period} Nie zostałeś obciążony.", + "donation_error": "Wystąpił błąd podczas przetwarzania Twojej dotacji. Spróbuj ponownie później.", + "payment_error": "Wystąpił błąd podczas pobierania danych płatności. Spróbuj ponownie później.", + "donation_notice_official": "Twoje dotacje trafiają do oficjalnej instancji VERT (vert.sh) i wspierają rozwój projektu.", + "donation_notice_unofficial": "Twoje dotacje trafiają do operatora tej instancji VERT. Jeśli chcesz wesprzeć oficjalnych deweloperów VERT, odwiedź [official_link]vert.sh[/official_link]." + }, + "credits": { + "title": "Uznanie", + "contact_team": "Jeśli chcesz się skontaktować z zespołem deweloperów, proszę użyć adresu email znajdującego się w zakładce \"Zasoby\".", + "notable_contributors": "Znaczący kontrybutorzy", + "notable_description": "Chcielibyśmy podziękować tym osobom za ich wielki wkład w VERT.", + "github_contributors": "Kontrybutorzy GitHub", + "github_description": "Wielkie podziękowania dla tych wszystkich osób za pomoc! [github_link]Też chcesz pomóc?[/github_link]", + "no_contributors": "Wygląda na to, że jeszcze nikt nie wspomógł... [contribute_link]bądź pierwszym, który nam pomoże![/contribute_link]", + "libraries": "Biblioteki", + "libraries_description": "Wielkie podziękowania dla FFmpeg (audio, wideo), ImageMagick (zdjęcia) oraz Pandoc (dokumenty) za utrzymywanie tak wspaniałych bibliotek przez tak wiele lat. VERT opiera się na nich aby umożliwić ci Twoje konwersje.", + "roles": { + "lead_developer": "Główny deweloper; backend konwersji, implementacja UI", + "developer": "Deweloper; implementacja UI", + "designer": "Designer; UX, branding, marketing", + "docker_ci": "Utrzymywanie Docker oraz wsparcie CI", + "former_cofounder": "Były współzałożyciel oraz designer" + } + }, + "errors": { + "github_contributors": "Błąd przy pobieraniu kontrybutorów GitHub" + } + }, + "workers": { + "errors": { + "general": "Błąd przy konwertowaniu {file}: {message}", + "cancel": "Błąd przy anulowaniu konwersji {file}: {message}", + "magick": "Błąd w Magick worker, konwersja zdjęć może działać niezgodnie z oczekiwaniami.", + "ffmpeg": "Błąd przy wczytywaniu FFmpeg, niektóre usługi mogą działać niezgodnie z oczekiwaniami.", + "pandoc": "Błąd przy wczytywaniu Pandoc worker, konwersja dokumentów może działać niezgodnie z oczekiwaniami.", + "no_audio": "Nie odnaleziono ścieżki audio.", + "invalid_rate": "Wprowadzona została nieprawidłowa częstotliwość próbkowania: {rate}Hz", + "file_too_large": "Ten plik przekracza limit {limit}GB przeglądarki / urządzenia. Spróbuj użyć przeglądarki Firefox bądź Safari do konwersji tego dużego pliku, gdyż przeglądarki te mają mniejsze ograniczenia." + } + }, + "privacy": { + "title": "Polityka Prywatności", + "summary": { + "title": "Podsumowanie", + "description": "Polityka prywatności VERT jest bardzo prosta: nie zbieramy ani nie przechowujemy żadnych Twoich danych. Nie używamy plików cookie ani trackerów, analityka jest w pełni prywatna a wszystkie konwersje (oprócz plików wideo) wykonywane są lokalnie w Twojej przeglądarce. Pliki wideo są usuwane po pobraniu lub po upływie godziny, chyba że wyrazisz zgodę na ich przechowanie; w takim przypadku użyte zostaną jedynie w celu rozwiązywaniu problemów. VERT samodzielnie hostuje instancję Coolify do hostowania strony internetowej oraz vertd (do konwersji wideo) oraz instancję Plausible do anonimowych i agregowanych analiz. Używamy Stripe w celu przetwarzania dotacji, który może zbierać niektóre dane w celu zabezpieczenia przed oszustwami.

Należy pamiętać, że dotyczy to jedynie oficjalnej instancji VERT pod adresem [vert_link]vert.sh[/vert_link]; instancje stron trzecich mogą przetwarzać Twe dane w inny sposób." + }, + "conversions": { + "title": "Konwersje", + "description": "Większość konwersji (zdjęć, dokumentów, audio) wykonywane są w pełni lokalnie na Twoim urządzeniu z użyciem wersji WebAssembly odpowiednich narzędzi (np. ImageMagick, Pandoc, FFmpeg). Oznacza to, że Twoje pliki nigdy nie opuszczają Twojego urządzenia a my nigdy nie będziemy mieli do nich dostępu.

Konwersje wideo wykonywane są na naszych serwerach, ponieważ wymagają one więcej mocy obliczeniowej i obecnie nie mogą być wykonywane odpowiednio szybko za pomocą przeglądarki. Pliki wideo które konwertujesz są usuwane po pobraniu lub po upływie godziny, chyba że wyrazisz zgodę na ich dłuższe przechowanie jedynie w celach rozwiązywania problemów." + }, + "donations": { + "title": "Dotacje", + "description": "Używamy Stripe w zakładce [about_link]O nas[/about_link] w celu zbierania dotacji. Stripe może zbierać konkretne informacje o płatności oraz urządzeniu aby zapobiegać oszustwom tak, jak jest to opisane w [stripe_link]ich dokumentacji o zaawansowanym wykrywaniu oszustw[/stripe_link]. Zewnętrzne żądania sieciowe kierowane do Stripe są opóźniane i są wysyłane dopiero po kliknięciu przycisku płatności." + }, + "conversion_errors": { + "title": "Błędy Konwersji", + "description": "Gdy konwersja wideo się nie powiedzie możemy zbierać pewne anonimowe dane w celu diagnozy problemu. Te dane mogą zawierać:", + "list_job_id": "Job ID, czyli anonimizowana nazwa pliku", + "list_format_from": "Format z którego konwertowano", + "list_format_to": "Format na który konwertowano", + "list_stderr": "Wyjście FFmpeg stderr Twojego zadania (wiadomość błędu)", + "list_video": "Sam plik wideo (jeśli wyrażono na to zgodę)", + "footer": "Informacje te używane są jedynie w celu diagnozy problemu konwersji. Sam plik wideo przechowany będzie tylko i wyłącznie po wyrażeniu Twojej zgody, gdzie będzie wykorzystywany jedynie w celu naprawy problemów." + }, + "analytics": { + "title": "Analityka", + "description": "Samodzielnie hostujemy instancję Plausible do kompletnie anonimowych i agregowanych analiz. Plausible nie używa plików cookie i przestrzega wszystkich regulacji prywatności (GDPR/CCPA/PECR). Możesz zrezygnować z analityki w sekcji \"Prywatność i dane\" w zakładce [settings_link]ustawienia[/settings_link] oraz przeczytać więcej na temat praktyk prywatności Plausible [plausible_link]tutaj[/plausible_link]." + }, + "local_storage": { + "title": "Lokalna Pamięć", + "description": "Używamy lokalnej pamięci Twojej przeglądarki aby zapisać Twoje ustawienia oraz pamięci sesji Twojej przeglądarki aby tymczasowo przechować listę kontrybutorów GitHub w zakładce \"O nas\" aby zredukować powtarzalne zapytania API GitHub. Żadne wrażliwe dane nie są przechowywane ani przesyłane.

Wersje WebAssembly narzędzi, których używamy (FFmpeg, ImageMagick, Pandoc) również są przechowywane lokalnie w Twojej przeglądarce po Twoim pierwszym odwiedzeniu strony abyś nie musiał pobierać ich przy każdym odwiedzeniu strony. Żadne wrażliwe dane nie są przechowywane ani przesyłane. Możesz zobaczyć lub usunąć te dane w dowolnym momencie w sekcji \"Prywatność i dane\" w zakładce [settings_link]ustawienia[/settings_link]." + }, + "contact": { + "title": "Kontakt", + "description": "W razie pytań prosimy o napisanie emaila na adres: [email_link]hello@vert.sh[/email_link]. Jeśli używasz instancji VERT stron trzecich prosimy o kontakt z właścicielem tej konkretnej instancji." + }, + "last_updated": "Ostatnia aktualizacja: 2025-10-29" + }, + "toast": { + "insecure_context": "Odwiedzasz VERT w niezabezpieczonym kontekście (np. uzyskujesz dostęp poprzez HTTP zamiast HTTPS). Niektóre funkcje mogą działać niezgodnie z oczekiwaniami." + } +} \ No newline at end of file From 835efd043f351af45f51f62f119ecdc1da6d04cc Mon Sep 17 00:00:00 2001 From: Kamiliusz Date: Wed, 11 Feb 2026 18:10:04 +0100 Subject: [PATCH 011/118] Update settings.json --- project.inlang/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/project.inlang/settings.json b/project.inlang/settings.json index 9e09cb91..bafd8965 100644 --- a/project.inlang/settings.json +++ b/project.inlang/settings.json @@ -16,7 +16,8 @@ "id", "zh-Hans", "zh-Hant", - "pt-BR" + "pt-BR", + "pl" ], "modules": [ "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js", From 2127692ae42ef61f855fdec7629a0a238c3a5c06 Mon Sep 17 00:00:00 2001 From: Kamiliusz Date: Wed, 11 Feb 2026 18:11:25 +0100 Subject: [PATCH 012/118] Update index.svelte.ts --- src/lib/store/index.svelte.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/store/index.svelte.ts b/src/lib/store/index.svelte.ts index 318c6bda..ccb55f76 100644 --- a/src/lib/store/index.svelte.ts +++ b/src/lib/store/index.svelte.ts @@ -454,6 +454,7 @@ export const availableLocales = { "zh-Hans": "简体中文", "zh-Hant": "繁體中文", "pt-BR": "Português (Brasil)", + pl: "Polski", }; export function updateLocale(newLocale: string) { From 45390516c7959ecf0d8a4430b0583209e6d925eb Mon Sep 17 00:00:00 2001 From: Maya Date: Wed, 11 Feb 2026 21:53:05 +0300 Subject: [PATCH 013/118] fix: add missing env variable to compose --- docker-compose.yml | 3 ++- src/routes/settings/+page.svelte | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 32d9bfd5..85b34243 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,9 +9,10 @@ services: PUB_PLAUSIBLE_URL: ${PUB_PLAUSIBLE_URL:-} PUB_ENV: ${PUB_ENV:-production} PUB_DISABLE_ALL_EXTERNAL_REQUESTS: ${PUB_DISABLE_ALL_EXTERNAL_REQUESTS:-false} - PUB_VERTD_URL: ${PUB_VERTD_URL:-} + PUB_VERTD_URL: ${PUB_VERTD_URL:-https://vertd.vert.sh} PUB_DONATION_URL: ${PUB_DONATION_URL:-https://donations.vert.sh} PUB_STRIPE_KEY: ${PUB_STRIPE_KEY:-pk_live_51RDVmAGSxPVad6bQwzVNnbc28nlmzA30krLWk1fefCMpUPiSRPkavMMbGqa8A3lUaOCMlsUEVy2CWDYg0ip3aPpL00ZJlsMkf2} + PUB_DISABLE_FAILURE_BLOCKS: ${PUB_DISABLE_FAILURE_BLOCKS:-false} restart: unless-stopped ports: - ${PORT:-3000}:80 diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte index 7d586c81..ec4666cc 100644 --- a/src/routes/settings/+page.svelte +++ b/src/routes/settings/+page.svelte @@ -72,6 +72,7 @@
+ {#if PUB_PLAUSIBLE_URL && !DISABLE_ALL_EXTERNAL_REQUESTS} From da39283e0827d1239090ce4cd9922bcf58c9c12a Mon Sep 17 00:00:00 2001 From: Maya Date: Wed, 11 Feb 2026 22:00:25 +0300 Subject: [PATCH 014/118] fix: better privacy section visibility behaviour --- src/lib/sections/settings/Privacy.svelte | 4 +++- src/routes/settings/+page.svelte | 5 ++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/lib/sections/settings/Privacy.svelte b/src/lib/sections/settings/Privacy.svelte index 30f3edaf..9751eb65 100644 --- a/src/lib/sections/settings/Privacy.svelte +++ b/src/lib/sections/settings/Privacy.svelte @@ -17,6 +17,7 @@ import { ToastManager } from "$lib/util/toast.svelte"; import { DISABLE_ALL_EXTERNAL_REQUESTS } from "$lib/util/consts"; import { addDialog } from "$lib/store/DialogProvider"; + import { PUB_PLAUSIBLE_URL } from "$env/static/public"; const { settings = $bindable() }: { settings: ISettings } = $props(); @@ -134,7 +135,7 @@ {m["settings.privacy.title"]()}
- {#if !DISABLE_ALL_EXTERNAL_REQUESTS} + {#if !DISABLE_ALL_EXTERNAL_REQUESTS && PUB_PLAUSIBLE_URL}

@@ -269,6 +270,7 @@ {m["settings.privacy.clear_all_data"]()}

+
diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte index ec4666cc..dea4e037 100644 --- a/src/routes/settings/+page.svelte +++ b/src/routes/settings/+page.svelte @@ -66,15 +66,14 @@ {#if !DISABLE_ALL_EXTERNAL_REQUESTS} - {:else if PUB_PLAUSIBLE_URL} + {:else} {/if}
- - {#if PUB_PLAUSIBLE_URL && !DISABLE_ALL_EXTERNAL_REQUESTS} + {#if !DISABLE_ALL_EXTERNAL_REQUESTS} {/if}
From 06d7cfaad94c4db3e075735fd070b651d05b6706 Mon Sep 17 00:00:00 2001 From: Maya Date: Wed, 11 Feb 2026 22:07:29 +0300 Subject: [PATCH 015/118] fix: don't use naturalWidth/Height fixes #219 --- src/lib/converters/magick.svelte.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/converters/magick.svelte.ts b/src/lib/converters/magick.svelte.ts index 1190d490..fcc3dbb5 100644 --- a/src/lib/converters/magick.svelte.ts +++ b/src/lib/converters/magick.svelte.ts @@ -314,10 +314,10 @@ export class MagickConverter extends Converter { return new Promise((resolve, reject) => { img.onload = () => { try { - canvas.width = img.naturalWidth || width; - canvas.height = img.naturalHeight || height; + canvas.width = width; + canvas.height = height; - ctx.drawImage(img, 0, 0); + ctx.drawImage(img, 0, 0, width, height); canvas.toBlob((blob) => { URL.revokeObjectURL(svgUrl); From 68465431d09478ee810e8aa4bfa691fc6d105888 Mon Sep 17 00:00:00 2001 From: Kamiliusz Date: Wed, 11 Feb 2026 22:52:01 +0100 Subject: [PATCH 016/118] Fix mistakes in pl.json --- messages/pl.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/messages/pl.json b/messages/pl.json index 2a458618..ceb9f86e 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -100,7 +100,7 @@ "vertd_generic_no": "Nie przesyłaj", "vertd_failed_to_keep": "Nie udało się przechować wideo na serwerze: {error}", "vertd_details": "Wyświetl szczegóły błędu", - "vertd_details_body": "Jeśli wciśniejsz przycisk prześlij, Twoje wideo również zostanie dołączone wraz z wpisem błędu, który zawsze jest do nas wysyłany do sprawdzenia. Następująca informacja jest wpisem, który automatycznie otrzymujemy:", + "vertd_details_body": "Jeśli wciśniesz przycisk prześlij, Twoje wideo również zostanie dołączone wraz z wpisem błędu, który zawsze jest do nas wysyłany do sprawdzenia. Następująca informacja jest wpisem, który automatycznie otrzymujemy:", "vertd_details_footer": "Informacja ta użyta zostanie jedynie w celu rozwiązywania błędów oraz nigdy nie zostanie udostępniona. Zobacz naszą [privacy_link]politykę prywatności[/privacy_link] po więcej szczegółów.", "vertd_details_job_id": "Job ID: {jobId}", "vertd_details_from": "Z formatu: {from}", @@ -139,7 +139,7 @@ "title": "Konwersja", "advanced_settings": "Zaawansowane ustawienia", "filename_format": "Format nazwy pliku", - "filename_description": "To ustawienie pozwoli ci określić nazwę pliku przy jego pobieraniu z wyłączeniem jego rozszerzenia. Możesz w formacie umieścić następujące wzorce, które następnie zostaną zamienioną na odpowiednią informację: %name% zostanie zamieniony na oryginalną nazwę pliku, %extension% zostanie zamieniony na oryginalne rozszerzenie pliku oraz %date% zamieniony zostanie na datę kiedy plik został przekonwertowany.", + "filename_description": "To ustawienie pozwoli ci określić nazwę pliku przy jego pobieraniu z wyłączeniem jego rozszerzenia. Możesz w formacie umieścić następujące wzorce, które następnie zostaną zamienione na odpowiednią informację: %name% zostanie zamieniony na oryginalną nazwę pliku, %extension% zostanie zamieniony na oryginalne rozszerzenie pliku oraz %date% zamieniony zostanie na datę kiedy plik został przekonwertowany.", "placeholder": "VERT_%name%", "default_format": "Domyślny format konwersji", "default_format_enable": "Włączony", @@ -322,4 +322,5 @@ "toast": { "insecure_context": "Odwiedzasz VERT w niezabezpieczonym kontekście (np. uzyskujesz dostęp poprzez HTTP zamiast HTTPS). Niektóre funkcje mogą działać niezgodnie z oczekiwaniami." } -} \ No newline at end of file + +} From 23e2601576fadf416606e9038ad7d85aa7e9c8d8 Mon Sep 17 00:00:00 2001 From: Maya Date: Wed, 11 Feb 2026 22:38:25 +0300 Subject: [PATCH 017/118] fix: use cache api for downloads above 2gb oops --- src/lib/types/file.svelte.ts | 49 ++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/src/lib/types/file.svelte.ts b/src/lib/types/file.svelte.ts index 75615420..26ca63e4 100644 --- a/src/lib/types/file.svelte.ts +++ b/src/lib/types/file.svelte.ts @@ -5,6 +5,8 @@ import { ToastManager } from "$lib/util/toast.svelte"; import type { Component } from "svelte"; import { MAX_ARRAY_BUFFER_SIZE } from "$lib/store/index.svelte"; +const MAX_BLOB_SIZE_LIMIT = 2 * 1024 * 1024 * 1024; // 2GB + export class VertFile { public id: string = Math.random().toString(36).slice(2, 8); public readonly file: File; @@ -287,15 +289,48 @@ export class VertFile { .replace(/%extension%/g, originalExtension); }; - const blob = URL.createObjectURL( - new Blob([await this.result.file.arrayBuffer()], { - // type: to.slice(1), - type: "application/octet-stream", // use generic type to prevent browsers changing extension - }), - ); + const filename = `${format(filenameFormat)}${to}`; + + // larger files (>2gb) requires cache API + // blob constructor can't use arraybuffer above 2gb + const useCacheApi = this.result.file.size > MAX_BLOB_SIZE_LIMIT; + let blob: string; + + if (useCacheApi) { + const cache = await caches.open("vert-downloads"); + const cacheKey = `vert-download-${Date.now()}-${filename}`; + + const response = new Response(this.result.file.stream(), { + headers: { + "Content-Type": "application/octet-stream", + "Content-Length": this.result.file.size.toString(), + }, + }); + + await cache.put(cacheKey, response); + + const cachedResponse = await cache.match(cacheKey); + if (!cachedResponse) + throw new Error("Failed to cache file for download"); + + const cachedBlob = await cachedResponse.blob(); + blob = URL.createObjectURL(cachedBlob); + + setTimeout(() => { + cache.delete(cacheKey); + }, 3000); + } else { + blob = URL.createObjectURL( + new Blob([await this.result.file.arrayBuffer()], { + type: "application/octet-stream", + }), + ); + } + + // download const a = document.createElement("a"); a.href = blob; - a.download = `${format(filenameFormat)}${to}`; + a.download = filename; // force it to not open in a new tab a.target = "_blank"; a.style.display = "none"; From 039ca40718b85e0b98b901b9d6b67a98df4f4d03 Mon Sep 17 00:00:00 2001 From: Kamiliusz Date: Thu, 12 Feb 2026 12:47:42 +0100 Subject: [PATCH 018/118] Final fixes to pl.json --- messages/pl.json | 51 ++++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/messages/pl.json b/messages/pl.json index ceb9f86e..6c4c7252 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -5,7 +5,7 @@ "convert": "Konwertuj", "settings": "Ustawienia", "about": "O nas", - "toggle_theme": "Przełącz motyw" + "toggle_theme": "Zmień motyw" }, "footer": { "copyright": "© {year} VERT.", @@ -15,9 +15,9 @@ }, "upload": { "title": "Konwerter plików, który pokochasz.", - "subtitle": "Wszystkie zdjęcia, pliki audio oraz dokumenty przetwarzane są na Twoim urządzeniu. Pliki wideo konwertowane są na naszych bardzo szybkich serwerach. Bez limitu rozmiaru plików, bez reklam, z otwartym kodem źródłowym.", + "subtitle": "Wszystkie zdjęcia, pliki audio oraz dokumenty przetwarzane są na twoim urządzeniu. Pliki wideo konwertowane są na naszych bardzo szybkich serwerach. Bez limitu rozmiaru plików, bez reklam, z otwartym kodem źródłowym.", "uploader": { - "text": "Przeciągnij lub kliknij aby {action}", + "text": "Przeciągnij lub kliknij, aby {action}", "convert": "konwertować" }, "cards": { @@ -43,7 +43,7 @@ "partial_support": "Ten format może zostać przekonwertowany jedynie jako {direction}.", "direction_input": "wejście (z)", "direction_output": "wyjście (do)", - "video_server_processing": "Domyślnie, w celu przetworzenia, pliki wideo przesyłane są na serwer, dowiedz się jak ustawić lokalne przetwarzanie tutaj." + "video_server_processing": "Domyślnie, w celu przetworzenia, pliki wideo przesyłane są na serwer, dowiedz się, jak ustawić lokalne przetwarzanie tutaj." } }, "convert": { @@ -94,20 +94,20 @@ "cant_convert": "Nie możemy przekonwertować tego pliku.", "vertd_server": "co robisz..? powinieneś uruchomić serwer vertd!", "vertd_generic_view": "Wyświetl szczegóły błędu", - "vertd_generic_body": "Wystąpił błąd podczas próby konwersji Twojego wideo. Czy chcesz przesłać to wideo deweloperom w celu pomocy naprawy tego błędu? Przesłane zostanie jedynie wideo. Żadne identyfikatory nie zostaną przesłane.", + "vertd_generic_body": "Wystąpił błąd podczas próby konwersji twojego wideo. Czy chcesz przesłać to wideo deweloperom w celu pomocy w naprawie tego błędu? Przesłane zostanie jedynie wideo. Żadne identyfikatory nie zostaną przesłane.", "vertd_generic_title": "Błąd konwersji wideo", "vertd_generic_yes": "Prześlij wideo", "vertd_generic_no": "Nie przesyłaj", "vertd_failed_to_keep": "Nie udało się przechować wideo na serwerze: {error}", "vertd_details": "Wyświetl szczegóły błędu", - "vertd_details_body": "Jeśli wciśniesz przycisk prześlij, Twoje wideo również zostanie dołączone wraz z wpisem błędu, który zawsze jest do nas wysyłany do sprawdzenia. Następująca informacja jest wpisem, który automatycznie otrzymujemy:", + "vertd_details_body": "Jeśli wciśniesz przycisk prześlij, twoje wideo również zostanie dołączone wraz z wpisem błędu, który zawsze jest do nas wysyłany do sprawdzenia. Następująca informacja jest wpisem, który automatycznie otrzymujemy:", "vertd_details_footer": "Informacja ta użyta zostanie jedynie w celu rozwiązywania błędów oraz nigdy nie zostanie udostępniona. Zobacz naszą [privacy_link]politykę prywatności[/privacy_link] po więcej szczegółów.", "vertd_details_job_id": "Job ID: {jobId}", "vertd_details_from": "Z formatu: {from}", "vertd_details_to": "Do formatu: {to}", "vertd_details_error_message": "Wiadomość błędu: [view_link]Wyświetl wpis błędu[/view_link]", "vertd_details_close": "Zamknij", - "vertd_ratelimit": "Nie udało się przekonwertować Twojego wideo '{filename}' kilkukrotnie. Aby zapobiec przeciążeniom serwera, dalsze próby konwersji tego pliku zostały chwilowo zablokowane. Spróbuj ponownie później.", + "vertd_ratelimit": "Nie udało się przekonwertować twojego wideo '{filename}' kilkukrotnie. Aby zapobiec przeciążeniom serwera, dalsze próby konwersji tego pliku zostały chwilowo zablokowane. Spróbuj ponownie później.", "unsupported_format": "Wspierane są jedynie pliki obrazów, wideo, audio oraz dokumentów", "format_output_only": "Obecnie, ten format może być używany jedynie jako wyjście (konwersja do), nie jako wejście.", "vertd_not_found": "Nie udało się znaleźć instancji vertd w celu uruchomienia konwersji wideo. Czy jesteś pewny, że instancja URL jest ustawiona poprawnie?", @@ -126,12 +126,12 @@ }, "appearance": { "title": "Wygląd", - "brightness_theme": "Motyw jasności", - "brightness_description": "Wolisz motyw jasny jak słońce, czy ciemny jak cicha, spokojna noc?", + "brightness_theme": "Motyw", + "brightness_description": "Wolisz motyw jasny jak słońce czy ciemny jak cicha, spokojna noc?", "light": "Jasny", "dark": "Ciemny", "effect_settings": "Ustawienia efektów", - "effect_description": "Chciałbyś włączyć szalone efekty, czy preferujesz bardziej statyczne doświadczenie?", + "effect_description": "Chciałbyś włączyć szalone efekty czy preferujesz bardziej statyczne doświadczenie?", "enable": "Włącz", "disable": "Wyłącz" }, @@ -150,11 +150,11 @@ "default_format_audio": "Audio", "default_format_document": "Dokumenty", "metadata": "Metadane pliku", - "metadata_description": "To ustawienie pozwola ci ustalić czy metadane (EXIF, informacje o piosence, itp.) oryginalnego pliku zostaną zachowane w przekonwertowanym pliku.", + "metadata_description": "To ustawienie pozwala ci ustalić czy metadane (EXIF, informacje o piosence, itp.) oryginalnego pliku zostaną zachowane w przekonwertowanym pliku.", "keep": "Zachowaj", "remove": "Usuń", "quality": "Jakość konwersji", - "quality_description": "To ustawienie pozwala ustalić domyślną jakość wyjściową przekonwertowanego pliku (w danej kategorii). Wyższe wartości mogą skutkować dłuższym czasem konwersji oraz większym rozmiarem pliku.", + "quality_description": "To ustawienie pozwala ci ustalić domyślną jakość wyjściową przekonwertowanego pliku (w danej kategorii). Wyższe wartości mogą skutkować dłuższym czasem konwersji oraz większym rozmiarem pliku.", "quality_video": "To ustawienie pozwala ustalić domyślną jakość wyjściową przekonwertowanego pliku wideo. Wyższe wartości mogą skutkować dłuższym czasem konwersji oraz większym rozmiarem pliku.", "quality_audio": "Audio (kbps)", "quality_images": "Zdjęcia (%)", @@ -167,11 +167,11 @@ "available": "dostępna, commit id {commitId}", "unavailable": "niedostępna (czy url jest poprawny?)", "description": "Projekt vertd jest serwerową nakładką na FFmpeg. To pozwala ci konwertować wideo za pomocą interfejsu sieciowego VERT, nadal będąc w stanie wykorzystywać moc swojego procesora graficznego (GPU) w celu jak najszybszej konwersji.", - "hosting_info": "Dla Twojej wygody hostujemy publiczną instancję, jednak ustawienie hosta na Twoim własnym komputerze lub serwerze jest stosunkowo proste jeśli wiesz co robisz. Możesz pobrać binaria serwera [vertd_link]tutaj[/vertd_link] - w przyszłości proces tej konfiguracji będzie łatwiejszy, więc czekajcie na więcej informacji!", + "hosting_info": "Dla twojej wygody hostujemy publiczną instancję, jednak ustawienie hosta na twoim własnym komputerze lub serwerze jest stosunkowo proste, jeśli wiesz, co robisz. Możesz pobrać binaria serwera [vertd_link]tutaj[/vertd_link] - w przyszłości proces tej konfiguracji będzie łatwiejszy, więc czekajcie na więcej informacji!", "instance": "Instancja", "url_placeholder": "Przykładowo: http://localhost:24153", "conversion_speed": "Prędkość konwersji", - "speed_description": "To opisuje kompromis pomiędzy prędkością a jakością. Większa prędkość może skutkować niższą jakością, ale wykona swoje zadanie szybciej", + "speed_description": "To opisuje kompromis pomiędzy prędkością a jakością. Większa prędkość może skutkować niższą jakością, ale wykona swoje zadanie szybciej.", "speeds": { "very_slow": "Najwolniejsza", "slower": "Wolniejsza", @@ -198,7 +198,7 @@ "files_cached": "{size} ({count} plików)", "loading_cache": "Ładowanie...", "total_size": "Łączny rozmiar", - "files_cached_label": "Przechowywanych plików", + "files_cached_label": "Przechowywane pliki", "cache_cleared": "Pomyślnie wyczyszczono pamięć podręczną!", "cache_clear_error": "Nie udało się wyczyścić pamięci podręcznej.", "site_data_title": "Zarządzanie danymi strony", @@ -219,7 +219,7 @@ "title": "O nas", "why": { "title": "Dlaczego VERT?", - "description": "Konwertery plików od zawsze nas zawodziły. Są brzydkie, przepełnione reklamami, ale przede wszystkim, powolne. Postanowiliśmy rozwiązać ten problem raz na zawsze poprzez stworzenie alternatywy, która rozwiązuje wszystkie te problemy i nie tylko.

Wszystkie pliki nie będące wideo są w pełni konwertowane na urządzeniu; to oznacza brak opóźnienia pomiędzy przesyłaniem a odbieraniem plików z serwera, a my nie możemy podejrzeć plików, które konwertujesz.

Pliki wideo przesyłane zostają na nasz bardzo szybki serwer RTX 4000 Ada. Twoje pliki wideo przechowywane są tam przez godzinę jeśli ich nie przekonwertujesz. Jeżeli je przekonwertujesz, wideo zostanie na serwerze przez godzinę lub do momentu jego pobrania. Po tym czasie, plik zostanie usunięty z naszego serwera." + "description": "Konwertery plików od zawsze nas zawodziły. Są brzydkie, przepełnione reklamami, ale przede wszystkim powolne. Postanowiliśmy rozwiązać ten problem raz na zawsze poprzez stworzenie alternatywy, która rozwiązuje wszystkie te problemy i nie tylko.

Wszystkie pliki niebędące wideo są w pełni konwertowane na urządzeniu; to oznacza brak opóźnienia pomiędzy przesyłaniem a odbieraniem plików z serwera, a my nie możemy podejrzeć plików, które konwertujesz.

Pliki wideo przesyłane zostają na nasz bardzo szybki serwer RTX 4000 Ada. Twoje pliki wideo przechowywane są tam przez godzinę jeśli ich nie przekonwertujesz. Jeżeli je przekonwertujesz, wideo zostanie na serwerze przez godzinę lub do momentu jego pobrania. Po tym czasie, plik zostanie usunięty z naszego serwera." }, "sponsors": { "title": "Sponsorzy", @@ -240,9 +240,9 @@ "custom": "Własna kwota", "pay_now": "Zapłać teraz", "donate_amount": "Dotacja w kwocie ${amount} USD", - "thank_you": "Dziękujemy za Twoje wsparcie!", + "thank_you": "Dziękujemy za twoje wsparcie!", "payment_failed": "Płatność nieudana: {message}{period} Nie zostałeś obciążony.", - "donation_error": "Wystąpił błąd podczas przetwarzania Twojej dotacji. Spróbuj ponownie później.", + "donation_error": "Wystąpił błąd podczas przetwarzania twojej dotacji. Spróbuj ponownie później.", "payment_error": "Wystąpił błąd podczas pobierania danych płatności. Spróbuj ponownie później.", "donation_notice_official": "Twoje dotacje trafiają do oficjalnej instancji VERT (vert.sh) i wspierają rozwój projektu.", "donation_notice_unofficial": "Twoje dotacje trafiają do operatora tej instancji VERT. Jeśli chcesz wesprzeć oficjalnych deweloperów VERT, odwiedź [official_link]vert.sh[/official_link]." @@ -256,7 +256,7 @@ "github_description": "Wielkie podziękowania dla tych wszystkich osób za pomoc! [github_link]Też chcesz pomóc?[/github_link]", "no_contributors": "Wygląda na to, że jeszcze nikt nie wspomógł... [contribute_link]bądź pierwszym, który nam pomoże![/contribute_link]", "libraries": "Biblioteki", - "libraries_description": "Wielkie podziękowania dla FFmpeg (audio, wideo), ImageMagick (zdjęcia) oraz Pandoc (dokumenty) za utrzymywanie tak wspaniałych bibliotek przez tak wiele lat. VERT opiera się na nich aby umożliwić ci Twoje konwersje.", + "libraries_description": "Wielkie podziękowania dla FFmpeg (audio, wideo), ImageMagick (zdjęcia) oraz Pandoc (dokumenty) za utrzymywanie tak wspaniałych bibliotek przez tak wiele lat. VERT opiera się na nich aby umożliwić ci twoje konwersje.", "roles": { "lead_developer": "Główny deweloper; backend konwersji, implementacja UI", "developer": "Deweloper; implementacja UI", @@ -277,7 +277,7 @@ "ffmpeg": "Błąd przy wczytywaniu FFmpeg, niektóre usługi mogą działać niezgodnie z oczekiwaniami.", "pandoc": "Błąd przy wczytywaniu Pandoc worker, konwersja dokumentów może działać niezgodnie z oczekiwaniami.", "no_audio": "Nie odnaleziono ścieżki audio.", - "invalid_rate": "Wprowadzona została nieprawidłowa częstotliwość próbkowania: {rate}Hz", + "invalid_rate": "Wprowadzono nieprawidłową częstotliwość próbkowania: {rate}Hz", "file_too_large": "Ten plik przekracza limit {limit}GB przeglądarki / urządzenia. Spróbuj użyć przeglądarki Firefox bądź Safari do konwersji tego dużego pliku, gdyż przeglądarki te mają mniejsze ograniczenia." } }, @@ -285,11 +285,11 @@ "title": "Polityka Prywatności", "summary": { "title": "Podsumowanie", - "description": "Polityka prywatności VERT jest bardzo prosta: nie zbieramy ani nie przechowujemy żadnych Twoich danych. Nie używamy plików cookie ani trackerów, analityka jest w pełni prywatna a wszystkie konwersje (oprócz plików wideo) wykonywane są lokalnie w Twojej przeglądarce. Pliki wideo są usuwane po pobraniu lub po upływie godziny, chyba że wyrazisz zgodę na ich przechowanie; w takim przypadku użyte zostaną jedynie w celu rozwiązywaniu problemów. VERT samodzielnie hostuje instancję Coolify do hostowania strony internetowej oraz vertd (do konwersji wideo) oraz instancję Plausible do anonimowych i agregowanych analiz. Używamy Stripe w celu przetwarzania dotacji, który może zbierać niektóre dane w celu zabezpieczenia przed oszustwami.

Należy pamiętać, że dotyczy to jedynie oficjalnej instancji VERT pod adresem [vert_link]vert.sh[/vert_link]; instancje stron trzecich mogą przetwarzać Twe dane w inny sposób." + "description": "Polityka prywatności VERT jest bardzo prosta: nie zbieramy ani nie przechowujemy żadnych twoich danych. Nie używamy plików cookie ani trackerów, analityka jest w pełni prywatna, a wszystkie konwersje (oprócz plików wideo) wykonywane są lokalnie w twojej przeglądarce. Pliki wideo są usuwane po pobraniu lub po upływie godziny, chyba że wyrazisz zgodę na ich przechowanie; w takim przypadku użyte zostaną jedynie w celu rozwiązywania problemów. VERT samodzielnie hostuje instancję Coolify do hostowania strony internetowej oraz vertd (do konwersji wideo), a także instancję Plausible do anonimowych i agregowanych analiz. Używamy Stripe w celu przetwarzania dotacji, który może zbierać niektóre dane w celu zabezpieczenia przed oszustwami.

Należy pamiętać, że dotyczy to jedynie oficjalnej instancji VERT pod adresem [vert_link]vert.sh[/vert_link]; instancje stron trzecich mogą przetwarzać twoje dane w inny sposób." }, "conversions": { "title": "Konwersje", - "description": "Większość konwersji (zdjęć, dokumentów, audio) wykonywane są w pełni lokalnie na Twoim urządzeniu z użyciem wersji WebAssembly odpowiednich narzędzi (np. ImageMagick, Pandoc, FFmpeg). Oznacza to, że Twoje pliki nigdy nie opuszczają Twojego urządzenia a my nigdy nie będziemy mieli do nich dostępu.

Konwersje wideo wykonywane są na naszych serwerach, ponieważ wymagają one więcej mocy obliczeniowej i obecnie nie mogą być wykonywane odpowiednio szybko za pomocą przeglądarki. Pliki wideo które konwertujesz są usuwane po pobraniu lub po upływie godziny, chyba że wyrazisz zgodę na ich dłuższe przechowanie jedynie w celach rozwiązywania problemów." + "description": "Większość konwersji (zdjęć, dokumentów, audio) wykonywana jest w pełni lokalnie na twoim urządzeniu z użyciem wersji WebAssembly odpowiednich narzędzi (np. ImageMagick, Pandoc, FFmpeg). Oznacza to, że twoje pliki nigdy nie opuszczają twojego urządzenia, a my nigdy nie będziemy mieli do nich dostępu.

Konwersje wideo wykonywane są na naszych serwerach, ponieważ wymagają one więcej mocy obliczeniowej i obecnie nie mogą być wykonywane odpowiednio szybko za pomocą przeglądarki. Pliki wideo które konwertujesz są usuwane po pobraniu lub po upływie godziny, chyba że wyrazisz zgodę na ich dłuższe przechowanie jedynie w celach rozwiązywania problemów." }, "donations": { "title": "Dotacje", @@ -301,9 +301,9 @@ "list_job_id": "Job ID, czyli anonimizowana nazwa pliku", "list_format_from": "Format z którego konwertowano", "list_format_to": "Format na który konwertowano", - "list_stderr": "Wyjście FFmpeg stderr Twojego zadania (wiadomość błędu)", + "list_stderr": "Wyjście FFmpeg stderr twojego zadania (wiadomość błędu)", "list_video": "Sam plik wideo (jeśli wyrażono na to zgodę)", - "footer": "Informacje te używane są jedynie w celu diagnozy problemu konwersji. Sam plik wideo przechowany będzie tylko i wyłącznie po wyrażeniu Twojej zgody, gdzie będzie wykorzystywany jedynie w celu naprawy problemów." + "footer": "Informacje te używane są jedynie w celu diagnozy problemu konwersji. Sam plik wideo przechowany będzie tylko i wyłącznie po wyrażeniu twojej zgody, gdzie będzie wykorzystywany jedynie w celu naprawy problemów." }, "analytics": { "title": "Analityka", @@ -311,7 +311,7 @@ }, "local_storage": { "title": "Lokalna Pamięć", - "description": "Używamy lokalnej pamięci Twojej przeglądarki aby zapisać Twoje ustawienia oraz pamięci sesji Twojej przeglądarki aby tymczasowo przechować listę kontrybutorów GitHub w zakładce \"O nas\" aby zredukować powtarzalne zapytania API GitHub. Żadne wrażliwe dane nie są przechowywane ani przesyłane.

Wersje WebAssembly narzędzi, których używamy (FFmpeg, ImageMagick, Pandoc) również są przechowywane lokalnie w Twojej przeglądarce po Twoim pierwszym odwiedzeniu strony abyś nie musiał pobierać ich przy każdym odwiedzeniu strony. Żadne wrażliwe dane nie są przechowywane ani przesyłane. Możesz zobaczyć lub usunąć te dane w dowolnym momencie w sekcji \"Prywatność i dane\" w zakładce [settings_link]ustawienia[/settings_link]." + "description": "Używamy lokalnej pamięci twojej przeglądarki aby zapisać twoje ustawienia oraz pamięci sesji twojej przeglądarki aby tymczasowo przechować listę kontrybutorów GitHub w zakładce \"O nas\" aby zredukować powtarzalne zapytania do API GitHub. Żadne wrażliwe dane nie są przechowywane ani przesyłane.

Wersje WebAssembly narzędzi, których używamy (FFmpeg, ImageMagick, Pandoc) również są przechowywane lokalnie w twojej przeglądarce po twoim pierwszym odwiedzeniu strony abyś nie musiał pobierać ich przy każdym odwiedzeniu strony. Żadne wrażliwe dane nie są przechowywane ani przesyłane. Możesz zobaczyć lub usunąć te dane w dowolnym momencie w sekcji \"Prywatność i dane\" w zakładce [settings_link]ustawienia[/settings_link]." }, "contact": { "title": "Kontakt", @@ -322,5 +322,4 @@ "toast": { "insecure_context": "Odwiedzasz VERT w niezabezpieczonym kontekście (np. uzyskujesz dostęp poprzez HTTP zamiast HTTPS). Niektóre funkcje mogą działać niezgodnie z oczekiwaniami." } - } From 30bbb764aeb5156f77de11aca1b71fcf263b4081 Mon Sep 17 00:00:00 2001 From: Maya Date: Thu, 12 Feb 2026 16:03:47 +0300 Subject: [PATCH 019/118] feat: start settings modal component --- messages/en.json | 4 + .../functional/FormatDropdown.svelte | 81 +++++++++++++ src/lib/components/functional/Modal.svelte | 113 ++++++++++++++++++ src/lib/components/layout/Dialogs.svelte | 2 +- src/lib/types/file.svelte.ts | 8 ++ src/routes/convert/+page.svelte | 3 +- 6 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 src/lib/components/functional/Modal.svelte diff --git a/messages/en.json b/messages/en.json index 220d5743..26614f2a 100644 --- a/messages/en.json +++ b/messages/en.json @@ -81,6 +81,10 @@ "no_formats": "No formats available", "no_results": "No formats match your search" }, + "settings": { + "settings": "Settings", + "title": "File conversion settings" + }, "tooltips": { "unknown_file": "Unknown file type", "audio_file": "Audio file", diff --git a/src/lib/components/functional/FormatDropdown.svelte b/src/lib/components/functional/FormatDropdown.svelte index d91d7371..321abab4 100644 --- a/src/lib/components/functional/FormatDropdown.svelte +++ b/src/lib/components/functional/FormatDropdown.svelte @@ -8,6 +8,8 @@ import { onMount } from "svelte"; import { quintOut } from "svelte/easing"; import { VertFile } from "$lib/types"; + import Modal from "./Modal.svelte"; + import Dropdown from "./Dropdown.svelte"; type Props = { categories: Categories; @@ -331,6 +333,13 @@ newFiles.forEach((f) => files.add(f)); }; + let showSettingsModal = $state(false); + const settings = () => { + if (!file) return; + // TODO: temporary - will have individual settings modals for each converter and show those instead + showSettingsModal = true; + }; + onMount(() => { const handleClickOutside = (e: MouseEvent) => { if (dropdown && !dropdown.contains(e.target as Node)) { @@ -355,6 +364,68 @@ }); +{#if showSettingsModal} + (showSettingsModal = false), + }, + { + text: "Apply", + action: () => (showSettingsModal = false), + primary: true, + }, + ]} + onclose={() => (showSettingsModal = false)} + > +
+
+
+

Format Settings

+

+ Configure conversion options for {file?.name} +

+
+ +
+

Example dropdown

+ { + console.log("selected format", e); + selected = e; + }} + /> +
+ +
+

Resolution

+ +
+ +
+

Frame Rate (FPS)

+ +
+
+
+
+{/if} +
{/if} + {#if file} +
+ +
+ {/if} {/if} diff --git a/src/lib/components/functional/Modal.svelte b/src/lib/components/functional/Modal.svelte new file mode 100644 index 00000000..2eb811c5 --- /dev/null +++ b/src/lib/components/functional/Modal.svelte @@ -0,0 +1,113 @@ + + + diff --git a/src/lib/components/layout/Dialogs.svelte b/src/lib/components/layout/Dialogs.svelte index b496244f..0682825e 100644 --- a/src/lib/components/layout/Dialogs.svelte +++ b/src/lib/components/layout/Dialogs.svelte @@ -16,7 +16,7 @@ {#if dialogList.length > 0}
From d54094722f513f0e58c1c0c32106dc62fb44a31d Mon Sep 17 00:00:00 2001 From: Maya Date: Fri, 13 Feb 2026 09:04:40 +0300 Subject: [PATCH 020/118] fix: clean up/use FancyInput everywhere forgor --- .../components/functional/FancyInput.svelte | 28 ++++++------------- .../functional/FormatDropdown.svelte | 7 +++-- src/lib/sections/settings/Vertd.svelte | 3 +- 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/src/lib/components/functional/FancyInput.svelte b/src/lib/components/functional/FancyInput.svelte index c1b6740d..179f8af8 100644 --- a/src/lib/components/functional/FancyInput.svelte +++ b/src/lib/components/functional/FancyInput.svelte @@ -1,40 +1,28 @@
{#if prefix} diff --git a/src/lib/components/functional/FormatDropdown.svelte b/src/lib/components/functional/FormatDropdown.svelte index 321abab4..e1198a4b 100644 --- a/src/lib/components/functional/FormatDropdown.svelte +++ b/src/lib/components/functional/FormatDropdown.svelte @@ -10,6 +10,7 @@ import { VertFile } from "$lib/types"; import Modal from "./Modal.svelte"; import Dropdown from "./Dropdown.svelte"; + import FancyInput from "./FancyInput.svelte"; type Props = { categories: Categories; @@ -406,7 +407,7 @@

Resolution

-

Frame Rate (FPS)

-
- (null); let abortController: AbortController | null = null; @@ -132,7 +133,7 @@ settingsStyle /> {#if VertdInstance.instance.innerData().type === "custom"} - Date: Sat, 14 Feb 2026 00:01:02 +0300 Subject: [PATCH 021/118] feat: start how settings is defined god kill me i fought with paraglide for a while --- messages/en.json | 21 ++- .../functional/FormatDropdown.svelte | 70 +--------- .../functional/SettingsModal.svelte | 125 ++++++++++++++++++ src/lib/converters/converter.svelte.ts | 32 ++++- src/lib/converters/magick.svelte.ts | 79 +++++++++++ src/lib/types/conversion-settings.ts | 18 +++ src/lib/types/file.svelte.ts | 18 ++- 7 files changed, 293 insertions(+), 70 deletions(-) create mode 100644 src/lib/components/functional/SettingsModal.svelte create mode 100644 src/lib/types/conversion-settings.ts diff --git a/messages/en.json b/messages/en.json index 26614f2a..2dbbfb39 100644 --- a/messages/en.json +++ b/messages/en.json @@ -83,7 +83,26 @@ }, "settings": { "settings": "Settings", - "title": "File conversion settings" + "title": "File conversion settings", + "image": { + "quality": "Quality", + "depth": "Color depth", + "color_space": "Color space", + "transparency": "Transparency", + "metadata": "Metadata" + }, + "audio": { + "quality": "Quality", + "rate": "Sample rate", + "metadata": "Metadata" + }, + "video": { + "quality": "Quality", + "metadata": "Metadata" + }, + "document": { + "metadata": "Metadata" + } }, "tooltips": { "unknown_file": "Unknown file type", diff --git a/src/lib/components/functional/FormatDropdown.svelte b/src/lib/components/functional/FormatDropdown.svelte index e1198a4b..efa8200a 100644 --- a/src/lib/components/functional/FormatDropdown.svelte +++ b/src/lib/components/functional/FormatDropdown.svelte @@ -11,6 +11,7 @@ import Modal from "./Modal.svelte"; import Dropdown from "./Dropdown.svelte"; import FancyInput from "./FancyInput.svelte"; + import SettingsModal from "./SettingsModal.svelte"; type Props = { categories: Categories; @@ -335,9 +336,9 @@ }; let showSettingsModal = $state(false); + const settings = () => { if (!file) return; - // TODO: temporary - will have individual settings modals for each converter and show those instead showSettingsModal = true; }; @@ -365,66 +366,8 @@ }); -{#if showSettingsModal} - (showSettingsModal = false), - }, - { - text: "Apply", - action: () => (showSettingsModal = false), - primary: true, - }, - ]} - onclose={() => (showSettingsModal = false)} - > -
-
-
-

Format Settings

-

- Configure conversion options for {file?.name} -

-
- -
-

Example dropdown

- { - console.log("selected format", e); - selected = e; - }} - /> -
- -
-

Resolution

- -
- -
-

Frame Rate (FPS)

- -
-
-
-
+{#if showSettingsModal && file} + (showSettingsModal = false)} /> {/if}
-
- {/if} - {#if file} + {:else if file}
{/each}
diff --git a/src/lib/components/functional/SettingsModal.svelte b/src/lib/components/functional/SettingsModal.svelte index 6052d54a..dfc32a6e 100644 --- a/src/lib/components/functional/SettingsModal.svelte +++ b/src/lib/components/functional/SettingsModal.svelte @@ -104,7 +104,13 @@ {#if setting.type === "select"} opt.value, + (opt) => + typeof opt === "string" + ? { + value: opt, + label: opt, + } + : opt, ) || []} selected={settings[setting.key] ?? file.conversionSettings[ diff --git a/src/lib/converters/ffmpeg.svelte.ts b/src/lib/converters/ffmpeg.svelte.ts index 9d7baad0..c7ea958e 100644 --- a/src/lib/converters/ffmpeg.svelte.ts +++ b/src/lib/converters/ffmpeg.svelte.ts @@ -121,7 +121,10 @@ export class FFmpegConverter extends Converter { default: global.ffmpegQuality, options: CONVERSION_BITRATES.map((b) => ({ value: b, - label: b, + label: + b === "auto" || b === "custom" + ? m[`convert.settings.common.${b}`]() + : `${b} kbps`, })), hasCustomInput: true, customInputKey: "customBitrate", @@ -138,7 +141,10 @@ export class FFmpegConverter extends Converter { : global.ffmpegSampleRate, options: SAMPLE_RATES.map((r) => ({ value: r, - label: r, + label: + r === "auto" || r === "custom" + ? m[`convert.settings.common.${r}`]() + : `${r} Hz`, })), hasCustomInput: true, customInputKey: "customSampleRate", @@ -179,11 +185,9 @@ export class FFmpegConverter extends Converter { return [bitrate, sampleRate, tracks, channels, metadata]; } - public async getDefaultSettings( - input: VertFile, - ): Promise { + public async getDefaultSettings(): Promise { const defaults: ConversionSettings = {}; - const settings = await this.getAvailableSettings(input); + const settings = await this.getAvailableSettings(); settings.forEach((setting) => { defaults[setting.key] = setting.default; }); @@ -200,7 +204,7 @@ export class FFmpegConverter extends Converter { const conversionSettings = Object.keys(settings).length > 0 ? settings - : await this.getDefaultSettings(input); // use defaults if not provided + : await this.getDefaultSettings(); // use defaults if not provided const isAlac = to === ".alac"; if (isAlac) to = ".m4a"; diff --git a/src/lib/converters/magick.svelte.ts b/src/lib/converters/magick.svelte.ts index 34377742..17d4a573 100644 --- a/src/lib/converters/magick.svelte.ts +++ b/src/lib/converters/magick.svelte.ts @@ -136,13 +136,12 @@ export class MagickConverter extends Converter { label: m["convert.settings.image.depth"](), type: "select", default: "auto", - // somehow implement custom option options: [ { value: "auto", label: "Auto" }, + { value: "custom", label: "Custom" }, { value: "8", label: "8-bit" }, { value: "16", label: "16-bit" }, { value: "32", label: "32-bit" }, - { value: "custom", label: "Custom" }, ], }; @@ -166,8 +165,12 @@ export class MagickConverter extends Converter { }; // TODO: check other formats for transparency support - const fromJpeg = input.from === ".jpg" || input.from === ".jpeg" || input.from === ".jfif"; - const toJpeg = input.to === ".jpg" || input.to === ".jpeg" || input.to === ".jfif"; + const fromJpeg = + input.from === ".jpg" || + input.from === ".jpeg" || + input.from === ".jfif"; + const toJpeg = + input.to === ".jpg" || input.to === ".jpeg" || input.to === ".jfif"; const transparency: SettingDefinition = { key: "transparency", label: m["convert.settings.image.transparency"](), @@ -188,9 +191,11 @@ export class MagickConverter extends Converter { return [quality, depth, colorSpace, transparency, metadata]; } - public async getDefaultSettings(): Promise { + public async getDefaultSettings( + input: VertFile, + ): Promise { const defaults: ConversionSettings = {}; - const settings = await this.getAvailableSettings(); + const settings = await this.getAvailableSettings(input); settings.forEach((setting) => { defaults[setting.key] = setting.default; }); @@ -272,7 +277,7 @@ export class MagickConverter extends Converter { const conversionSettings = JSON.stringify( Object.keys(settings).length > 0 ? settings // user-provided settings - : await this.getDefaultSettings(), // use defaults if not provided + : await this.getDefaultSettings(input), // use defaults if not provided ); const convertMsg: WorkerMessage = { type: "convert", diff --git a/src/lib/converters/pandoc.svelte.ts b/src/lib/converters/pandoc.svelte.ts index 6dfe84dc..e447088d 100644 --- a/src/lib/converters/pandoc.svelte.ts +++ b/src/lib/converters/pandoc.svelte.ts @@ -61,8 +61,8 @@ export class PandocConverter extends Converter { from: file.from, to, }, - compression: null, id: file.id, + conversionSettings: "", // no settings for pandoc yet }; worker.postMessage(convertMsg); const result = await waitForMessage(worker); diff --git a/src/lib/converters/vertd.svelte.ts b/src/lib/converters/vertd.svelte.ts index 5792f2f5..8cad8d77 100644 --- a/src/lib/converters/vertd.svelte.ts +++ b/src/lib/converters/vertd.svelte.ts @@ -483,7 +483,12 @@ export class VertdConverter extends Converter { default: "auto", options: CONVERSION_BITRATES.map((b) => ({ value: b, - label: b, + label: + b === "auto" + ? m["convert.settings.common.auto"]() + : b === "custom" + ? m["convert.settings.common.custom"]() + : `${b} kbps`, })), hasCustomInput: true, customInputKey: "customBitrate", @@ -497,7 +502,12 @@ export class VertdConverter extends Converter { default: "auto", options: SAMPLE_RATES.map((r) => ({ value: r, - label: r, + label: + r === "auto" + ? m["convert.settings.common.auto"]() + : r === "custom" + ? m["convert.settings.common.custom"]() + : `${r} Hz`, })), hasCustomInput: true, customInputKey: "customSampleRate", diff --git a/src/lib/sections/settings/Conversion.svelte b/src/lib/sections/settings/Conversion.svelte index e9598390..1e00f865 100644 --- a/src/lib/sections/settings/Conversion.svelte +++ b/src/lib/sections/settings/Conversion.svelte @@ -43,7 +43,9 @@ {m["settings.conversion.filename_format"]()}

- {@html sanitize(m["settings.conversion.filename_description"]())} + {@html sanitize( + m["settings.conversion.filename_description"](), + )}

@@ -102,7 +110,9 @@ size="24" class="inline-block mr-2" /> - {m["settings.conversion.default_format_enable"]()} + {m[ + "settings.conversion.default_format_enable" + ]()}
@@ -193,9 +205,7 @@

{m["settings.conversion.metadata"]()}

-

+

{m[ "settings.conversion.metadata_description" ]()} @@ -273,8 +283,17 @@ ]()}

- b.toString(), + options={CONVERSION_BITRATES.map( + (b) => ({ + value: b.toString(), + label: + b === "auto" || + b === "custom" + ? m[ + `convert.settings.common.${b}` + ]() + : `${b} kbps`, + }), )} selected={settings.ffmpegQuality.toString()} onselect={(option: string) => @@ -290,13 +309,19 @@ {m["settings.conversion.rate"]()}

- r.toString(), - )} + options={SAMPLE_RATES.map((r) => ({ + value: r.toString(), + label: + r === "auto" || r === "custom" + ? m[ + `convert.settings.common.${r}` + ]() + : `${r} Hz`, + }))} selected={settings.ffmpegSampleRate.toString()} onselect={(option: string) => { settings.ffmpegSampleRate = - option as SampleRate; + option as SampleRate as string; }} settingsStyle /> From e61214619b0b6dfab3d4ed2c7d94fa5e9a794099 Mon Sep 17 00:00:00 2001 From: Maya Date: Thu, 19 Feb 2026 20:08:14 +0300 Subject: [PATCH 035/118] fix: dropdown menu fixes --- src/lib/components/functional/Dropdown.svelte | 97 +++++++++++++------ 1 file changed, 65 insertions(+), 32 deletions(-) diff --git a/src/lib/components/functional/Dropdown.svelte b/src/lib/components/functional/Dropdown.svelte index c6205df2..f107d097 100644 --- a/src/lib/components/functional/Dropdown.svelte +++ b/src/lib/components/functional/Dropdown.svelte @@ -1,7 +1,6 @@ @@ -68,6 +100,7 @@ bind:this={dropdown} > - {#if open} -
- {#each options as option} - - {/each} -
- {/if}
+ +{#if open} +
+ {#each options as option} + + {/each} +
+{/if} From 3dbb80a309e7f28a55f91643341f3d9ee9e7c9ce Mon Sep 17 00:00:00 2001 From: Maya Date: Fri, 20 Feb 2026 18:16:01 +0300 Subject: [PATCH 036/118] fix: build failure oops { resolve(structuredClone(o)); From ff7456b8b94c211252f924b6a8a87bb10f626deb Mon Sep 17 00:00:00 2001 From: Maya Date: Sat, 21 Feb 2026 19:49:34 +0300 Subject: [PATCH 037/118] fix: all categories/formats fallback fall back to showing everything if there is no formats returned for some reason & add more logging. some people are seeing this for some reason and idk why --- messages/en.json | 3 +- .../functional/FormatDropdown.svelte | 60 ++++++++++++++++++- src/routes/convert/+page.svelte | 1 - 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/messages/en.json b/messages/en.json index fc2e363c..82791cf7 100644 --- a/messages/en.json +++ b/messages/en.json @@ -79,7 +79,8 @@ "image": "Image", "placeholder": "Search format", "no_formats": "No formats available", - "no_results": "No formats match your search" + "no_results": "No formats match your search", + "fallback": "An error occurred loading available categories & formats, showing everything." }, "settings": { "settings": "Settings", diff --git a/src/lib/components/functional/FormatDropdown.svelte b/src/lib/components/functional/FormatDropdown.svelte index 2dc23ff4..6ebcd051 100644 --- a/src/lib/components/functional/FormatDropdown.svelte +++ b/src/lib/components/functional/FormatDropdown.svelte @@ -9,6 +9,7 @@ import { quintOut } from "svelte/easing"; import { VertFile } from "$lib/types"; import SettingsModal from "./SettingsModal.svelte"; + import { log } from "$lib/util/logger"; type Props = { categories: Categories; @@ -41,6 +42,11 @@ $effect(() => { if (currentCategory) return; + log( + ["dropdown", "init"], + `initializing category, file: ${file?.name}, from: ${from}`, + ); + // find the category whose formats overlap most with the converters for this file (or all files) // this finds the best matching category based on the formats supported by the converters const pickCategoryFromConverters = ( @@ -72,6 +78,12 @@ : file.converters : files.files.flatMap((f) => f.converters); + log( + ["dropdown", "init"], + `checking converters:`, + convertersToCheck.map((c) => c.formatStrings()), + ); + // if file is provided, first try to find its category by input format let detectedCategory: string | null = null; if (file && from) { @@ -79,6 +91,11 @@ Object.keys(categories).find((cat) => categories[cat].formats.includes(from), ) || null; + log( + ["dropdown", "init"], + `detected category from input format (${from}):`, + detectedCategory, + ); } // fallback to category with most converter overlap if input category not found @@ -87,6 +104,8 @@ pickCategoryFromConverters(convertersToCheck) || Object.keys(categories)[0]; + log(["dropdown", "init"], `final detected category:`, detectedCategory); + currentCategory = detectedCategory; rootCategory = detectedCategory; }); @@ -143,9 +162,33 @@ ) : []; + // if no formats found at all, show everything + if (formats.length !== 0) { + const allCategories = Object.keys(categories); + // show formats for current category if set, otherwise all formats + const fallbackFormats = + currentCategory && allCategories.includes(currentCategory) + ? categories[currentCategory].formats + : allCategories.flatMap( + (cat) => categories[cat].formats, + ); + + log( + ["dropdown", "filter"], + `no formats found for category ${currentCategory}, showing all categories and formats as fallback`, + ); + + return { + categories: allCategories, + formats: fallbackFormats, + isFallback: true, + }; + } + return { categories: availableCategories, formats, + isFallback: false, }; } const searchLower = normalize(searchQuery); @@ -162,6 +205,7 @@ return { categories: availableCategories, formats: [], + isFallback: false, }; } @@ -198,6 +242,7 @@ ? matchingCategories : availableCategories, formats: filteredFormats, + isFallback: false, }; }); @@ -451,16 +496,21 @@ {}} id="format-search" autocomplete="off" + disabled={filteredData.isFallback} /> @@ -477,6 +527,12 @@ {/if}
+ + {#if filteredData.isFallback} +
+ {m["convert.dropdown.fallback"]()} +
+ {/if}
{#each filteredData.categories as category} diff --git a/src/routes/convert/+page.svelte b/src/routes/convert/+page.svelte index b93ce0cb..2a8a9ce1 100644 --- a/src/routes/convert/+page.svelte +++ b/src/routes/convert/+page.svelte @@ -32,7 +32,6 @@ import { Settings } from "$lib/sections/settings/index.svelte"; import { MAX_ARRAY_BUFFER_SIZE } from "$lib/store/index.svelte"; import { GB } from "$lib/util/consts"; - import { log } from "$lib/util/logger"; let processedFileIds = $state(new Set()); From 6980d6dc05fb178f3fb54e2371e3d73c9c3a0375 Mon Sep 17 00:00:00 2001 From: Maya Date: Mon, 23 Feb 2026 21:43:59 +0300 Subject: [PATCH 038/118] feat: send custom settings to vertd --- .../functional/FormatDropdown.svelte | 2 +- src/lib/converters/vertd.svelte.ts | 51 +++++++++---------- src/lib/sections/settings/index.svelte.ts | 2 +- 3 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/lib/components/functional/FormatDropdown.svelte b/src/lib/components/functional/FormatDropdown.svelte index 6ebcd051..99f11ce6 100644 --- a/src/lib/components/functional/FormatDropdown.svelte +++ b/src/lib/components/functional/FormatDropdown.svelte @@ -163,7 +163,7 @@ : []; // if no formats found at all, show everything - if (formats.length !== 0) { + if (formats.length === 0) { const allCategories = Object.keys(categories); // show formats for current category if set, otherwise all formats const fallbackFormats = diff --git a/src/lib/converters/vertd.svelte.ts b/src/lib/converters/vertd.svelte.ts index 8cad8d77..8ca9ca80 100644 --- a/src/lib/converters/vertd.svelte.ts +++ b/src/lib/converters/vertd.svelte.ts @@ -101,8 +101,7 @@ interface StartJobMessage { token: string; jobId: string; to: string; - speed: ConversionSpeed; - keepMetadata: boolean; + settings: ConversionSettings; }; } @@ -413,12 +412,12 @@ export class VertdConverter extends Converter { value: "custom", label: m["convert.settings.common.custom"](), }, - { value: 24, label: "24" }, - { value: 30, label: "30" }, - { value: 60, label: "60" }, - { value: 120, label: "120" }, - { value: 144, label: "144" }, - { value: 240, label: "240" }, + { value: "24", label: "24" }, + { value: "30", label: "30" }, + { value: "60", label: "60" }, + { value: "120", label: "120" }, + { value: "144", label: "144" }, + { value: "240", label: "240" }, ], hasCustomInput: true, customInputKey: "customFps", @@ -461,12 +460,12 @@ export class VertdConverter extends Converter { value: "custom", label: m["convert.settings.common.custom"](), }, - { value: 1000, label: "1000 kbps" }, - { value: 2500, label: "2500 kbps" }, - { value: 5000, label: "5000 kbps" }, - { value: 8000, label: "8000 kbps" }, - { value: 12000, label: "12000 kbps" }, - { value: 18000, label: "18000 kbps" }, + { value: "1000", label: "1000 kbps" }, + { value: "2500", label: "2500 kbps" }, + { value: "5000", label: "5000 kbps" }, + { value: "8000", label: "8000 kbps" }, + { value: "12000", label: "12000 kbps" }, + { value: "18000", label: "18000 kbps" }, ], hasCustomInput: true, customInputKey: "customBitrate", @@ -482,7 +481,7 @@ export class VertdConverter extends Converter { type: "select", default: "auto", options: CONVERSION_BITRATES.map((b) => ({ - value: b, + value: b.toString(), label: b === "auto" ? m["convert.settings.common.auto"]() @@ -501,7 +500,7 @@ export class VertdConverter extends Converter { type: "select", default: "auto", options: SAMPLE_RATES.map((r) => ({ - value: r, + value: r.toString(), label: r === "auto" ? m["convert.settings.common.auto"]() @@ -559,6 +558,11 @@ export class VertdConverter extends Converter { if (to.startsWith(".")) to = to.slice(1); let fileUpload = input; + // TODO: replace input of "custom" options with actual (bitrate = customBitrate) before sending to vertd + const conversionSettings = // vertd expects object not string json + Object.keys(settings).length > 0 + ? settings // user-provided settings + : await this.getDefaultSettings(); // use defaults if not provided // if converting animated webp to video, first convert to gif // ffmpeg (in vertd) doesn't support decoding animated webp still.. while supporting encoding animated webp for some reason @@ -573,7 +577,7 @@ export class VertdConverter extends Converter { fileUpload = await magickConverter.convert( input, ".gif", - settings, + conversionSettings, 100, ); this.log(`successfully converted webp to gif`); @@ -614,15 +618,6 @@ export class VertdConverter extends Converter { }); ws.onopen = () => { - let speed = settings.vertdSpeed as ConversionSpeed | undefined; - const sliderIndex = settings.vertdSpeedSlider as - | number - | undefined; - if (sliderIndex !== undefined) { - speed = vertdSpeedValues[sliderIndex] || speed; - } - if (!speed) speed = Settings.instance.settings.vertdSpeed; - const keepMetadata = Settings.instance.settings.metadata; this.log( `opened ws connection to vertd for file ${input.name}`, ); @@ -632,11 +627,11 @@ export class VertdConverter extends Converter { jobId: uploadRes.id, token: uploadRes.auth, to, - speed, - keepMetadata, + settings: conversionSettings, }, }; ws.send(JSON.stringify(msg)); + this.log(JSON.stringify(msg, null, 2)); this.log(`sent startJob message for file ${input.name}`); }; diff --git a/src/lib/sections/settings/index.svelte.ts b/src/lib/sections/settings/index.svelte.ts index 549501df..ab9c9422 100644 --- a/src/lib/sections/settings/index.svelte.ts +++ b/src/lib/sections/settings/index.svelte.ts @@ -50,7 +50,7 @@ export class Settings { magickQuality: 100, ffmpegQuality: "auto", ffmpegSampleRate: "auto", - ffmpegCustomSampleRate: 44100, + ffmpegCustomSampleRate: 44100, //TODO: make string to match for vertd vertdBlockedHashes: new Map(), }); From 7f888e3f43acf97263de6385b408fcf51e0a7bfc Mon Sep 17 00:00:00 2001 From: not-nullptr Date: Thu, 26 Feb 2026 11:34:19 +0000 Subject: [PATCH 039/118] feat: preliminary mediabunny (sorry maya) --- src/lib/converters/mediabunny.svelte.ts | 98 +++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/lib/converters/mediabunny.svelte.ts diff --git a/src/lib/converters/mediabunny.svelte.ts b/src/lib/converters/mediabunny.svelte.ts new file mode 100644 index 00000000..fbd7654f --- /dev/null +++ b/src/lib/converters/mediabunny.svelte.ts @@ -0,0 +1,98 @@ +import { VertFile } from "$lib/types"; +import { + ALL_FORMATS, + BufferTarget, + Conversion, + Input, + MkvOutputFormat, + MovOutputFormat, + Mp4InputFormat, + Mp4OutputFormat, + Output, + ReadableStreamSource, + WebMInputFormat, + WebMOutputFormat, +} from "mediabunny"; +import { Converter, FormatInfo, type WorkerStatus } from "./converter.svelte"; +import { ToastManager } from "$lib/util/toast.svelte"; + +export class MediabunnyConverter extends Converter { + public name = "mediabunny"; + public status: WorkerStatus = $state("ready"); + + public supportedFormats: FormatInfo[] = [ + new FormatInfo("mp4", true, true), + new FormatInfo("mkv", false, true), + new FormatInfo("webm", true, true), + new FormatInfo("mov", false, true), + ]; + + constructor() { + super(); + } + + public async convert(file: VertFile, to: string): Promise { + const stream = file.file.stream(); // ReadableStream> + const input = new Input({ + formats: [new Mp4InputFormat(), new WebMInputFormat()], + source: new ReadableStreamSource(stream), + }); + + const toFormat = to.startsWith(".") ? to.slice(1) : to; + const originalName = file.file.name.split(".").slice(0, -1).join("."); + + const output = new Output({ + format: this.format(to), + target: new BufferTarget(), + }); + + const conversion = await Conversion.init({ + input, + output, + }); + + if (!conversion.isValid) { + for (const discarded of conversion.discardedTracks) { + ToastManager.add({ + type: "error", + message: `Mediabunny discarded unsupported track: ${discarded.reason}`, + }); + } + + throw new Error(`Mediabunny conversion not valid`); + } + + await conversion.execute(); + + if (!output.target.buffer) { + throw new Error("Mediabunny conversion failed: no output buffer"); + } + + const f = new File( + [output.target.buffer], + `${originalName}.${toFormat}`, + { + type: "application/octet-stream", + }, + ); + + return new VertFile(f, toFormat); + } + + private format(ext: string) { + switch (ext) { + case ".mp4": + return new Mp4OutputFormat(); + case ".mkv": + return new MkvOutputFormat(); + case ".webm": + return new WebMOutputFormat(); + case ".mov": + return new MovOutputFormat(); + default: + throw new Error(`Unsupported format: ${ext}`); + } + } + + public async cancel(input: VertFile): Promise {} +} From 3c5a01f0b26ee10578bf14237b9d2064a14a5000 Mon Sep 17 00:00:00 2001 From: Maya Date: Fri, 27 Feb 2026 13:08:26 +0300 Subject: [PATCH 040/118] fix: vertd custom settings --- src/lib/converters/vertd.svelte.ts | 36 ++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/lib/converters/vertd.svelte.ts b/src/lib/converters/vertd.svelte.ts index 8ca9ca80..1ed62625 100644 --- a/src/lib/converters/vertd.svelte.ts +++ b/src/lib/converters/vertd.svelte.ts @@ -183,6 +183,35 @@ const progressEstimate = ( return progress * progressEstimates[type] + previousValues; }; +const processSettings = ( + settings: ConversionSettings, +): ConversionSettings => { + const newSettings = { ...settings }; + + if (newSettings.fps === "custom") { + newSettings.fps = newSettings.customFps; + delete newSettings.customFps; + } + if (newSettings.resolution === "custom") { + newSettings.resolution = newSettings.customResolution; + delete newSettings.customResolution; + } + if (newSettings.videoBitrate === "custom") { + newSettings.videoBitrate = newSettings.customVideoBitrate; + delete newSettings.customVideoBitrate; + } + if (newSettings.audioBitrate === "custom") { + newSettings.audioBitrate = newSettings.customAudioBitrate; + delete newSettings.customAudioBitrate; + } + if (newSettings.sampleRate === "custom") { + newSettings.sampleRate = newSettings.customSampleRate; + delete newSettings.customSampleRate; + } + + return newSettings; +}; + const uploadFile = async (file: VertFile): Promise => { const apiUrl = await VertdInstance.instance.url(); const formData = new FormData(); @@ -468,7 +497,7 @@ export class VertdConverter extends Converter { { value: "18000", label: "18000 kbps" }, ], hasCustomInput: true, - customInputKey: "customBitrate", + customInputKey: "customVideoBitrate", placeholder: m["convert.settings.video.bitrate_placeholder"](), }; @@ -490,7 +519,7 @@ export class VertdConverter extends Converter { : `${b} kbps`, })), hasCustomInput: true, - customInputKey: "customBitrate", + customInputKey: "customAudioBitrate", placeholder: m["convert.settings.audio.bitrate_placeholder"](), }; @@ -558,7 +587,6 @@ export class VertdConverter extends Converter { if (to.startsWith(".")) to = to.slice(1); let fileUpload = input; - // TODO: replace input of "custom" options with actual (bitrate = customBitrate) before sending to vertd const conversionSettings = // vertd expects object not string json Object.keys(settings).length > 0 ? settings // user-provided settings @@ -627,7 +655,7 @@ export class VertdConverter extends Converter { jobId: uploadRes.id, token: uploadRes.auth, to, - settings: conversionSettings, + settings: processSettings(conversionSettings), }, }; ws.send(JSON.stringify(msg)); From b35c863749f991c6502e992e8e8078ad8d0c42a9 Mon Sep 17 00:00:00 2001 From: Maya Date: Fri, 27 Feb 2026 14:10:12 +0300 Subject: [PATCH 041/118] fix: default vertd speed slider --- src/lib/converters/vertd.svelte.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/lib/converters/vertd.svelte.ts b/src/lib/converters/vertd.svelte.ts index 1ed62625..95e575e8 100644 --- a/src/lib/converters/vertd.svelte.ts +++ b/src/lib/converters/vertd.svelte.ts @@ -183,9 +183,7 @@ const progressEstimate = ( return progress * progressEstimates[type] + previousValues; }; -const processSettings = ( - settings: ConversionSettings, -): ConversionSettings => { +const processSettings = (settings: ConversionSettings): ConversionSettings => { const newSettings = { ...settings }; if (newSettings.fps === "custom") { @@ -413,6 +411,9 @@ export class VertdConverter extends Converter { }, ]; + // get default vertd speed + const defaultSpeed = Settings.instance.settings.vertdSpeed; + const defaultSpeedIndex = vertdSpeedValues.indexOf(defaultSpeed); const qualitySpeedRange: SettingDefinition = { key: "vertdSpeedSlider", label: m["convert.settings.video.speed"](), @@ -421,7 +422,7 @@ export class VertdConverter extends Converter { min: 0, max: qualityOptions.length - 1, step: 1, - default: 3, + default: defaultSpeedIndex !== -1 ? defaultSpeedIndex : 3, options: qualityOptions.map((option, index) => ({ value: index, label: option.label, From 9e58c693a0c640510dbd4ea714e048fba721d19f Mon Sep 17 00:00:00 2001 From: Maya Date: Fri, 27 Feb 2026 21:49:56 +0300 Subject: [PATCH 042/118] feat: mediabunny new formats, fix bugs --- bun.lock | 16 ++++++++--- package.json | 1 + src/lib/converters/index.ts | 7 +++-- src/lib/converters/mediabunny.svelte.ts | 35 ++++++++++++++++++------- 4 files changed, 44 insertions(+), 15 deletions(-) diff --git a/bun.lock b/bun.lock index 355ea6b9..fbdab173 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "vert", @@ -17,7 +18,8 @@ "clsx": "^2.1.1", "fflate": "^0.8.2", "lucide-svelte": "^0.554.0", - "music-metadata": "^11.10.1", + "mediabunny": "^1.35.1", + "music-metadata": "^11.10.3", "overlayscrollbars": "^2.12.0", "overlayscrollbars-svelte": "^0.5.5", "p-queue": "^9.0.1", @@ -31,7 +33,7 @@ "@inlang/paraglide-js": "^2.5.0", "@poppanator/sveltekit-svg": "^5.0.1", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.48.5", + "@sveltejs/kit": "^2.49.0", "@sveltejs/vite-plugin-svelte": "^4.0.4", "@types/eslint": "^9.6.1", "@types/sanitize-html": "^2.16.0", @@ -44,8 +46,8 @@ "prettier": "^3.6.2", "prettier-plugin-svelte": "^3.4.0", "prettier-plugin-tailwindcss": "^0.6.14", - "sass": "^1.94.1", - "svelte": "^5.43.12", + "sass": "^1.94.2", + "svelte": "^5.43.14", "svelte-check": "^4.3.4", "tailwindcss": "^3.4.18", "typescript": "^5.9.3", @@ -314,6 +316,10 @@ "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], + "@types/dom-mediacapture-transform": ["@types/dom-mediacapture-transform@0.1.11", "", { "dependencies": { "@types/dom-webcodecs": "*" } }, "sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ=="], + + "@types/dom-webcodecs": ["@types/dom-webcodecs@0.1.13", "", {}, "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ=="], + "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], @@ -628,6 +634,8 @@ "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + "mediabunny": ["mediabunny@1.35.1", "", { "dependencies": { "@types/dom-mediacapture-transform": "^0.1.11", "@types/dom-webcodecs": "0.1.13" } }, "sha512-VrprpjkLTZyIyhzBAc9D3HqgXarAE+le7+6x0Sdu9WN2SD86L8bUy0hz06Xwf14dVPqS7OwpY2KOhlUyqmI2eQ=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], diff --git a/package.json b/package.json index 46988826..46e573e9 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "clsx": "^2.1.1", "fflate": "^0.8.2", "lucide-svelte": "^0.554.0", + "mediabunny": "^1.35.1", "music-metadata": "^11.10.3", "overlayscrollbars": "^2.12.0", "overlayscrollbars-svelte": "^0.5.5", diff --git a/src/lib/converters/index.ts b/src/lib/converters/index.ts index 11089ae9..82c4a7b4 100644 --- a/src/lib/converters/index.ts +++ b/src/lib/converters/index.ts @@ -5,6 +5,7 @@ import { PandocConverter } from "./pandoc.svelte"; import { VertdConverter } from "./vertd.svelte"; import { MagickConverter } from "./magick.svelte"; import { DISABLE_ALL_EXTERNAL_REQUESTS } from "$lib/util/consts"; +import { MediabunnyConverter } from "./mediabunny.svelte"; const getConverters = (): Converter[] => { const converters: Converter[] = [ @@ -12,10 +13,12 @@ const getConverters = (): Converter[] => { new FFmpegConverter(), ]; - if (!DISABLE_ALL_EXTERNAL_REQUESTS) { + if (DISABLE_ALL_EXTERNAL_REQUESTS) { converters.push(new VertdConverter()); } + converters.push(new MediabunnyConverter()); + converters.push(new PandocConverter()); return converters; }; @@ -45,7 +48,7 @@ categories.audio.formats = .map((f) => f.name) || []; categories.video.formats = converters - .find((c) => c.name === "vertd") + .find((c) => c.name === "mediabunny") ?.supportedFormats.filter((f) => f.toSupported && f.isNative) .map((f) => f.name) || []; categories.image.formats = diff --git a/src/lib/converters/mediabunny.svelte.ts b/src/lib/converters/mediabunny.svelte.ts index fbd7654f..006e0933 100644 --- a/src/lib/converters/mediabunny.svelte.ts +++ b/src/lib/converters/mediabunny.svelte.ts @@ -1,16 +1,19 @@ import { VertFile } from "$lib/types"; import { - ALL_FORMATS, + BlobSource, BufferTarget, Conversion, Input, + MATROSKA, MkvOutputFormat, MovOutputFormat, - Mp4InputFormat, + MP4, Mp4OutputFormat, + MPEG_TS, + MpegTsOutputFormat, Output, - ReadableStreamSource, - WebMInputFormat, + QTFF, + WEBM, WebMOutputFormat, } from "mediabunny"; import { Converter, FormatInfo, type WorkerStatus } from "./converter.svelte"; @@ -22,9 +25,16 @@ export class MediabunnyConverter extends Converter { public supportedFormats: FormatInfo[] = [ new FormatInfo("mp4", true, true), - new FormatInfo("mkv", false, true), + new FormatInfo("m4v", true, true), + new FormatInfo("mkv", true, true), new FormatInfo("webm", true, true), - new FormatInfo("mov", false, true), + new FormatInfo("mov", true, true), + + // mp4-based formats (should work) + new FormatInfo("f4v", true, true), + new FormatInfo("3gp", true, true), + new FormatInfo("3g2", true, true), + new FormatInfo("ts", true, true), ]; constructor() { @@ -32,10 +42,10 @@ export class MediabunnyConverter extends Converter { } public async convert(file: VertFile, to: string): Promise { - const stream = file.file.stream(); // ReadableStream> const input = new Input({ - formats: [new Mp4InputFormat(), new WebMInputFormat()], - source: new ReadableStreamSource(stream), + // TODO: add settings & special handling for certain formats & codecs + formats: [MP4, QTFF, MATROSKA, WEBM, MPEG_TS], + source: new BlobSource(file.file) }); const toFormat = to.startsWith(".") ? to.slice(1) : to; @@ -81,7 +91,12 @@ export class MediabunnyConverter extends Converter { private format(ext: string) { switch (ext) { + // i'm seeing this "ISMV" format from microsoft, so maybe? case ".mp4": + case ".m4v": + case ".f4v": + case ".3gp": + case ".3g2": return new Mp4OutputFormat(); case ".mkv": return new MkvOutputFormat(); @@ -89,6 +104,8 @@ export class MediabunnyConverter extends Converter { return new WebMOutputFormat(); case ".mov": return new MovOutputFormat(); + case ".ts": + return new MpegTsOutputFormat(); default: throw new Error(`Unsupported format: ${ext}`); } From ffb6ef856b3d8303a033cf0c6b4f8bdd3021d20c Mon Sep 17 00:00:00 2001 From: Maya Date: Sat, 28 Feb 2026 00:06:16 +0300 Subject: [PATCH 043/118] feat: mediabunny progress and cancel --- messages/en.json | 2 +- src/lib/converters/mediabunny.svelte.ts | 36 +++++++++++++++++++++---- src/routes/+page.svelte | 20 +++++++++++--- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/messages/en.json b/messages/en.json index ff2d4065..7e331ff7 100644 --- a/messages/en.json +++ b/messages/en.json @@ -26,7 +26,7 @@ "audio": "Audio", "documents": "Documents", "video": "Video", - "video_server_processing": "Server supported", + "video_server_processing": "Local & server supported", "local_supported": "Local supported", "status": { "text": "Status: {status}", diff --git a/src/lib/converters/mediabunny.svelte.ts b/src/lib/converters/mediabunny.svelte.ts index 006e0933..a6fffb0e 100644 --- a/src/lib/converters/mediabunny.svelte.ts +++ b/src/lib/converters/mediabunny.svelte.ts @@ -18,10 +18,14 @@ import { } from "mediabunny"; import { Converter, FormatInfo, type WorkerStatus } from "./converter.svelte"; import { ToastManager } from "$lib/util/toast.svelte"; +import { error, log } from "$lib/util/logger"; export class MediabunnyConverter extends Converter { public name = "mediabunny"; public status: WorkerStatus = $state("ready"); + public reportsProgress: boolean = true; + + private activeConversions = new Map(); public supportedFormats: FormatInfo[] = [ new FormatInfo("mp4", true, true), @@ -45,12 +49,9 @@ export class MediabunnyConverter extends Converter { const input = new Input({ // TODO: add settings & special handling for certain formats & codecs formats: [MP4, QTFF, MATROSKA, WEBM, MPEG_TS], - source: new BlobSource(file.file) + source: new BlobSource(file.file), }); - const toFormat = to.startsWith(".") ? to.slice(1) : to; - const originalName = file.file.name.split(".").slice(0, -1).join("."); - const output = new Output({ format: this.format(to), target: new BufferTarget(), @@ -72,12 +73,20 @@ export class MediabunnyConverter extends Converter { throw new Error(`Mediabunny conversion not valid`); } + conversion.onProgress = (progress) => { + file.progress = progress * 100; + }; + + this.activeConversions.set(file.id, conversion); await conversion.execute(); + this.activeConversions.delete(file.id); if (!output.target.buffer) { throw new Error("Mediabunny conversion failed: no output buffer"); } + const toFormat = to.startsWith(".") ? to.slice(1) : to; + const originalName = file.file.name.split(".").slice(0, -1).join("."); const f = new File( [output.target.buffer], `${originalName}.${toFormat}`, @@ -111,5 +120,22 @@ export class MediabunnyConverter extends Converter { } } - public async cancel(input: VertFile): Promise {} + public async cancel(input: VertFile): Promise { + const conversion = this.activeConversions.get(input.id); + if (!conversion) { + error( + ["converters", this.name], + `no active conversion found for file ${input.name}`, + ); + return; + } + + log( + ["converters", this.name], + `cancelling conversion for file ${input.name}`, + ); + + conversion.cancel(); + this.activeConversions.delete(input.id); + } } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index c063d641..954ad0df 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -66,10 +66,20 @@ }; if (!DISABLE_ALL_EXTERNAL_REQUESTS) { + const formats = Array.from( + new Set([ + ...getSupportedFormats("vertd").split(", "), + ...getSupportedFormats("mediabunny").split(", "), + ]), + ) + .filter((f) => f !== "none") + .join(", "); + output.Video = { - formats: getSupportedFormats("vertd"), + formats, icon: Film, title: m["upload.cards.video"](), + // TODO: add "partial" state? somehow figure out diff between vertd and mediabunny status: $vertdLoaded === true ? "ready" : "not-ready", // not using converter.status for this }; } @@ -231,9 +241,11 @@

{/if}

- {@html sanitize(m["upload.cards.status.text"]({ - status: getStatusText(s.status), - }))} + {@html sanitize( + m["upload.cards.status.text"]({ + status: getStatusText(s.status), + }), + )}

Date: Sun, 1 Mar 2026 12:30:36 +0300 Subject: [PATCH 044/118] feat: additional coders --- bun.lock | 6 ++++++ package.json | 2 ++ src/lib/converters/mediabunny.svelte.ts | 18 +++++++++++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/bun.lock b/bun.lock index fbdab173..75b8e91c 100644 --- a/bun.lock +++ b/bun.lock @@ -12,6 +12,8 @@ "@fontsource/lexend": "^5.2.11", "@fontsource/radio-canada-big": "^5.2.7", "@imagemagick/magick-wasm": "^0.0.37", + "@mediabunny/ac3": "^1.35.1", + "@mediabunny/mp3-encoder": "^1.35.1", "@stripe/stripe-js": "^8.5.2", "byte-data": "^19.0.1", "client-zip": "^2.5.0", @@ -176,6 +178,10 @@ "@lix-js/server-protocol-schema": ["@lix-js/server-protocol-schema@0.1.1", "", {}, "sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ=="], + "@mediabunny/ac3": ["@mediabunny/ac3@1.35.1", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-gLx3mFfs58/cdz2/f5Fp+6ZOrX5Jli3AZMXw/5EJcgm2VpnC/2oxtJyP1x/00PIS4UCE770slwIdz7U+2CQ31g=="], + + "@mediabunny/mp3-encoder": ["@mediabunny/mp3-encoder@1.35.1", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-iY6FcPs7GbHMs/ASPmdzwojKcBN4AfMa+zFh4KNZNaLToyR7aEZILj9FsPVJA11bshaoo80dTaBcn69i33JHVA=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], diff --git a/package.json b/package.json index 46e573e9..3999768c 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,8 @@ "@fontsource/lexend": "^5.2.11", "@fontsource/radio-canada-big": "^5.2.7", "@imagemagick/magick-wasm": "^0.0.37", + "@mediabunny/ac3": "^1.35.1", + "@mediabunny/mp3-encoder": "^1.35.1", "@stripe/stripe-js": "^8.5.2", "byte-data": "^19.0.1", "client-zip": "^2.5.0", diff --git a/src/lib/converters/mediabunny.svelte.ts b/src/lib/converters/mediabunny.svelte.ts index a6fffb0e..fb2680d6 100644 --- a/src/lib/converters/mediabunny.svelte.ts +++ b/src/lib/converters/mediabunny.svelte.ts @@ -2,6 +2,7 @@ import { VertFile } from "$lib/types"; import { BlobSource, BufferTarget, + canEncodeAudio, Conversion, Input, MATROSKA, @@ -16,6 +17,8 @@ import { WEBM, WebMOutputFormat, } from "mediabunny"; +import { registerMp3Encoder } from "@mediabunny/mp3-encoder"; +import { registerAc3Decoder, registerAc3Encoder } from "@mediabunny/ac3"; import { Converter, FormatInfo, type WorkerStatus } from "./converter.svelte"; import { ToastManager } from "$lib/util/toast.svelte"; import { error, log } from "$lib/util/logger"; @@ -43,6 +46,19 @@ export class MediabunnyConverter extends Converter { constructor() { super(); + + // additional mediabunny coders + // currently both official ones -- maybe add our own in the future + this.initializeCodecs(); + } + + private async initializeCodecs(): Promise { + if (!(await canEncodeAudio("mp3"))) { + // Only register the custom encoder if there's no native support + registerMp3Encoder(); + } + registerAc3Decoder(); + registerAc3Encoder(); } public async convert(file: VertFile, to: string): Promise { @@ -114,7 +130,7 @@ export class MediabunnyConverter extends Converter { case ".mov": return new MovOutputFormat(); case ".ts": - return new MpegTsOutputFormat(); + return new MpegTsOutputFormat(); // FIXME: audio tracks discarded - prob needs another audio codec default: throw new Error(`Unsupported format: ${ext}`); } From 1aac50a649549b1add289758f90ffdf7bded103e Mon Sep 17 00:00:00 2001 From: Maya Date: Sun, 1 Mar 2026 12:52:57 +0300 Subject: [PATCH 045/118] feat: codec compatibility object will use later --- src/lib/converters/mediabunny.svelte.ts | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/lib/converters/mediabunny.svelte.ts b/src/lib/converters/mediabunny.svelte.ts index fb2680d6..22c610ea 100644 --- a/src/lib/converters/mediabunny.svelte.ts +++ b/src/lib/converters/mediabunny.svelte.ts @@ -23,6 +23,33 @@ import { Converter, FormatInfo, type WorkerStatus } from "./converter.svelte"; import { ToastManager } from "$lib/util/toast.svelte"; import { error, log } from "$lib/util/logger"; +// codec compatibility object, based on docs +// https://mediabunny.dev/guide/supported-formats-and-codecs#compatibility-table +const codecCompatibility = { + video: { + mp4: ['avc', 'hevc', 'vp8', 'vp9', 'av1'], + m4v: ['avc', 'hevc', 'vp8', 'vp9', 'av1'], + f4v: ['avc', 'hevc', 'vp8', 'vp9', 'av1'], + '3gp': ['avc', 'hevc', 'vp8', 'vp9', 'av1'], + '3g2': ['avc', 'hevc', 'vp8', 'vp9', 'av1'], + mkv: ['avc', 'hevc', 'vp8', 'vp9', 'av1'], + webm: ['vp8', 'vp9', 'av1'], + mov: ['avc', 'hevc', 'vp8', 'vp9', 'av1'], + ts: ['avc', 'hevc'], + }, + audio: { + mp4: ['aac', 'opus', 'mp3', 'vorbis', 'flac', 'ac3', 'eac3', 'pcm-s16', 'pcm-s16be', 'pcm-s24', 'pcm-s24be', 'pcm-s32', 'pcm-s32be', 'pcm-f32', 'pcm-f64'], + m4v: ['aac', 'opus', 'mp3', 'vorbis', 'flac', 'ac3', 'eac3', 'pcm-s16', 'pcm-s16be', 'pcm-s24', 'pcm-s24be', 'pcm-s32', 'pcm-s32be', 'pcm-f32', 'pcm-f64'], + f4v: ['aac', 'opus', 'mp3', 'vorbis', 'flac', 'ac3', 'eac3', 'pcm-s16', 'pcm-s16be', 'pcm-s24', 'pcm-s24be', 'pcm-s32', 'pcm-s32be', 'pcm-f32', 'pcm-f64'], + '3gp': ['aac', 'opus', 'mp3', 'vorbis', 'flac', 'ac3', 'eac3', 'pcm-s16', 'pcm-s16be', 'pcm-s24', 'pcm-s24be', 'pcm-s32', 'pcm-s32be', 'pcm-f32', 'pcm-f64'], + '3g2': ['aac', 'opus', 'mp3', 'vorbis', 'flac', 'ac3', 'eac3', 'pcm-s16', 'pcm-s16be', 'pcm-s24', 'pcm-s24be', 'pcm-s32', 'pcm-s32be', 'pcm-f32', 'pcm-f64'], + mkv: ['aac', 'opus', 'mp3', 'vorbis', 'flac', 'ac3', 'eac3', 'pcm-u8', 'pcm-s16', 'pcm-s24', 'pcm-s32', 'pcm-f32', 'pcm-f64'], + webm: ['opus', 'vorbis'], + mov: ['aac', 'opus', 'mp3', 'vorbis', 'flac', 'ac3', 'eac3', 'pcm-u8', 'pcm-s8', 'pcm-s16', 'pcm-s16be', 'pcm-s24', 'pcm-s24be', 'pcm-s32', 'pcm-s32be', 'pcm-f32', 'pcm-f32be', 'pcm-f64', 'ulaw', 'alaw'], + ts: ['aac', 'mp3', 'ac3', 'eac3'], + }, +} as const; + export class MediabunnyConverter extends Converter { public name = "mediabunny"; public status: WorkerStatus = $state("ready"); From 96da202e6cc3a4c30f4c4827ed0f174fa98d2158 Mon Sep 17 00:00:00 2001 From: Maya Date: Fri, 6 Mar 2026 12:00:43 +0300 Subject: [PATCH 046/118] feat: use & rework findConverters() adds a priority to FormatInfo which ensures that we can have certain formats prioritize certain converters (mp4 supports mediabunny/local so use that, then fallback to next if fails) --- bun.lock | 9 +- eslint.config.js | 38 ++-- package.json | 5 +- .../functional/FormatDropdown.svelte | 5 +- .../functional/SettingsModal.svelte | 11 +- src/lib/converters/converter.svelte.ts | 1 + src/lib/converters/ffmpeg.svelte.ts | 2 +- src/lib/converters/index.ts | 9 +- src/lib/converters/mediabunny.svelte.ts | 181 +++++++++++++++--- src/lib/converters/vertd.svelte.ts | 2 +- src/lib/types/file.svelte.ts | 93 +++++---- src/routes/convert/+page.svelte | 6 +- 12 files changed, 260 insertions(+), 102 deletions(-) diff --git a/bun.lock b/bun.lock index 4337d87b..c2873514 100644 --- a/bun.lock +++ b/bun.lock @@ -12,17 +12,18 @@ "@fontsource/lexend": "^5.2.11", "@fontsource/radio-canada-big": "^5.2.7", "@imagemagick/magick-wasm": "^0.0.37", - "@stripe/stripe-js": "^8.7.0", "@mediabunny/ac3": "^1.35.1", + "@mediabunny/flac-encoder": "^1.37.0", "@mediabunny/mp3-encoder": "^1.35.1", + "@stripe/stripe-js": "^8.7.0", "byte-data": "^19.0.1", "client-zip": "^2.5.0", "clsx": "^2.1.1", "fflate": "^0.8.2", "lucide-svelte": "^0.554.0", + "mediabunny": "^1.37.0", "music-metadata": "^11.12.0", "overlayscrollbars": "^2.14.0", - "mediabunny": "^1.35.1", "overlayscrollbars-svelte": "^0.5.5", "p-queue": "^9.1.0", "riff-file": "^1.0.3", @@ -180,6 +181,8 @@ "@mediabunny/ac3": ["@mediabunny/ac3@1.35.1", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-gLx3mFfs58/cdz2/f5Fp+6ZOrX5Jli3AZMXw/5EJcgm2VpnC/2oxtJyP1x/00PIS4UCE770slwIdz7U+2CQ31g=="], + "@mediabunny/flac-encoder": ["@mediabunny/flac-encoder@1.37.0", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-VwKIL5p1WZE4dSwZ1SVv/bd2ksul8a4run4S1eEbPRysnG87nmCXddO5ajD3b2k2478XWitKnVDXl/kxdIIWBw=="], + "@mediabunny/mp3-encoder": ["@mediabunny/mp3-encoder@1.35.1", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-iY6FcPs7GbHMs/ASPmdzwojKcBN4AfMa+zFh4KNZNaLToyR7aEZILj9FsPVJA11bshaoo80dTaBcn69i33JHVA=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -640,7 +643,7 @@ "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - "mediabunny": ["mediabunny@1.35.1", "", { "dependencies": { "@types/dom-mediacapture-transform": "^0.1.11", "@types/dom-webcodecs": "0.1.13" } }, "sha512-VrprpjkLTZyIyhzBAc9D3HqgXarAE+le7+6x0Sdu9WN2SD86L8bUy0hz06Xwf14dVPqS7OwpY2KOhlUyqmI2eQ=="], + "mediabunny": ["mediabunny@1.37.0", "", { "dependencies": { "@types/dom-mediacapture-transform": "^0.1.11", "@types/dom-webcodecs": "0.1.13" } }, "sha512-eV7M9IJ29pr/8RNL1sYtIxNbdMfDMN1hMwMaOFfNLhwuKKGSC+eKwiJFpdVjEJ3zrMA4LGerF4Hps0SENFSAlg=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], diff --git a/eslint.config.js b/eslint.config.js index a5265658..b45523bf 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,33 +1,39 @@ -import prettier from 'eslint-config-prettier'; -import js from '@eslint/js'; -import svelte from 'eslint-plugin-svelte'; -import globals from 'globals'; -import ts from 'typescript-eslint'; +import prettier from "eslint-config-prettier"; +import js from "@eslint/js"; +import svelte from "eslint-plugin-svelte"; +import globals from "globals"; +import ts from "typescript-eslint"; export default ts.config( js.configs.recommended, ...ts.configs.recommended, - ...svelte.configs['flat/recommended'], + ...svelte.configs["flat/recommended"], prettier, - ...svelte.configs['flat/prettier'], + ...svelte.configs["flat/prettier"], { languageOptions: { globals: { ...globals.browser, - ...globals.node - } - } + ...globals.node, + }, + }, }, { - files: ['**/*.svelte'], + files: ["**/*.svelte"], languageOptions: { parserOptions: { - parser: ts.parser - } - } + parser: ts.parser, + }, + }, }, { - ignores: ['build/', '.svelte-kit/', 'dist/'] - } + ignores: ["build/", ".svelte-kit/", "dist/"], + }, + { + files: ["**/*.ts", "**/*.svelte.ts"], + rules: { + "no-at-html-tags": "off", + }, + }, ); diff --git a/package.json b/package.json index 7f537460..3d264a17 100644 --- a/package.json +++ b/package.json @@ -45,18 +45,19 @@ "@fontsource/lexend": "^5.2.11", "@fontsource/radio-canada-big": "^5.2.7", "@imagemagick/magick-wasm": "^0.0.37", - "@stripe/stripe-js": "^8.7.0", "@mediabunny/ac3": "^1.35.1", + "@mediabunny/flac-encoder": "^1.37.0", "@mediabunny/mp3-encoder": "^1.35.1", + "@stripe/stripe-js": "^8.7.0", "byte-data": "^19.0.1", "client-zip": "^2.5.0", "clsx": "^2.1.1", "fflate": "^0.8.2", "lucide-svelte": "^0.554.0", + "mediabunny": "^1.37.0", "music-metadata": "^11.12.0", "overlayscrollbars": "^2.14.0", "overlayscrollbars-svelte": "^0.5.5", - "mediabunny": "^1.35.1", "p-queue": "^9.1.0", "riff-file": "^1.0.3", "sanitize-html": "^2.17.0", diff --git a/src/lib/components/functional/FormatDropdown.svelte b/src/lib/components/functional/FormatDropdown.svelte index 99f11ce6..990d2504 100644 --- a/src/lib/components/functional/FormatDropdown.svelte +++ b/src/lib/components/functional/FormatDropdown.svelte @@ -72,9 +72,10 @@ // decide which converters to use to detect category: // - if file provided, prefer its primary converter -- individual file dropdown // - if no file provided, use all converters from all files -- "set all to" dropdown + const primaryConverter = file ? (file.isZip() ? file.converters[0] : file.findConverters()[0]) : null; const convertersToCheck = file - ? file.findConverter() - ? [file.findConverter()!] + ? primaryConverter + ? [primaryConverter] : file.converters : files.files.flatMap((f) => f.converters); diff --git a/src/lib/components/functional/SettingsModal.svelte b/src/lib/components/functional/SettingsModal.svelte index dfc32a6e..36a31744 100644 --- a/src/lib/components/functional/SettingsModal.svelte +++ b/src/lib/components/functional/SettingsModal.svelte @@ -1,4 +1,5 @@ {#snippet fileItem(file: VertFile, index: number)} - {@const currentConverter = file.findConverter()} + {@const currentConverter = file.isZip() ? file.converters[0] : file.findConverters()[0]} {@const isImage = currentConverter?.name === "imagemagick"} {@const isAudio = currentConverter?.name === "ffmpeg"} {@const isVideo = currentConverter?.name === "vertd"} From 9164dc882425900059d579e802a94c6ea2f85e46 Mon Sep 17 00:00:00 2001 From: Maya Date: Fri, 6 Mar 2026 14:29:47 +0300 Subject: [PATCH 047/118] fix: formatdropdown logic rework & bug fixes --- .../functional/FormatDropdown.svelte | 328 ++++++------------ src/lib/converters/index.ts | 16 +- src/lib/types/file.svelte.ts | 15 +- 3 files changed, 133 insertions(+), 226 deletions(-) diff --git a/src/lib/components/functional/FormatDropdown.svelte b/src/lib/components/functional/FormatDropdown.svelte index 990d2504..3ba801c8 100644 --- a/src/lib/components/functional/FormatDropdown.svelte +++ b/src/lib/components/functional/FormatDropdown.svelte @@ -30,159 +30,110 @@ dropdownSize = "default", file, }: Props = $props(); + let open = $state(false); let dropdown = $state(); - let currentCategory = $state(); - let searchQuery = $state(""); let dropdownMenu: HTMLElement | undefined = $state(); - let rootCategory: string | null = null; let dropdownPosition = $state<"left" | "center" | "right">("center"); + let showSettingsModal = $state(false); + let currentCategory = $state(null); + let searchQuery = $state(""); + let rootCategory: string | null = null; - // initialize current category - $effect(() => { - if (currentCategory) return; + const normalize = (str: string) => str.replace(/^\./, "").toLowerCase(); - log( - ["dropdown", "init"], - `initializing category, file: ${file?.name}, from: ${from}`, + const shouldExclude = (format: string): boolean => + !!( + categories["audio"]?.formats.includes(from ?? "") && + format === ".gif" ); - // find the category whose formats overlap most with the converters for this file (or all files) + const getFormats = (cat: string) => + (categories[cat]?.formats ?? []).filter((f) => !shouldExclude(f)); + + const detectCategory = (): string => { + if (from) { + // find category containing the input format, if any + const match = Object.keys(categories).find((cat) => + categories[cat].formats.includes(from), + ); + if (match) return match; + } + + // else, fall back by finding the category whose formats overlap most with the converters for this file // this finds the best matching category based on the formats supported by the converters - const pickCategoryFromConverters = ( - convList: VertFile["converters"], - ) => { - let bestCategory: string | null = null; - let maxOverlap = 0; - - for (const cat of Object.keys(categories)) { - const overlapCount = categories[cat].formats.filter((fmt) => - convList.some((conv) => conv.formatStrings().includes(fmt)), - ).length; - - if (overlapCount > maxOverlap) { - maxOverlap = overlapCount; - bestCategory = cat; - } + const converters = file + ? file.findConverters() + : files.files.flatMap((f) => f.findConverters()); + + let best: string | null = null; + let maxOverlap = 0; + for (const cat of Object.keys(categories)) { + const count = categories[cat].formats.filter((fmt) => + converters.some((c) => c.formatStrings().includes(fmt)), + ).length; + if (count > maxOverlap) { + maxOverlap = count; + best = cat; } + } - return bestCategory; - }; - - // decide which converters to use to detect category: - // - if file provided, prefer its primary converter -- individual file dropdown - // - if no file provided, use all converters from all files -- "set all to" dropdown - const primaryConverter = file ? (file.isZip() ? file.converters[0] : file.findConverters()[0]) : null; - const convertersToCheck = file - ? primaryConverter - ? [primaryConverter] - : file.converters - : files.files.flatMap((f) => f.converters); + return best ?? Object.keys(categories)[0]; + }; + $effect(() => { + if (currentCategory) return; + const detected = detectCategory(); log( ["dropdown", "init"], - `checking converters:`, - convertersToCheck.map((c) => c.formatStrings()), + `root category: ${detected} (file: ${file?.name}, from: ${from})`, ); - - // if file is provided, first try to find its category by input format - let detectedCategory: string | null = null; - if (file && from) { - detectedCategory = - Object.keys(categories).find((cat) => - categories[cat].formats.includes(from), - ) || null; - log( - ["dropdown", "init"], - `detected category from input format (${from}):`, - detectedCategory, - ); - } - - // fallback to category with most converter overlap if input category not found - detectedCategory = - detectedCategory || - pickCategoryFromConverters(convertersToCheck) || - Object.keys(categories)[0]; - - log(["dropdown", "init"], `final detected category:`, detectedCategory); - - currentCategory = detectedCategory; - rootCategory = detectedCategory; + currentCategory = detected; + rootCategory = detected; }); // other available categories based on current category (e.g. converting between video and audio) const availableCategories = $derived.by(() => { if (!rootCategory) return Object.keys(categories); - let finalCategories = Object.keys(categories).filter( + let cats = Object.keys(categories).filter( (cat) => cat === rootCategory || categories[rootCategory!]?.canConvertTo?.includes(cat), ); // handle special cases - if (from === ".gif" || from === ".webp") finalCategories.push("video"); + if (from === ".gif" || from === ".webp") cats.push("video"); if (from === ".apng") { - //finalCategories.push("image"); // -- buggy, magick can't convert from or to apng properly - finalCategories = finalCategories.filter((cat) => cat !== "audio"); + //cats.push("image"); // -- buggy, magick can't convert from or to apng properly + cats = cats.filter((cat) => cat !== "audio"); } - // filter out categories that can't handle large files (due to browser/device limitations) - if (file && file.isLarge()) { - // if file is large video, disable audio conversion - if (rootCategory === "video") - finalCategories = finalCategories.filter( - (cat) => cat !== "audio", - ); - } + // large videos can't be extracted to audio (browser/device limitations) + if (file && file.isLarge() && rootCategory === "video") + cats = cats.filter((cat) => cat !== "audio"); - return finalCategories; + return cats.filter( + (cat) => (categories[cat]?.formats?.length ?? 0) > 0, + ); }); - const shouldInclude = (format: string, category: string): boolean => { - // if converting from audio to video, dont show gifs - if ( - categories["audio"]?.formats.includes(from ?? "") && - format === ".gif" - ) { - return false; - } - - return true; - }; - const filteredData = $derived.by(() => { - const normalize = (str: string) => str.replace(/^\./, "").toLowerCase(); - // if no query, return formats for current category if (!searchQuery) { - let formats = currentCategory - ? categories[currentCategory].formats.filter((format) => - shouldInclude(format, currentCategory!), - ) - : []; - - // if no formats found at all, show everything - if (formats.length === 0) { - const allCategories = Object.keys(categories); - // show formats for current category if set, otherwise all formats - const fallbackFormats = - currentCategory && allCategories.includes(currentCategory) - ? categories[currentCategory].formats - : allCategories.flatMap( - (cat) => categories[cat].formats, - ); + const formats = getFormats(currentCategory ?? ""); + // if no formats & categories for some reason, fall back and show all categories/formats + if (formats.length === 0 && availableCategories.length === 0) { log( ["dropdown", "filter"], - `no formats found for category ${currentCategory}, showing all categories and formats as fallback`, + `no formats or available categories found for file ${file?.name}, falling back to all categories and formats`, ); - return { - categories: allCategories, - formats: fallbackFormats, + categories: Object.keys(categories), + formats: categories[currentCategory ?? ""]?.formats ?? [], isFallback: true, + resolvedCategory: currentCategory, }; } @@ -190,87 +141,70 @@ categories: availableCategories, formats, isFallback: false, + resolvedCategory: currentCategory, }; } - const searchLower = normalize(searchQuery); - // find all categories that have formats matching the search query + const query = normalize(searchQuery); + const matches = (f: string) => + normalize(f).includes(query) && !shouldExclude(f); + const matchingCategories = availableCategories.filter((cat) => - categories[cat].formats.some( - (format) => - normalize(format).includes(searchLower) && - shouldInclude(format, cat), - ), + (categories[cat]?.formats ?? []).some(matches), ); + if (matchingCategories.length === 0) { return { categories: availableCategories, formats: [], isFallback: false, + resolvedCategory: currentCategory, }; } - // if current category has no matches, switch to first category that does - const currentCategoryHasMatches = - currentCategory && - matchingCategories.some((cat) => cat === currentCategory); - if (!currentCategoryHasMatches && matchingCategories.length > 0) { - const newCategory = matchingCategories[0]; - currentCategory = newCategory; - } - - // return formats only from the current category that match the search - let filteredFormats = currentCategory - ? categories[currentCategory].formats.filter( - (format) => - normalize(format).includes(searchLower) && - shouldInclude(format, currentCategory!), - ) - : []; - - // sorting exact match first, then others - filteredFormats = filteredFormats.sort((a, b) => { - const aExact = normalize(a) === searchLower; - const bExact = normalize(b) === searchLower; - if (aExact && !bExact) return -1; - if (!aExact && bExact) return 1; - return 0; - }); + // stay on current category if it matches, else move to first matched category + const resolvedCategory = + currentCategory && matchingCategories.includes(currentCategory) + ? currentCategory + : matchingCategories[0]; + + const formats = (categories[resolvedCategory ?? ""]?.formats ?? []) + .filter(matches) + .sort((a, b) => { + // exact matches first, then original order + const aExact = normalize(a) === query; + const bExact = normalize(b) === query; + if (aExact !== bExact) return aExact ? -1 : 1; + return 0; + }); + // show categories with matches, formats from within resolved category return { - categories: - matchingCategories.length > 0 - ? matchingCategories - : availableCategories, - formats: filteredFormats, + categories: matchingCategories, + formats, isFallback: false, + resolvedCategory, }; }); + $effect(() => { + if ( + filteredData.resolvedCategory && + filteredData.resolvedCategory !== currentCategory + ) + currentCategory = filteredData.resolvedCategory; + }); + const selectOption = (option: string) => { selected = option; open = false; // save user's selection to dropdownStates for this session if (file) { - dropdownStates.update((states) => { - const updated = { ...states, [file.name]: option }; - return updated; - }); - } - - // find the category of this option if it's not in the current category - if ( - currentCategory && - !categories[currentCategory].formats.includes(option) - ) { - const formatCategory = Object.keys(categories).find((cat) => - categories[cat].formats.includes(option), - ); - - if (formatCategory) { - currentCategory = formatCategory; - } + dropdownStates.update((states) => ({ + ...states, + [file.name]: option, + })); } onselect?.(option); @@ -282,39 +216,14 @@ }; const handleSearch = (event: Event) => { - const query = (event.target as HTMLInputElement).value; - searchQuery = query; - - // find which categories have matching formats & switch - if (query) { - const queryLower = query.toLowerCase(); - const categoriesWithMatches = availableCategories.filter((cat) => - categories[cat].formats.some((format) => - format.toLowerCase().includes(queryLower), - ), - ); - - if (categoriesWithMatches.length > 0) { - const currentHasMatches = - currentCategory && - categories[currentCategory].formats.some((format) => - format.toLowerCase().includes(queryLower), - ); - - if (!currentHasMatches) { - currentCategory = categoriesWithMatches[0]; - } - } - } + searchQuery = (event.target as HTMLInputElement).value; }; const onEnter = (event: KeyboardEvent) => { - if (event.key === "Enter") { - event.preventDefault(); - if (filteredData.formats.length > 0) { - selectOption(filteredData.formats[0]); - } - } + if (event.key !== "Enter") return; + event.preventDefault(); + if (filteredData.formats.length > 0) + selectOption(filteredData.formats[0]); }; const clickDropdown = () => { @@ -327,25 +236,18 @@ const viewportWidth = window.innerWidth; let dropdownWidth: number; - if (dropdownSize === "large") { - dropdownWidth = rect.width * 3.2; - } else if (dropdownSize === "default") { + if (dropdownSize === "large") dropdownWidth = rect.width * 3.2; + else if (dropdownSize === "default") dropdownWidth = rect.width * 2.5; - } else { - dropdownWidth = rect.width * 1.5; - } + else dropdownWidth = rect.width * 1.5; const centerX = rect.left + rect.width / 2; const leftEdge = centerX - dropdownWidth / 2; const rightEdge = centerX + dropdownWidth / 2; - if (leftEdge < 0) { - dropdownPosition = "left"; - } else if (rightEdge > viewportWidth) { - dropdownPosition = "right"; - } else { - dropdownPosition = "center"; - } + if (leftEdge < 0) dropdownPosition = "left"; + else if (rightEdge > viewportWidth) dropdownPosition = "right"; + else dropdownPosition = "center"; } setTimeout(() => { @@ -387,8 +289,6 @@ newFiles.forEach((f) => files.add(f)); }; - let showSettingsModal = $state(false); - const settings = () => { if (!file) return; showSettingsModal = true; @@ -396,9 +296,7 @@ onMount(() => { const handleClickOutside = (e: MouseEvent) => { - if (dropdown && !dropdown.contains(e.target as Node)) { - open = false; - } + if (dropdown && !dropdown.contains(e.target as Node)) open = false; }; const handleResize = () => { diff --git a/src/lib/converters/index.ts b/src/lib/converters/index.ts index ddaa7c76..e0b3491a 100644 --- a/src/lib/converters/index.ts +++ b/src/lib/converters/index.ts @@ -43,11 +43,17 @@ categories.audio.formats = .find((c) => c.name === "ffmpeg") ?.supportedFormats.filter((f) => f.toSupported && f.isNative) .map((f) => f.name) || []; -categories.video.formats = - converters - .find((c) => c.name === "mediabunny") - ?.supportedFormats.filter((f) => f.toSupported && f.isNative) - .map((f) => f.name) || []; +categories.video.formats = [ + ...new Set( + converters + .filter((c) => c.name === "mediabunny" || c.name === "vertd") + .flatMap((c) => + c.supportedFormats + .filter((f) => f.toSupported && f.isNative) + .map((f) => f.name), + ), + ), +]; categories.image.formats = converters .find((c) => c.name === "imagemagick") diff --git a/src/lib/types/file.svelte.ts b/src/lib/types/file.svelte.ts index af09dd5e..e4976e5b 100644 --- a/src/lib/types/file.svelte.ts +++ b/src/lib/types/file.svelte.ts @@ -8,6 +8,7 @@ import type { ConversionSettings, SettingDefinition, } from "./conversion-settings"; +import { log } from "$lib/util/logger"; const MAX_BLOB_SIZE_LIMIT = 2 * 1024 * 1024 * 1024; // 2GB @@ -98,10 +99,12 @@ export class VertFile { public supportsStreaming(): boolean { // only vertd (video/gif -> video/gif) supports streaming // rest of converters need entire file in memory, limited by ArrayBuffer limits - const converter = this.isZip() - ? this.converters[0] - : this.findConverters()[0]; - return converter?.name === "vertd"; + const availableConverters = this.isZip() + ? this.converters + : this.findConverters(); + return availableConverters.some( + (converter) => converter.name === "vertd", + ); } constructor(file: File, to: string, blobUrl?: string) { @@ -119,8 +122,8 @@ export class VertFile { this.download = this.download.bind(this); this.blobUrl = blobUrl; - console.log(`VertFile: ${this.name}`); - console.log( + log( + ["file", "init"], `findConverters: ${this.findConverters() .map((c) => c.name) .join(", ")}`, From 9b0470e4de09682f16649e2c5d03cec7781d09df Mon Sep 17 00:00:00 2001 From: Maya Date: Sun, 8 Mar 2026 13:46:32 +0300 Subject: [PATCH 048/118] feat: choose converter, clean up logic --- eslint.config.js | 4 +- messages/en.json | 3 +- .../functional/SettingsModal.svelte | 349 ++++++++++-------- src/lib/converters/index.ts | 15 +- src/lib/converters/mediabunny.svelte.ts | 152 ++++++++ src/lib/store/index.svelte.ts | 13 +- src/lib/types/file.svelte.ts | 34 +- src/routes/convert/+page.svelte | 61 ++- 8 files changed, 442 insertions(+), 189 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index b45523bf..91dafe05 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -31,9 +31,9 @@ export default ts.config( ignores: ["build/", ".svelte-kit/", "dist/"], }, { - files: ["**/*.ts", "**/*.svelte.ts"], + files: ["**/*.ts", "**/*.svelte.ts", "**/*.svelte"], rules: { - "no-at-html-tags": "off", + "svelte/no-at-html-tags": "off", }, }, ); diff --git a/messages/en.json b/messages/en.json index fdd4785e..644fb3d4 100644 --- a/messages/en.json +++ b/messages/en.json @@ -85,8 +85,9 @@ "settings": { "settings": "Settings", "title": "File conversion settings", - "description": "Change the conversion settings for {filename}, which is using {converter}. These settings may not be available for all formats. This is an early beta and may have some issues.", + "description": "Change the conversion settings for {filename} with the selected converter. These settings may not be available for all formats. This is an early beta and may have some issues.", "none": "No settings available for this format.", + "converter": "Converter", "image": { "quality": "Quality", "depth": "Color depth", diff --git a/src/lib/components/functional/SettingsModal.svelte b/src/lib/components/functional/SettingsModal.svelte index 36a31744..caf417e2 100644 --- a/src/lib/components/functional/SettingsModal.svelte +++ b/src/lib/components/functional/SettingsModal.svelte @@ -17,19 +17,40 @@ let { file, onclose }: Props = $props(); - let settings = $state({}); + const getCurrentConverter = ( + vertFile: VertFile, + converterOverride?: string, + ) => { + const converterName = + converterOverride || vertFile.conversionSettings.converter; + const availableConverters = vertFile.isZip() + ? vertFile.converters + : vertFile.findConverters(); + + if (converterName) { + const selectedConverter = + availableConverters.find((c) => c.name === converterName) || + vertFile.converters.find((c) => c.name === converterName); + if (selectedConverter) return selectedConverter; + } + + return vertFile.isZip() + ? vertFile.converters[0] + : vertFile.findConverters()[0]; + }; + + let settings = $derived({ + converter: file ? getCurrentConverter(file)?.name : undefined, + }); const handleSettingChange = (key: string, value: any) => { if (!file) return; settings[key] = value; }; - const applySettings = async () => { - onclose?.(); + const applySettings = async (converterName: string) => { if (!file) return; - const converter = file.isZip() - ? file.converters[0] - : file.findConverters()[0]; + const converter = getCurrentConverter(file, converterName); if (!converter) { log( ["settings", "modal"], @@ -61,7 +82,10 @@ }, { text: "Apply", - action: applySettings, + action: () => { + applySettings(settings.converter!); + onclose?.(); + }, primary: true, }, ]} @@ -69,171 +93,196 @@ >
{#if file} - - {#await file.getAvailableSettings(file) then availableSettings} -
-

- {@html sanitize( - m["convert.settings.description"]({ - converter: file.isZip() - ? file.converters[0].name - : file.findConverters()[0].name || - "unknown", - filename: file.name, - }), - )} -

+ {@const currentConverter = getCurrentConverter(file)} + {@const availableConverters = file.isZip() + ? file.converters + : file.findConverters()} +

+ {@html sanitize( + m["convert.settings.description"]({ + converter: currentConverter?.name || "unknown", + filename: file.name, + }), + )} +

- {#if availableSettings.length === 0} -

- {m["convert.settings.none"]()} -

- {:else} -
- {#each availableSettings as setting (setting.key)} -
-

- {setting.label} -

- - {#if setting.description} -

- {setting.description} +

+

+ {m["convert.settings.converter"]()} +

+ ({ + value: converter.name, + label: converter.name, + }))} + selected={settings.converter || currentConverter?.name} + settingsStyle + onselect={(value) => { + settings = { converter: value }; // TODO: dont think i need to add the converter here + }} + /> +
+ + {#key settings} + {#await file.getAvailableSettings(file, settings.converter) then availableSettings} +
+ {#if availableSettings.length === 0} +

+ {m["convert.settings.none"]()} +

+ {:else} +
+ {#each availableSettings as setting (setting.key)} +
+

+ {setting.label}

- {/if} + + {#if setting.description} +

+ {setting.description} +

+ {/if} - {#if setting.type === "select"} - - typeof opt === "string" - ? { - value: opt, - label: opt, - } - : opt, - ) || []} - selected={settings[setting.key] ?? - file.conversionSettings[ + {#if setting.type === "select"} + + typeof opt === "string" + ? { + value: opt, + label: opt, + } + : opt, + ) || []} + selected={settings[ setting.key ] ?? - setting.default} - settingsStyle - onselect={(value) => - handleSettingChange( - setting.key, - value, - )} - disabled={setting.disabled} - /> - {#if setting.hasCustomInput} - {@const disabled = - (settings[setting.key] ?? file.conversionSettings[ setting.key - ]) !== "custom"} + ] ?? + setting.default} + settingsStyle + onselect={(value) => + handleSettingChange( + setting.key, + value, + )} + disabled={setting.disabled} + /> + {#if setting.hasCustomInput} + {@const disabled = + (settings[setting.key] ?? + file.conversionSettings[ + setting.key + ]) !== "custom"} + + handleSettingChange( + setting.customInputKey!, + e.currentTarget + .value, + )} + /> + {/if} + {:else if setting.type === "boolean"} + onchange={(e) => handleSettingChange( - setting.customInputKey!, - e.currentTarget.value, + setting.key, + e.currentTarget.checked, )} + disabled={setting.disabled} /> - {/if} - {:else if setting.type === "boolean"} - - handleSettingChange( - setting.key, - e.currentTarget.checked, - )} - disabled={setting.disabled} - /> - {:else if setting.type === "range"} - {@const rangeValue = (settings[ - setting.key - ] ?? - file.conversionSettings[ - setting.key - ] ?? - setting.default ?? - setting.min ?? - 0) as number} - {@const rangeLabel = - setting.options?.[rangeValue] - ?.label ?? rangeValue} -
- { - const nextValue = - e.currentTarget - .valueAsNumber; + setting.default ?? + setting.min ?? + 0) as number} + {@const rangeLabel = + setting.options?.[rangeValue] + ?.label ?? rangeValue} +
+ { + const nextValue = + e.currentTarget + .valueAsNumber; + handleSettingChange( + setting.key, + nextValue, + ); + }} + disabled={setting.disabled} + /> + + {rangeLabel} + +
+ {:else} + handleSettingChange( setting.key, - nextValue, - ); - }} + e.currentTarget.value, + )} disabled={setting.disabled} /> - - {rangeLabel} - -
- {:else} - - handleSettingChange( - setting.key, - e.currentTarget.value, - )} - disabled={setting.disabled} - /> - {/if} -
- {/each} -
- {/if} -
- {/await} + {/if} +
+ {/each} +
+ {/if} +
+ {/await} + {/key} {/if}
diff --git a/src/lib/converters/index.ts b/src/lib/converters/index.ts index e0b3491a..aa95fc54 100644 --- a/src/lib/converters/index.ts +++ b/src/lib/converters/index.ts @@ -7,6 +7,7 @@ import { MagickConverter } from "./magick.svelte"; import { DISABLE_ALL_EXTERNAL_REQUESTS } from "$lib/util/consts"; import { MediabunnyConverter } from "./mediabunny.svelte"; +// TODO: change this to include category with initialization to replace converterCategories and maybe categories as well const getConverters = (): Converter[] => { const converters: Converter[] = [ new MagickConverter(), @@ -21,6 +22,12 @@ const getConverters = (): Converter[] => { }; export const converters = getConverters(); +export const converterCategories = { + image: ["imagemagick"], + video: ["mediabunny", "vertd"], + audio: ["ffmpeg"], + doc: ["pandoc"], +} export function getConverterByFormat(format: string) { for (const converter of converters) { @@ -40,13 +47,13 @@ export const categories: Categories = { categories.audio.formats = converters - .find((c) => c.name === "ffmpeg") + .find((c) => converterCategories.audio.includes(c.name)) ?.supportedFormats.filter((f) => f.toSupported && f.isNative) .map((f) => f.name) || []; categories.video.formats = [ ...new Set( converters - .filter((c) => c.name === "mediabunny" || c.name === "vertd") + .filter((c) => converterCategories.video.includes(c.name)) .flatMap((c) => c.supportedFormats .filter((f) => f.toSupported && f.isNative) @@ -56,11 +63,11 @@ categories.video.formats = [ ]; categories.image.formats = converters - .find((c) => c.name === "imagemagick") + .find((c) => converterCategories.image.includes(c.name)) ?.formatStrings((f) => f.toSupported) || []; categories.doc.formats = converters - .find((c) => c.name === "pandoc") + .find((c) => converterCategories.doc.includes(c.name)) ?.supportedFormats.filter((f) => f.toSupported && f.isNative) .map((f) => f.name) || []; diff --git a/src/lib/converters/mediabunny.svelte.ts b/src/lib/converters/mediabunny.svelte.ts index f5339fae..75d11e1e 100644 --- a/src/lib/converters/mediabunny.svelte.ts +++ b/src/lib/converters/mediabunny.svelte.ts @@ -23,6 +23,12 @@ import { Converter, FormatInfo, type WorkerStatus } from "./converter.svelte"; import { ToastManager } from "$lib/util/toast.svelte"; import { error, log } from "$lib/util/logger"; import { registerFlacEncoder } from "@mediabunny/flac-encoder"; +import { m } from "$lib/paraglide/messages"; +import type { + SettingDefinition, + ConversionSettings, +} from "$lib/types/conversion-settings"; +import { CONVERSION_BITRATES, SAMPLE_RATES } from "./ffmpeg.svelte"; // codec compatibility object, based on docs // https://mediabunny.dev/guide/supported-formats-and-codecs#compatibility-table @@ -209,6 +215,152 @@ export class MediabunnyConverter extends Converter { registerAc3Encoder(); } + public async getAvailableSettings(): Promise { + // TODO: maybe have a slider for conversion speed/quality like vertd + + const fps: SettingDefinition = { + key: "fps", + label: m["convert.settings.video.fps"](), + type: "select", + default: "auto", + options: [ + { value: "auto", label: m["convert.settings.common.auto"]() }, + { + value: "custom", + label: m["convert.settings.common.custom"](), + }, + { value: "24", label: "24" }, + { value: "30", label: "30" }, + { value: "60", label: "60" }, + { value: "120", label: "120" }, + { value: "144", label: "144" }, + { value: "240", label: "240" }, + ], + hasCustomInput: true, + customInputKey: "customFps", + placeholder: m["convert.settings.video.fps_placeholder"](), + }; + + const resolution: SettingDefinition = { + key: "resolution", + label: m["convert.settings.video.resolution"](), + type: "select", + default: "auto", + options: [ + { value: "auto", label: m["convert.settings.common.auto"]() }, + { + value: "custom", + label: m["convert.settings.common.custom"](), + }, + { value: "426x240", label: "426x240" }, + { value: "640x360", label: "640x360" }, + { value: "854x480", label: "854x480" }, + { value: "1280x720", label: "1280x720" }, + { value: "1920x1080", label: "1920x1080" }, + { value: "2560x1440", label: "2560x1440" }, + { value: "3840x2160", label: "3840x2160" }, + ], + hasCustomInput: true, + customInputKey: "customResolution", + placeholder: m["convert.settings.video.resolution_placeholder"](), + }; + + // TODO: allow CRF for consistent quality? + const videoBitrate: SettingDefinition = { + key: "videoBitrate", + label: m["convert.settings.video.video_bitrate"](), + type: "select", + default: "auto", + options: [ + { value: "auto", label: m["convert.settings.common.auto"]() }, + { + value: "custom", + label: m["convert.settings.common.custom"](), + }, + { value: "1000", label: "1000 kbps" }, + { value: "2500", label: "2500 kbps" }, + { value: "5000", label: "5000 kbps" }, + { value: "8000", label: "8000 kbps" }, + { value: "12000", label: "12000 kbps" }, + { value: "18000", label: "18000 kbps" }, + ], + hasCustomInput: true, + customInputKey: "customVideoBitrate", + placeholder: m["convert.settings.video.bitrate_placeholder"](), + }; + + /* + * audio settings + */ + const audioBitrate: SettingDefinition = { + key: "audioBitrate", + label: m["convert.settings.video.audio_bitrate"](), + type: "select", + default: "auto", + options: CONVERSION_BITRATES.map((b) => ({ + value: b.toString(), + label: + b === "auto" + ? m["convert.settings.common.auto"]() + : b === "custom" + ? m["convert.settings.common.custom"]() + : `${b} kbps`, + })), + hasCustomInput: true, + customInputKey: "customAudioBitrate", + placeholder: m["convert.settings.audio.bitrate_placeholder"](), + }; + + const sampleRate: SettingDefinition = { + key: "sampleRate", + label: m["convert.settings.audio.sample_rate"](), + type: "select", + default: "auto", + options: SAMPLE_RATES.map((r) => ({ + value: r.toString(), + label: + r === "auto" + ? m["convert.settings.common.auto"]() + : r === "custom" + ? m["convert.settings.common.custom"]() + : `${r} Hz`, + })), + hasCustomInput: true, + customInputKey: "customSampleRate", + placeholder: m["convert.settings.audio.sample_rate_placeholder"](), + }; + + /* + * common + */ + const metadata: SettingDefinition = { + key: "metadata", + label: m["convert.settings.common.metadata"](), + type: "boolean", + default: true, + }; + + // trim/crop/rotate - also have another ui for this prob + + return [ + videoBitrate, + resolution, + fps, + metadata, + audioBitrate, + sampleRate, + ]; + } + + public async getDefaultSettings(): Promise { + const defaults: ConversionSettings = {}; + const settings = await this.getAvailableSettings(); + settings.forEach((setting) => { + defaults[setting.key] = setting.default; + }); + return defaults; + } + public async convert(file: VertFile, to: string): Promise { const input = new Input({ // TODO: add settings & special handling for certain formats & codecs diff --git a/src/lib/store/index.svelte.ts b/src/lib/store/index.svelte.ts index b873a57d..b1aa0964 100644 --- a/src/lib/store/index.svelte.ts +++ b/src/lib/store/index.svelte.ts @@ -1,5 +1,5 @@ import { browser } from "$app/environment"; -import { byNative, converters } from "$lib/converters"; +import { byNative, converterCategories, converters } from "$lib/converters"; import { error, log } from "$lib/util/logger"; import { VertFile } from "$lib/types"; import { parseBlob, selectCover } from "music-metadata"; @@ -36,12 +36,12 @@ class Files { private _addThumbnail = async (file: VertFile) => { this.thumbnailQueue.add(async () => { const isAudio = converters - .find((c) => c.name === "ffmpeg") + .find((c) => converterCategories.audio.includes(c.name)) ?.supportedFormats.filter((f) => f.isNative) .map((f) => f.name) ?.includes(file.from.toLowerCase()); const isVideo = converters - .find((c) => c.name === "vertd") + .find((c) => converterCategories.video.includes(c.name)) ?.supportedFormats.filter((f) => f.isNative) .map((f) => f.name) ?.includes(file.from.toLowerCase()); @@ -291,7 +291,7 @@ class Files { this._addThumbnail(vf); const convName = converter.name; - if (file.size > MAX_ARRAY_BUFFER_SIZE && convName === "vertd") { + if (file.size > MAX_ARRAY_BUFFER_SIZE && (converterCategories.video.includes(convName))) { ToastManager.add({ type: "warning", message: m["convert.large_file_warning"]({ @@ -303,10 +303,11 @@ class Files { }); } - const isVideo = convName === "vertd"; + // TODO: only show if vertd is needed/requested + const isServerVideo = convName === "vertd"; const acceptedExternalWarning = localStorage.getItem("acceptedExternalWarning") === "true"; - if (isVideo && !acceptedExternalWarning && !this._warningShown) { + if (isServerVideo && !acceptedExternalWarning && !this._warningShown) { this._warningShown = true; const title = m["convert.external_warning.title"](); const message = m["convert.external_warning.text"](); diff --git a/src/lib/types/file.svelte.ts b/src/lib/types/file.svelte.ts index e4976e5b..c68f60c4 100644 --- a/src/lib/types/file.svelte.ts +++ b/src/lib/types/file.svelte.ts @@ -40,10 +40,15 @@ export class VertFile { public isZip = $state(() => this.from === ".zip"); - public getAvailableSettings(input: VertFile): Promise { - const converter = this.findConverters()[0]; - if (!converter) return Promise.resolve([]); - return converter.getAvailableSettings(input); + public getAvailableSettings( + input: VertFile, + converter: string | undefined = this.conversionSettings.converter, + ): Promise { + const converterInstance = this.converters.find( + (c) => c.name === converter, + ); + if (!converterInstance) return Promise.resolve([]); + return converterInstance.getAvailableSettings(input); } public findConverters(supportedFormats: string[] = [this.from]) { @@ -133,10 +138,19 @@ export class VertFile { // eslint-disable-next-line @typescript-eslint/no-explicit-any public async convert(...args: any[]) { if (!this.converters.length) throw new Error("No converters found"); - const converter = this.isZip() - ? this.converters[0] - : this.findConverters()[0]; + + const customConverter = this.converters.find( + (c) => c.name === this.conversionSettings.converter, + ); + const converter = + customConverter || + (this.isZip() // TODO: not sure if the zip needs to be changed now + ? this.converters[0] + : this.findConverters([this.from, this.to])[0]); + log(["file", "convert"], `using converter: ${converter.name}`); + if (!converter) throw new Error("No converter found"); + this.result = null; this.progress = 0; this.processing = true; @@ -255,9 +269,9 @@ export class VertFile { public async cancel() { if (!this.processing) return; - const converter = this.isZip() - ? this.converters[0] - : this.findConverters()[0]; + const converter = this.converters.find( + (c) => c.name === this.conversionSettings.converter, + ); if (!converter) throw new Error("No converter found"); this.cancelled = true; try { diff --git a/src/routes/convert/+page.svelte b/src/routes/convert/+page.svelte index 8160afc6..206b6e7d 100644 --- a/src/routes/convert/+page.svelte +++ b/src/routes/convert/+page.svelte @@ -5,7 +5,11 @@ import Panel from "$lib/components/visual/Panel.svelte"; import ProgressBar from "$lib/components/visual/ProgressBar.svelte"; import Tooltip from "$lib/components/visual/Tooltip.svelte"; - import { categories, converters } from "$lib/converters"; + import { + categories, + converterCategories, + converters, + } from "$lib/converters"; import { effects, files, @@ -35,6 +39,22 @@ let processedFileIds = $state(new Set()); + const getCurrentConverter = (file: VertFile) => { + const converterName = file.conversionSettings.converter; + const availableConverters = file.isZip() + ? file.converters + : file.findConverters(); + + if (converterName) { + const selectedConverter = + availableConverters.find((c) => c.name === converterName) || + file.converters.find((c) => c.name === converterName); + if (selectedConverter) return selectedConverter; + } + + return file.isZip() ? file.converters[0] : file.findConverters()[0]; + }; + $effect(() => { if (!Settings.instance.settings || files.files.length === 0) return; @@ -42,14 +62,19 @@ const settings = Settings.instance.settings; if (processedFileIds.has(file.id)) return; - const converter = file.isZip() ? file.converters[0] : file.findConverters()[0]; + const converter = getCurrentConverter(file); if (!converter) return; + // Initialize converter in settings if not already set + if (!file.conversionSettings.converter) + file.conversionSettings.converter = converter.name; + + let category: string | undefined; - const isImage = converter.name === "imagemagick"; - const isAudio = converter.name === "ffmpeg"; - const isVideo = converter.name === "vertd"; - const isDocument = converter.name === "pandoc"; + const isImage = converterCategories.image.includes(converter.name); + const isAudio = converterCategories.audio.includes(converter.name); + const isVideo = converterCategories.video.includes(converter.name); + const isDocument = converterCategories.doc.includes(converter.name); if (isImage) category = "image"; else if (isAudio) category = "audio"; @@ -108,16 +133,19 @@ let type = ""; if (files.files.length) { const converters = files.files.map( - (file) => (file.isZip() ? file.converters[0] : file.findConverters()[0])?.name, + (file) => getCurrentConverter(file)?.name, ); const uniqueTypes = new Set(converters); if (uniqueTypes.size === 1) { const onlyType = converters[0]; - if (onlyType === "imagemagick") type = "blue"; - else if (onlyType === "ffmpeg") type = "purple"; - else if (onlyType === "vertd") type = "red"; - else if (onlyType === "pandoc") type = "green"; + if (converterCategories.image.includes(onlyType)) type = "blue"; + else if (converterCategories.audio.includes(onlyType)) + type = "purple"; + else if (converterCategories.video.includes(onlyType)) + type = "red"; + else if (converterCategories.doc.includes(onlyType)) + type = "green"; } } @@ -130,11 +158,12 @@ {#snippet fileItem(file: VertFile, index: number)} - {@const currentConverter = file.isZip() ? file.converters[0] : file.findConverters()[0]} - {@const isImage = currentConverter?.name === "imagemagick"} - {@const isAudio = currentConverter?.name === "ffmpeg"} - {@const isVideo = currentConverter?.name === "vertd"} - {@const isDocument = currentConverter?.name === "pandoc"} + {@const currentConverter = getCurrentConverter(file)} + {@const name = currentConverter?.name || "unknown"} + {@const isImage = converterCategories.image.includes(name)} + {@const isAudio = converterCategories.audio.includes(name)} + {@const isVideo = converterCategories.video.includes(name)} + {@const isDocument = converterCategories.doc.includes(name)}
{#if !converters.length} From 95210e84eb73a886638eceeb2371a79308c21d2a Mon Sep 17 00:00:00 2001 From: Maya Date: Mon, 9 Mar 2026 19:41:10 +0300 Subject: [PATCH 049/118] chore: re-organize structure a bit --- .../functional/FormatDropdown.svelte | 4 +-- .../functional/{ => popups}/Dialog.svelte | 0 .../functional/{ => popups}/Modal.svelte | 0 .../{ => popups}/SettingsModal.svelte | 4 +-- .../functional/{ => popups}/VertdError.svelte | 5 ++-- .../{ => popups}/VertdErrorDetails.svelte | 0 src/lib/components/layout/Dialogs.svelte | 2 +- src/lib/converters/mediabunny.svelte.ts | 7 +++--- src/lib/converters/vertd.svelte.ts | 2 +- src/lib/types/file.svelte.ts | 25 ++++++++++++++----- src/routes/convert/+page.svelte | 6 ----- 11 files changed, 32 insertions(+), 23 deletions(-) rename src/lib/components/functional/{ => popups}/Dialog.svelte (100%) rename src/lib/components/functional/{ => popups}/Modal.svelte (100%) rename src/lib/components/functional/{ => popups}/SettingsModal.svelte (98%) rename src/lib/components/functional/{ => popups}/VertdError.svelte (98%) rename src/lib/components/functional/{ => popups}/VertdErrorDetails.svelte (100%) diff --git a/src/lib/components/functional/FormatDropdown.svelte b/src/lib/components/functional/FormatDropdown.svelte index 3ba801c8..5cb27d10 100644 --- a/src/lib/components/functional/FormatDropdown.svelte +++ b/src/lib/components/functional/FormatDropdown.svelte @@ -8,7 +8,7 @@ import { onMount } from "svelte"; import { quintOut } from "svelte/easing"; import { VertFile } from "$lib/types"; - import SettingsModal from "./SettingsModal.svelte"; + import SettingsModal from "./popups/SettingsModal.svelte"; import { log } from "$lib/util/logger"; type Props = { @@ -279,7 +279,7 @@ }); const ext = filename.split(".").pop() ?? ""; return new VertFile(f, ext); - } catch (err) { + } catch { return null; } }) diff --git a/src/lib/components/functional/Dialog.svelte b/src/lib/components/functional/popups/Dialog.svelte similarity index 100% rename from src/lib/components/functional/Dialog.svelte rename to src/lib/components/functional/popups/Dialog.svelte diff --git a/src/lib/components/functional/Modal.svelte b/src/lib/components/functional/popups/Modal.svelte similarity index 100% rename from src/lib/components/functional/Modal.svelte rename to src/lib/components/functional/popups/Modal.svelte diff --git a/src/lib/components/functional/SettingsModal.svelte b/src/lib/components/functional/popups/SettingsModal.svelte similarity index 98% rename from src/lib/components/functional/SettingsModal.svelte rename to src/lib/components/functional/popups/SettingsModal.svelte index caf417e2..54055f47 100644 --- a/src/lib/components/functional/SettingsModal.svelte +++ b/src/lib/components/functional/popups/SettingsModal.svelte @@ -1,8 +1,8 @@ + +
+

+ {m["convert.errors.converter_fallback.body"]({ + filename: additional.fileName, + converter: additional.nextConverter, + })} +

+
+ + +
+
diff --git a/src/lib/types/file.svelte.ts b/src/lib/types/file.svelte.ts index 25c31bad..b767033e 100644 --- a/src/lib/types/file.svelte.ts +++ b/src/lib/types/file.svelte.ts @@ -4,6 +4,7 @@ import { m } from "$lib/paraglide/messages"; import { ToastManager } from "$lib/util/toast.svelte"; import type { Component } from "svelte"; import { MAX_ARRAY_BUFFER_SIZE } from "$lib/store/index.svelte"; +import FallbackToast from "$lib/components/functional/popups/FallbackToast.svelte"; import type { ConversionSettings, SettingDefinition, @@ -37,6 +38,9 @@ export class VertFile { public cancelled = $state(false); public converters: Converter[] = []; + private fallbackToastId: number | null = null; + private attemptedConverters = new Set(); + private retryingFallback = false; public isZip = $state(() => this.from === ".zip"); @@ -137,6 +141,8 @@ export class VertFile { // eslint-disable-next-line @typescript-eslint/no-explicit-any public async convert(...args: any[]) { + if (!this.retryingFallback) this.attemptedConverters.clear(); + if (!this.converters.length) throw new Error("No converters found"); const customConverter = this.converters.find( @@ -162,6 +168,7 @@ export class VertFile { } if (!converter) throw new Error("No converter found"); + this.attemptedConverters.add(converter.name); log(["file", "convert"], `using converter: ${converter.name}`); this.result = null; @@ -181,8 +188,74 @@ export class VertFile { ...args, ); this.result = res; + if (this.fallbackToastId !== null) { + ToastManager.remove(this.fallbackToastId); + this.fallbackToastId = null; + } } catch (err) { if (!this.cancelled) this.toastErr(err); + + const compatibleConverters = this.findConverters([ + this.from, + this.to, + ]); + const nextConverter = compatibleConverters.find( + (c) => !this.attemptedConverters.has(c.name), + ); + + // TODO: clean up languages file, then migrate all languages to new structure + + // TODO: should figure out a cleaner way to do this + if (!this.cancelled && nextConverter) { + if (this.fallbackToastId !== null) + ToastManager.remove(this.fallbackToastId); + + this.fallbackToastId = ToastManager.add({ + type: "warning", + disappearing: false, + message: FallbackToast, + additional: { + fileName: this.file.name, + nextConverter: nextConverter.name, + onNext: async () => { + if (this.fallbackToastId !== null) + ToastManager.remove(this.fallbackToastId); + this.fallbackToastId = null; + + log( + ["file", "convert"], + `retrying ${this.name} with next compatible converter: ${nextConverter.name}`, + ); + + this.conversionSettings = { + ...this.conversionSettings, + converter: nextConverter.name, + }; + this.retryingFallback = true; + try { + await this.convert(...args); + } finally { + this.retryingFallback = false; + } + }, + onCancel: () => { + if (this.fallbackToastId !== null) + ToastManager.remove(this.fallbackToastId); + this.fallbackToastId = null; + this.cancelled = true; + }, + }, + }); + } else if (!this.cancelled) { + this.cancelled = true; + ToastManager.add({ + type: "error", + message: m["convert.errors.converter_fallback.all_failed"]({ + filename: this.file.name, + }), + }); + } + this.result = null; } this.processing = false; From 55b67ec04672bf4bddbcae67f5cae857161f6d55 Mon Sep 17 00:00:00 2001 From: Maya Date: Tue, 10 Mar 2026 13:31:06 +0300 Subject: [PATCH 051/118] fix: cleaner and better converter select logic only shows converters compatible with .from and .to now as well --- .../functional/popups/SettingsModal.svelte | 43 ++++++++----------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/src/lib/components/functional/popups/SettingsModal.svelte b/src/lib/components/functional/popups/SettingsModal.svelte index 54055f47..acb94de5 100644 --- a/src/lib/components/functional/popups/SettingsModal.svelte +++ b/src/lib/components/functional/popups/SettingsModal.svelte @@ -17,30 +17,20 @@ let { file, onclose }: Props = $props(); - const getCurrentConverter = ( - vertFile: VertFile, - converterOverride?: string, - ) => { - const converterName = - converterOverride || vertFile.conversionSettings.converter; - const availableConverters = vertFile.isZip() + const getAvailableConverters = (vertFile: VertFile) => { + return vertFile.isZip() ? vertFile.converters - : vertFile.findConverters(); - - if (converterName) { - const selectedConverter = - availableConverters.find((c) => c.name === converterName) || - vertFile.converters.find((c) => c.name === converterName); - if (selectedConverter) return selectedConverter; - } + : vertFile.findConverters([vertFile.from, vertFile.to]); + }; - return vertFile.isZip() - ? vertFile.converters[0] - : vertFile.findConverters()[0]; + const getValidConverter = (vertFile: VertFile, converterName?: string) => { + const available = getAvailableConverters(vertFile); + const name = converterName || vertFile.conversionSettings.converter; + return available.find((c) => c.name === name) || available[0]; }; let settings = $derived({ - converter: file ? getCurrentConverter(file)?.name : undefined, + converter: file ? getValidConverter(file)?.name : undefined, }); const handleSettingChange = (key: string, value: any) => { @@ -50,7 +40,7 @@ const applySettings = async (converterName: string) => { if (!file) return; - const converter = getCurrentConverter(file, converterName); + const converter = getValidConverter(file, converterName); if (!converter) { log( ["settings", "modal"], @@ -93,14 +83,15 @@ >
{#if file} - {@const currentConverter = getCurrentConverter(file)} - {@const availableConverters = file.isZip() - ? file.converters - : file.findConverters()} + {@const availableConverters = getAvailableConverters(file)} + {@const validConverter = getValidConverter( + file, + settings.converter, + )}

{@html sanitize( m["convert.settings.description"]({ - converter: currentConverter?.name || "unknown", + converter: validConverter?.name || "unknown", filename: file.name, }), )} @@ -115,7 +106,7 @@ value: converter.name, label: converter.name, }))} - selected={settings.converter || currentConverter?.name} + selected={validConverter?.name} settingsStyle onselect={(value) => { settings = { converter: value }; // TODO: dont think i need to add the converter here From 9201f1e92f7e441e4de2da4c9dcb675e0e17b399 Mon Sep 17 00:00:00 2001 From: Maya Date: Tue, 10 Mar 2026 16:28:00 +0300 Subject: [PATCH 052/118] fix: more bug fixes --- .../functional/popups/SettingsModal.svelte | 14 ++++++++--- src/lib/converters/vertd.svelte.ts | 25 ++++++++++--------- src/lib/types/conversion-settings.ts | 2 +- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/lib/components/functional/popups/SettingsModal.svelte b/src/lib/components/functional/popups/SettingsModal.svelte index acb94de5..03e73c4d 100644 --- a/src/lib/components/functional/popups/SettingsModal.svelte +++ b/src/lib/components/functional/popups/SettingsModal.svelte @@ -9,6 +9,7 @@ import { sanitize } from "$lib/store/index.svelte"; import { log } from "$lib/util/logger"; import { type ConversionSettings } from "$lib/types/conversion-settings"; + import { onMount } from "svelte"; type Props = { file: VertFile | null; @@ -29,9 +30,7 @@ return available.find((c) => c.name === name) || available[0]; }; - let settings = $derived({ - converter: file ? getValidConverter(file)?.name : undefined, - }); + let settings = $state({}); const handleSettingChange = (key: string, value: any) => { if (!file) return; @@ -59,6 +58,13 @@ `Applied settings for ${file.name}: ${JSON.stringify(file.conversionSettings, null, 2)}`, ); }; + + onMount(() => { + if (!file) return; + + // always have a converter initialized so we can show its settings + settings.converter = getValidConverter(file)?.name; + }); { - settings = { converter: value }; // TODO: dont think i need to add the converter here + settings.converter = value; }} />

diff --git a/src/lib/converters/vertd.svelte.ts b/src/lib/converters/vertd.svelte.ts index 151c5b75..c8147617 100644 --- a/src/lib/converters/vertd.svelte.ts +++ b/src/lib/converters/vertd.svelte.ts @@ -392,21 +392,27 @@ export class VertdConverter extends Converter { const qualityOptions = [ { - value: "verySlow", + value: 0, // very slow label: m["convert.settings.video.speed_very_slow"](), }, { - value: "slower", + value: 1, // slower label: m["convert.settings.video.speed_slower"](), }, - { value: "slow", label: m["convert.settings.video.speed_slow"]() }, { - value: "medium", + value: 2, // slow + label: m["convert.settings.video.speed_slow"](), + }, + { + value: 3, // medium label: m["convert.settings.video.speed_medium"](), }, - { value: "fast", label: m["convert.settings.video.speed_fast"]() }, { - value: "ultraFast", + value: 4, // faster + label: m["convert.settings.video.speed_fast"](), + }, + { + value: 5, // fastest label: m["convert.settings.video.speed_ultra_fast"](), }, ]; @@ -415,7 +421,7 @@ export class VertdConverter extends Converter { const defaultSpeed = Settings.instance.settings.vertdSpeed; const defaultSpeedIndex = vertdSpeedValues.indexOf(defaultSpeed); const qualitySpeedRange: SettingDefinition = { - key: "vertdSpeedSlider", + key: "vertdSpeed", label: m["convert.settings.video.speed"](), description: m["convert.settings.video.speed_description"](), type: "range", @@ -426,7 +432,6 @@ export class VertdConverter extends Converter { options: qualityOptions.map((option, index) => ({ value: index, label: option.label, - speedValue: option.value, })), forceFullWidth: true, }; @@ -573,10 +578,6 @@ export class VertdConverter extends Converter { defaults[setting.key] = setting.default; }); - if (defaults.vertdSpeedSlider !== undefined) { - const sliderIndex = defaults.vertdSpeedSlider as number; - defaults.vertdSpeed = vertdSpeedValues[sliderIndex]; - } return defaults; } diff --git a/src/lib/types/conversion-settings.ts b/src/lib/types/conversion-settings.ts index 8c58bf4c..27672d66 100644 --- a/src/lib/types/conversion-settings.ts +++ b/src/lib/types/conversion-settings.ts @@ -11,7 +11,7 @@ export interface SettingDefinition { min?: number; max?: number; step?: number; - options?: Array<{ value: any; label: any; speedValue?: any }>; // for select/range types + options?: Array<{ value: any; label: any; }>; // for select/range types description?: string; hasCustomInput?: boolean; // for select types with a "custom" option customInputKey?: string; // key to use for custom input value in settings object From fb1bd521f38278947fd7e94943a5a01a764f586c Mon Sep 17 00:00:00 2001 From: Maya Date: Tue, 10 Mar 2026 21:29:31 +0300 Subject: [PATCH 053/118] feat: mediabunny video/audio codec, add custom inputs, show discarded tracks --- messages/en.json | 4 +- .../functional/popups/SettingsModal.svelte | 1 - src/lib/converters/mediabunny.svelte.ts | 304 +++++++++++------- 3 files changed, 192 insertions(+), 117 deletions(-) diff --git a/messages/en.json b/messages/en.json index 224582bf..73ce8d1f 100644 --- a/messages/en.json +++ b/messages/en.json @@ -123,7 +123,9 @@ "audio_bitrate": "Audio bitrate (kbps)", "bitrate_placeholder": "Custom bitrate", "sample_rate": "Audio sample rate (Hz)", - "sample_rate_placeholder": "Custom sample rate" + "sample_rate_placeholder": "Custom sample rate", + "video_codec": "Video codec", + "audio_codec": "Audio codec" }, "document": { "something": "Something" diff --git a/src/lib/components/functional/popups/SettingsModal.svelte b/src/lib/components/functional/popups/SettingsModal.svelte index 03e73c4d..727c1626 100644 --- a/src/lib/components/functional/popups/SettingsModal.svelte +++ b/src/lib/components/functional/popups/SettingsModal.svelte @@ -119,7 +119,6 @@ }} />
- {#key settings} {#await file.getAvailableSettings(file, settings.converter) then availableSettings}
diff --git a/src/lib/converters/mediabunny.svelte.ts b/src/lib/converters/mediabunny.svelte.ts index 031ab32f..d0171c0b 100644 --- a/src/lib/converters/mediabunny.svelte.ts +++ b/src/lib/converters/mediabunny.svelte.ts @@ -21,7 +21,6 @@ import { registerAc3Decoder, registerAc3Encoder } from "@mediabunny/ac3"; import { registerMp3Encoder } from "@mediabunny/mp3-encoder"; import { registerFlacEncoder } from "@mediabunny/flac-encoder"; import { Converter, FormatInfo, type WorkerStatus } from "./converter.svelte"; -import { ToastManager } from "$lib/util/toast.svelte"; import { error, log } from "$lib/util/logger"; import { m } from "$lib/paraglide/messages"; import type { @@ -29,108 +28,49 @@ import type { ConversionSettings, } from "$lib/types/conversion-settings"; import { CONVERSION_BITRATES, SAMPLE_RATES } from "./ffmpeg.svelte"; +import { ToastManager } from "$lib/util/toast.svelte"; -// codec compatibility object, based on docs +// codec compatibility stuff, based on mediabunny's docs // https://mediabunny.dev/guide/supported-formats-and-codecs#compatibility-table -// eslint-disable-next-line @typescript-eslint/no-unused-vars +const mp4VideoCodecs = ["avc", "hevc", "vp8", "vp9", "av1"] as const; +const mp4AudioCodecs = [ + "aac", + "opus", + "mp3", + "vorbis", + "flac", + "ac3", + "eac3", + "pcm-s16", + "pcm-s16be", + "pcm-s24", + "pcm-s24be", + "pcm-s32", + "pcm-s32be", + "pcm-f32", + "pcm-f64", +] as const; const codecCompatibility = { video: { - mp4: ["avc", "hevc", "vp8", "vp9", "av1"], - m4v: ["avc", "hevc", "vp8", "vp9", "av1"], - f4v: ["avc", "hevc", "vp8", "vp9", "av1"], - "3gp": ["avc", "hevc", "vp8", "vp9", "av1"], - "3g2": ["avc", "hevc", "vp8", "vp9", "av1"], - mkv: ["avc", "hevc", "vp8", "vp9", "av1"], + mp4: mp4VideoCodecs, + m4v: mp4VideoCodecs, + f4v: mp4VideoCodecs, + "3gp": mp4VideoCodecs, + "3g2": mp4VideoCodecs, + mkv: mp4VideoCodecs, webm: ["vp8", "vp9", "av1"], - mov: ["avc", "hevc", "vp8", "vp9", "av1"], + mov: mp4VideoCodecs, ts: ["avc", "hevc"], }, audio: { - mp4: [ - "aac", - "opus", - "mp3", - "vorbis", - "flac", - "ac3", - "eac3", - "pcm-s16", - "pcm-s16be", - "pcm-s24", - "pcm-s24be", - "pcm-s32", - "pcm-s32be", - "pcm-f32", - "pcm-f64", - ], - m4v: [ - "aac", - "opus", - "mp3", - "vorbis", - "flac", - "ac3", - "eac3", - "pcm-s16", - "pcm-s16be", - "pcm-s24", - "pcm-s24be", - "pcm-s32", - "pcm-s32be", - "pcm-f32", - "pcm-f64", - ], - f4v: [ - "aac", - "opus", - "mp3", - "vorbis", - "flac", - "ac3", - "eac3", - "pcm-s16", - "pcm-s16be", - "pcm-s24", - "pcm-s24be", - "pcm-s32", - "pcm-s32be", - "pcm-f32", - "pcm-f64", - ], - "3gp": [ - "aac", - "opus", - "mp3", - "vorbis", - "flac", - "ac3", - "eac3", - "pcm-s16", - "pcm-s16be", - "pcm-s24", - "pcm-s24be", - "pcm-s32", - "pcm-s32be", - "pcm-f32", - "pcm-f64", - ], - "3g2": [ - "aac", - "opus", - "mp3", - "vorbis", - "flac", - "ac3", - "eac3", - "pcm-s16", - "pcm-s16be", - "pcm-s24", - "pcm-s24be", - "pcm-s32", - "pcm-s32be", - "pcm-f32", - "pcm-f64", - ], + mp4: mp4AudioCodecs, + m4v: mp4AudioCodecs, + f4v: mp4AudioCodecs, + "3gp": mp4AudioCodecs, + "3g2": mp4AudioCodecs, + m4a: mp4AudioCodecs, + m4b: mp4AudioCodecs, + m4p: mp4AudioCodecs, mkv: [ "aac", "opus", @@ -173,6 +113,80 @@ const codecCompatibility = { }, } as const; +const getCompatibleCodecs = ( + type: keyof typeof codecCompatibility, + format: string, +) => { + const normalized = format.replace(/^\./, "").toLowerCase(); + const direct = + codecCompatibility[type][ + normalized as keyof (typeof codecCompatibility)[typeof type] + ]; + if (direct) return [...direct]; + return []; +}; + +const buildVideoConfig = ( + settings: ConversionSettings, +): Record => { + const config: Record = {}; + + if (settings.videoCodec !== "auto") config.codec = settings.videoCodec; + + if (settings.videoBitrate !== "auto") { + const bitrate = + settings.videoBitrate === "custom" + ? settings.customVideoBitrate + : settings.videoBitrate; + config.bitrate = Number(bitrate); + } + + if (settings.fps !== "auto") { + const fps = + settings.fps === "custom" ? settings.customFps : settings.fps; + config.frameRate = Number(fps); + } + + if (settings.resolution !== "auto") { + const resolution = + settings.resolution === "custom" + ? settings.customResolution + : settings.resolution; + const [width, height] = resolution.split("x").map(Number); + config.width = width; + config.height = height; + config.fit = "contain"; // TODO: maybe allow changing this? + } + + return config; +}; + +const buildAudioConfig = ( + settings: ConversionSettings, +): Record => { + const config: Record = {}; + + if (settings.audioCodec !== "auto") config.codec = settings.audioCodec; + + if (settings.audioBitrate !== "auto") { + const bitrate = + settings.audioBitrate === "custom" + ? settings.customAudioBitrate + : settings.audioBitrate; + config.bitrate = Number(bitrate); + } + + if (settings.sampleRate !== "auto") { + const sampleRate = + settings.sampleRate === "custom" + ? settings.customSampleRate + : settings.sampleRate; + config.sampleRate = Number(sampleRate); + } + + return config; +}; + export class MediabunnyConverter extends Converter { public name = "mediabunny"; public status: WorkerStatus = $state("ready"); @@ -197,9 +211,17 @@ export class MediabunnyConverter extends Converter { ...this.formats.map((f) => new FormatInfo(f, true, true, true, 2)), ]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private log: (...msg: any[]) => void = () => {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private error: (...msg: any[]) => void = () => {}; + constructor() { super(); + this.log = (msg) => log(["converters", this.name], msg); + this.error = (msg) => error(["converters", this.name], msg); + // additional mediabunny coders // currently the official ones -- maybe add our own in the future this.initializeCodecs(); @@ -216,7 +238,9 @@ export class MediabunnyConverter extends Converter { registerAc3Encoder(); } - public async getAvailableSettings(): Promise { + public async getAvailableSettings( + input: VertFile, + ): Promise { // TODO: maybe have a slider for conversion speed/quality like vertd const fps: SettingDefinition = { @@ -290,6 +314,38 @@ export class MediabunnyConverter extends Converter { placeholder: m["convert.settings.video.bitrate_placeholder"](), }; + const toFormat = input.to; + const supportedVideoCodecs = getCompatibleCodecs("video", toFormat); + const videoCodec: SettingDefinition = { + key: "videoCodec", + label: m["convert.settings.video.video_codec"](), + type: "select", + default: "auto", + // TODO: get supported from codecCompatibility based on output format + options: [ + { value: "auto", label: m["convert.settings.common.auto"]() }, + ...supportedVideoCodecs.map((codec) => ({ + value: codec, + label: codec, + })), + ], + }; + + const supportedAudioCodecs = getCompatibleCodecs("audio", toFormat); + const audioCodec: SettingDefinition = { + key: "audioCodec", + label: m["convert.settings.video.audio_codec"](), + type: "select", + default: "auto", + options: [ + { value: "auto", label: m["convert.settings.common.auto"]() }, + ...supportedAudioCodecs.map((codec) => ({ + value: codec, + label: codec, + })), + ], + }; + /* * audio settings */ @@ -346,6 +402,8 @@ export class MediabunnyConverter extends Converter { return [ videoBitrate, resolution, + videoCodec, + audioCodec, fps, metadata, audioBitrate, @@ -353,16 +411,22 @@ export class MediabunnyConverter extends Converter { ]; } - public async getDefaultSettings(): Promise { + public async getDefaultSettings( + input: VertFile, + ): Promise { const defaults: ConversionSettings = {}; - const settings = await this.getAvailableSettings(); + const settings = await this.getAvailableSettings(input); settings.forEach((setting) => { defaults[setting.key] = setting.default; }); return defaults; } - public async convert(file: VertFile, to: string): Promise { + public async convert( + file: VertFile, + to: string, + settings: ConversionSettings, + ): Promise { const input = new Input({ // TODO: add settings & special handling for certain formats & codecs formats: [MP4, QTFF, MATROSKA, WEBM, MPEG_TS], @@ -374,27 +438,41 @@ export class MediabunnyConverter extends Converter { target: new BufferTarget(), }); + const conversionSettings = + Object.keys(settings).length > 0 + ? settings // user-provided settings + : await this.getDefaultSettings(file); // use defaults if not provided + + const videoConfig = buildVideoConfig(conversionSettings); + const audioConfig = buildAudioConfig(conversionSettings); + const conversion = await Conversion.init({ input, output, + video: videoConfig, + audio: audioConfig, + ...(conversionSettings.metadata === "false" ? { tags: {} } : {}), }); - if (!conversion.isValid) { - for (const discarded of conversion.discardedTracks) { - ToastManager.add({ - type: "error", - message: `Mediabunny discarded unsupported track: ${discarded.reason}`, - }); - } + this.activeConversions.set(file.id, conversion); + + this.log(`videoConfig: ${JSON.stringify(videoConfig)}`); + this.log(`audioConfig: ${JSON.stringify(audioConfig)}`); - throw new Error(`Mediabunny conversion not valid`); + for (const discarded of conversion.discardedTracks) { + ToastManager.add({ + type: "error", + message: `Mediabunny discarded ${discarded.track.type} track ${discarded.track.id} (${discarded.track.codec}) for reason: ${discarded.reason}`, + durations: { + stay: 10000, + }, + }); } conversion.onProgress = (progress) => { file.progress = progress * 100; }; - this.activeConversions.set(file.id, conversion); await conversion.execute(); this.activeConversions.delete(file.id); @@ -431,7 +509,7 @@ export class MediabunnyConverter extends Converter { case ".mov": return new MovOutputFormat(); case ".ts": - return new MpegTsOutputFormat(); // FIXME: audio tracks discarded - prob needs another audio codec + return new MpegTsOutputFormat(); default: throw new Error(`Unsupported format: ${ext}`); } @@ -440,17 +518,13 @@ export class MediabunnyConverter extends Converter { public async cancel(input: VertFile): Promise { const conversion = this.activeConversions.get(input.id); if (!conversion) { - error( - ["converters", this.name], + this.error( `no active conversion found for file ${input.name}`, ); return; } - log( - ["converters", this.name], - `cancelling conversion for file ${input.name}`, - ); + this.log(`cancelling conversion for file ${input.name}`); conversion.cancel(); this.activeConversions.delete(input.id); From ed851d8bd3969dcdaa8a217545290dc444845afb Mon Sep 17 00:00:00 2001 From: Maya Date: Tue, 10 Mar 2026 21:30:18 +0300 Subject: [PATCH 054/118] chore: clean up logging --- src/lib/converters/ffmpeg.svelte.ts | 121 +++++++++------------------- src/lib/converters/magick.svelte.ts | 38 ++++----- src/lib/converters/pandoc.svelte.ts | 22 +++-- src/lib/converters/vertd.svelte.ts | 13 ++- 4 files changed, 66 insertions(+), 128 deletions(-) diff --git a/src/lib/converters/ffmpeg.svelte.ts b/src/lib/converters/ffmpeg.svelte.ts index 3c04683d..ca9c260b 100644 --- a/src/lib/converters/ffmpeg.svelte.ts +++ b/src/lib/converters/ffmpeg.svelte.ts @@ -79,9 +79,16 @@ export class FFmpegConverter extends Converter { public readonly reportsProgress = true; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private log: (...msg: any[]) => void = () => {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private error: (...msg: any[]) => void = () => {}; + constructor() { super(); - log(["converters", this.name], `created converter`); + this.log = (msg) => log(["converters", this.name], msg); + this.error = (msg) => error(["converters", this.name], msg); + this.log(`created converter`); if (!browser) return; try { // this is just to cache the wasm and js for when we actually use it. we're not using this ffmpeg instance @@ -246,10 +253,7 @@ export class FFmpegConverter extends Converter { const buf = new Uint8Array(await input.file.arrayBuffer()); await ffmpeg.writeFile("input", buf); - log( - ["converters", this.name], - `wrote ${input.name} to ffmpeg virtual fs`, - ); + this.log(`wrote ${input.name} to ffmpeg virtual fs`); const command = await this.buildConversionCommand( ffmpeg, @@ -258,9 +262,9 @@ export class FFmpegConverter extends Converter { conversionSettings, isAlac, ); - log(["converters", this.name], `FFmpeg command: ${command.join(" ")}`); + this.log(`FFmpeg command: ${command.join(" ")}`); await ffmpeg.exec(command); - log(["converters", this.name], "executed ffmpeg command"); + this.log("executed ffmpeg command"); if (conversionError) { ffmpeg.off("log", errorListener); @@ -280,10 +284,7 @@ export class FFmpegConverter extends Converter { const outputFileName = input.name.split(".").slice(0, -1).join(".") + to; - log( - ["converters", this.name], - `read ${outputFileName} from ffmpeg virtual fs`, - ); + this.log(`read ${outputFileName} from ffmpeg virtual fs`); ffmpeg.off("log", errorListener); ffmpeg.terminate(); @@ -295,17 +296,11 @@ export class FFmpegConverter extends Converter { public async cancel(input: VertFile): Promise { const ffmpeg = this.activeConversions.get(input.id); if (!ffmpeg) { - error( - ["converters", this.name], - `no active conversion found for file ${input.name}`, - ); + this.error(`no active conversion found for file ${input.name}`); return; } - log( - ["converters", this.name], - `cancelling conversion for file ${input.name}`, - ); + this.log(`cancelling conversion for file ${input.name}`); ffmpeg.terminate(); this.activeConversions.delete(input.id); @@ -323,7 +318,7 @@ export class FFmpegConverter extends Converter { }); ffmpeg.on("log", (l) => { - log(["converters", this.name], l.message); + this.log(l.message); }); } @@ -358,10 +353,7 @@ export class FFmpegConverter extends Converter { const n = parseInt(event.message.trim(), 10); if (!n) return; bitrate = Math.round(n / 1000); - log( - ["converters", this.name], - `Detected stream audio bitrate: ${bitrate} kbps`, - ); + this.log(`Detected stream audio bitrate: ${bitrate} kbps`); }; ffmpeg.on("log", bitrateListener); @@ -400,10 +392,7 @@ export class FFmpegConverter extends Converter { const n = parseInt(event.message.trim(), 10); if (!n) return; sampleRate = n; - log( - ["converters", this.name], - `Detected stream audio sample rate: ${sampleRate} Hz`, - ); + this.log(`Detected stream audio sample rate: ${sampleRate} Hz`); }; ffmpeg.on("log", sampleRateListener); @@ -453,7 +442,7 @@ export class FFmpegConverter extends Converter { let metadataArgs: string[] = []; let m4aArgs: string[] = []; - log(["converters", this.name], `keep metadata: ${keepMetadata}`); + this.log(`keep metadata: ${keepMetadata}`); if (!keepMetadata) { metadataArgs = [ "-map_metadata", // remove metadata @@ -473,17 +462,13 @@ export class FFmpegConverter extends Converter { "-b:a", `${userBitrate === "custom" ? customBitrate : userBitrate}k`, ]; - log( - ["converters", this.name], - `using user setting for audio bitrate: ${userBitrate}`, - ); + this.log(`using user setting for audio bitrate: ${userBitrate}`); } else { // detect bitrate of original file and use if (isLosslessToLossy) { // use safe default audioBitrateArgs = ["-b:a", "128k"]; - log( - ["converters", this.name], + this.log( `converting from lossless to lossy, using default audio bitrate: 128k`, ); } else { @@ -491,10 +476,7 @@ export class FFmpegConverter extends Converter { audioBitrateArgs = inputBitrate ? ["-b:a", `${inputBitrate}k`] : []; - log( - ["converters", this.name], - `using detected audio bitrate: ${inputBitrate}k`, - ); + this.log(`using detected audio bitrate: ${inputBitrate}k`); } } @@ -504,17 +486,13 @@ export class FFmpegConverter extends Converter { "-ar", userSampleRate === "custom" ? customSampleRate : userSampleRate, ]; - log( - ["converters", this.name], - `using user setting for sample rate: ${userSampleRate}Hz`, - ); + this.log(`using user setting for sample rate: ${userSampleRate}Hz`); } else { // detect sample rate of original file and use if (isLosslessToLossy) { // use safe default const defaultRate = to === ".opus" ? "48000" : "44100"; - log( - ["converters", this.name], + this.log( `converting from lossless to lossy, using default sample rate: ${defaultRate}Hz`, ); sampleRateArgs = ["-ar", defaultRate]; @@ -522,9 +500,8 @@ export class FFmpegConverter extends Converter { let inputSampleRate = await this.detectAudioSampleRate(ffmpeg); if (to === ".opus" && inputSampleRate === 44100) { // special case: opus does not support 44100Hz which is more common - adjust to 48000Hz - log( - ["converters", this.name], - "conversion to opus with 44100Hz sample rate detected, adjusting to 48000Hz", + this.log( + `conversion to opus with 44100Hz sample rate detected, adjusting to 48000Hz`, ); inputSampleRate = 48000; } @@ -532,8 +509,7 @@ export class FFmpegConverter extends Converter { sampleRateArgs = inputSampleRate ? ["-ar", `${inputSampleRate}`] : []; - log( - ["converters", this.name], + this.log( `using detected audio sample rate: ${inputSampleRate}Hz`, ); } @@ -542,8 +518,7 @@ export class FFmpegConverter extends Converter { // channels setting if (settings.channels !== "auto") { channelsArgs = ["-ac", settings.channels]; - log( - ["converters", this.name], + this.log( `using user setting for audio channels: ${settings.channels}`, ); } @@ -560,18 +535,12 @@ export class FFmpegConverter extends Converter { tracksArgs = ["-map", "0:a:0"]; // default to first audio track if not specified } - log( - ["converters", this.name], - `using user setting for audio tracks: ${settings.tracks}`, - ); + this.log(`using user setting for audio tracks: ${settings.tracks}`); } // video to audio if (videoFormats.includes(inputFormat)) { - log( - ["converters", this.name], - `Converting video ${input.from} to audio ${to}`, - ); + this.log(`Converting video ${input.from} to audio ${to}`); return [ "-i", "input", @@ -588,10 +557,7 @@ export class FFmpegConverter extends Converter { // audio to video if (videoFormats.includes(outputFormat)) { - log( - ["converters", this.name], - `Converting audio ${input.from} to video ${to}`, - ); + this.log(`Converting audio ${input.from} to video ${to}`); const hasAlbumArt = keepMetadata ? await this.extractAlbumArt(ffmpeg) @@ -599,10 +565,7 @@ export class FFmpegConverter extends Converter { const codecArgs = toArgs(to, isAlac); if (hasAlbumArt) { - log( - ["converters", this.name], - "Using album art as video background", - ); + this.log("Using album art as video background"); return [ "-loop", "1", @@ -626,7 +589,7 @@ export class FFmpegConverter extends Converter { "output" + to, ]; } else { - log(["converters", this.name], "Using solid color background"); + this.log("Using solid color background"); return [ "-f", "lavfi", @@ -651,10 +614,7 @@ export class FFmpegConverter extends Converter { } // audio to audio - log( - ["converters", this.name], - `Converting audio ${input.from} to audio ${to}`, - ); + this.log(`Converting audio ${input.from} to audio ${to}`); const { audio: audioCodec } = getCodecs(to, isAlac); if (m4a && keepMetadata) m4aArgs = ["-c:v", "copy"]; // for album art @@ -688,10 +648,7 @@ export class FFmpegConverter extends Converter { "cover.jpg", ]) ) { - log( - ["converters", this.name], - "Successfully extracted album art from stream 0:1", - ); + this.log("Successfully extracted album art from stream 0:1"); return true; } @@ -708,17 +665,11 @@ export class FFmpegConverter extends Converter { "cover.jpg", ]) ) { - log( - ["converters", this.name], - "Successfully extracted album art (fallback method)", - ); + this.log("Successfully extracted album art (fallback method)"); return true; } - log( - ["converters", this.name], - "No album art found, will create solid color background", - ); + this.log("No album art found, will create solid color background"); return false; } diff --git a/src/lib/converters/magick.svelte.ts b/src/lib/converters/magick.svelte.ts index 17d4a573..29a18485 100644 --- a/src/lib/converters/magick.svelte.ts +++ b/src/lib/converters/magick.svelte.ts @@ -83,9 +83,16 @@ export class MagickConverter extends Converter { public readonly reportsProgress = false; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private log: (...msg: any[]) => void = () => {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private error: (...msg: any[]) => void = () => {}; + constructor() { super(); - log(["converters", this.name], `created converter`); + this.log = (msg) => log(["converters", this.name], msg); + this.error = (msg) => error(["converters", this.name], msg); + this.log(`created converter`); if (!browser) return; this.initializeWasm(); } @@ -104,10 +111,7 @@ export class MagickConverter extends Converter { this.status = "ready"; } catch (err) { this.status = "error"; - error( - ["converters", this.name], - `Failed to load ImageMagick WASM: ${err}`, - ); + this.error(`Failed to load ImageMagick WASM: ${err}`); ToastManager.add({ type: "error", @@ -209,7 +213,7 @@ export class MagickConverter extends Converter { // eslint-disable-next-line @typescript-eslint/no-explicit-any ...args: any[] ): Promise { - log(["converters", this.name], `converting ${input.name} to ${to}`); + this.log(`converting ${input.name} to ${to}`); // handle converting from SVG manually because magick-wasm doesn't support it if (input.from === ".svg") { @@ -222,10 +226,7 @@ export class MagickConverter extends Converter { if (to === ".png") return pngFile; // if target is png, return it directly return await this.convert(pngFile, to, settings, ...args); // otherwise, recursively convert png to user's target format } catch (err) { - error( - ["converters", this.name], - `SVG conversion failed: ${err}`, - ); + this.error(`SVG conversion failed: ${err}`); throw err; } } @@ -295,10 +296,7 @@ export class MagickConverter extends Converter { const res = await this.waitForMessage(worker); if (res.type === "finished") { - log( - ["converters", this.name], - `converted ${input.name} to ${to}`, - ); + this.log(`converted ${input.name} to ${to}`); return new VertFile( new File([res.output as unknown as BlobPart], input.name), res.zip ? ".zip" : to, @@ -319,17 +317,11 @@ export class MagickConverter extends Converter { public async cancel(input: VertFile): Promise { const worker = this.activeConversions.get(input.id); if (!worker) { - error( - ["converters", this.name], - `no active conversion found for file ${input.name}`, - ); + this.error(`no active conversion found for file ${input.name}`); return; } - log( - ["converters", this.name], - `cancelling conversion for file ${input.name}`, - ); + this.log(`cancelling conversion for file ${input.name}`); worker.terminate(); this.activeConversions.delete(input.id); @@ -366,7 +358,7 @@ export class MagickConverter extends Converter { } private async svgToImage(input: VertFile): Promise { - log(["converters", this.name], `converting SVG to image (PNG)`); + this.log(`converting SVG to image (PNG)`); const svgText = await input.file.text(); const svgBlob = new Blob([svgText], { type: "image/svg+xml" }); diff --git a/src/lib/converters/pandoc.svelte.ts b/src/lib/converters/pandoc.svelte.ts index e447088d..8d1f8691 100644 --- a/src/lib/converters/pandoc.svelte.ts +++ b/src/lib/converters/pandoc.svelte.ts @@ -13,8 +13,15 @@ export class PandocConverter extends Converter { private activeConversions = new Map(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private log: (...msg: any[]) => void = () => {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private error: (...msg: any[]) => void = () => {}; + constructor() { super(); + this.log = (msg) => log(["converters", this.name], msg); + this.error = (msg) => error(["converters", this.name], msg); if (!browser) return; (async () => { try { @@ -26,10 +33,7 @@ export class PandocConverter extends Converter { this.status = "ready"; } catch (err) { this.status = "error"; - error( - ["converters", this.name], - `Failed to load Pandoc worker: ${err}`, - ); + this.error(`Failed to load Pandoc worker: ${err}`); ToastManager.add({ type: "error", message: m["workers.errors.pandoc"](), @@ -113,17 +117,11 @@ export class PandocConverter extends Converter { public async cancel(input: VertFile): Promise { const worker = this.activeConversions.get(input.id); if (!worker) { - error( - ["converters", this.name], - `no active conversion found for file ${input.name}`, - ); + this.error(`no active conversion found for file ${input.name}`); return; } - log( - ["converters", this.name], - `cancelling conversion for file ${input.name}`, - ); + this.log(`cancelling conversion for file ${input.name}`); worker.terminate(); this.activeConversions.delete(input.id); diff --git a/src/lib/converters/vertd.svelte.ts b/src/lib/converters/vertd.svelte.ts index c8147617..672f0603 100644 --- a/src/lib/converters/vertd.svelte.ts +++ b/src/lib/converters/vertd.svelte.ts @@ -332,10 +332,13 @@ export class VertdConverter extends Converter { // eslint-disable-next-line @typescript-eslint/no-explicit-any private log: (...msg: any[]) => void = () => {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private error: (...msg: any[]) => void = () => {}; constructor() { super(); this.log = (msg) => log(["converters", this.name], msg); + this.error = (msg) => error(["converters", this.name], msg); this.log("created converter"); this.log("not rly sure how to implement this :P"); this.status = "ready"; @@ -712,10 +715,7 @@ export class VertdConverter extends Converter { `confirmed download for file ${input.name}`, ); } catch (e) { - error( - ["converters", this.name], - `failed to confirm download: ${e}`, - ); + this.error(`failed to confirm download: ${e}`); } resolve(new VertFile(new File([res], input.name), to)); @@ -754,10 +754,7 @@ export class VertdConverter extends Converter { public async cancel(input: VertFile): Promise { const activeConversion = this.activeConversions.get(input.id); if (!activeConversion) { - error( - ["converters", this.name], - `no active conversion found for file ${input.name}`, - ); + this.error(`no active conversion found for file ${input.name}`); return; } From d18fe388324a557125def2e5061c9d8d1543ffba Mon Sep 17 00:00:00 2001 From: Maya Date: Tue, 10 Mar 2026 21:40:17 +0300 Subject: [PATCH 055/118] feat: add "partially ready" status checks if one or the other of mediabunny/vertd is ready and shows "partially ready" --- messages/en.json | 1 + src/lib/converters/converter.svelte.ts | 2 +- src/lib/converters/mediabunny.svelte.ts | 9 +++++++++ src/routes/+page.svelte | 17 +++++++++++++++-- src/routes/convert/+page.svelte | 12 ------------ 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/messages/en.json b/messages/en.json index 73ce8d1f..de1c67d3 100644 --- a/messages/en.json +++ b/messages/en.json @@ -31,6 +31,7 @@ "status": { "text": "Status: {status}", "ready": "ready", + "partially_ready": "partially ready", "not_ready": "not ready", "not_initialized": "not initialized", "downloading": "downloading...", diff --git a/src/lib/converters/converter.svelte.ts b/src/lib/converters/converter.svelte.ts index 10313eba..76a23156 100644 --- a/src/lib/converters/converter.svelte.ts +++ b/src/lib/converters/converter.svelte.ts @@ -6,7 +6,7 @@ import type { SettingDefinition, } from "$lib/types/conversion-settings"; -export type WorkerStatus = "not-ready" | "downloading" | "ready" | "error"; +export type WorkerStatus = "not-ready" | "downloading" | "ready" | "partially-ready" | "error"; export class FormatInfo { public name: string; diff --git a/src/lib/converters/mediabunny.svelte.ts b/src/lib/converters/mediabunny.svelte.ts index d0171c0b..cf5a1a82 100644 --- a/src/lib/converters/mediabunny.svelte.ts +++ b/src/lib/converters/mediabunny.svelte.ts @@ -225,6 +225,15 @@ export class MediabunnyConverter extends Converter { // additional mediabunny coders // currently the official ones -- maybe add our own in the future this.initializeCodecs(); + + // don't know how to check if mediabunny is ready rn, maybe test smol conversion or run some other method? shrug + if (typeof Conversion === "undefined") { + this.status = "not-ready"; + this.error("Mediabunny failed to load"); + } else { + this.status = "ready"; + this.log("Mediabunny loaded successfully"); + } } private async initializeCodecs(): Promise { diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 954ad0df..90a669da 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -75,12 +75,23 @@ .filter((f) => f !== "none") .join(", "); + const mediabunnyStatus = converters.find( + (c) => c.name === "mediabunny", + )?.status; + const vertdReady = $vertdLoaded === true; + const mediabunnyReady = mediabunnyStatus === "ready"; + const videoStatus = + vertdReady && mediabunnyReady + ? "ready" + : vertdReady || mediabunnyReady + ? "partially-ready" + : "not-ready"; + output.Video = { formats, icon: Film, title: m["upload.cards.video"](), - // TODO: add "partial" state? somehow figure out diff between vertd and mediabunny - status: $vertdLoaded === true ? "ready" : "not-ready", // not using converter.status for this + status: videoStatus as WorkerStatus, }; } @@ -109,6 +120,8 @@ switch (status) { case "downloading": return m["upload.cards.status.downloading"](); + case "partially-ready": + return m["upload.cards.status.partially_ready"](); case "ready": return m["upload.cards.status.ready"](); default: diff --git a/src/routes/convert/+page.svelte b/src/routes/convert/+page.svelte index 533d1a34..d47f9831 100644 --- a/src/routes/convert/+page.svelte +++ b/src/routes/convert/+page.svelte @@ -15,7 +15,6 @@ files, gradientColor, showGradient, - vertdLoaded, dropdownStates, } from "$lib/store/index.svelte"; import { VertFile } from "$lib/types"; @@ -335,17 +334,6 @@ })}

- {:else if isVideo && !$vertdLoaded && !isAudio && !isImage && !isDocument} -
-

- {m["convert.errors.cant_convert"]()} -

-

- {m["convert.errors.vertd_not_found"]()} -

-
{:else}
Date: Wed, 11 Mar 2026 09:58:43 +0300 Subject: [PATCH 056/118] feat: generic server upload warning added a generic server upload warning, turning it into a toast w/ options instead of a dialog. fixed some hardcoded translations & tooltips --- messages/en.json | 10 ++-- .../functional/popups/FallbackToast.svelte | 6 +- .../popups/ServerUploadWarning.svelte | 47 +++++++++++++++ .../functional/popups/VertdError.svelte | 4 +- src/lib/components/visual/Tooltip.svelte | 4 +- src/lib/store/index.svelte.ts | 57 +++---------------- src/lib/types/file.svelte.ts | 52 ++++++++++++++++- 7 files changed, 120 insertions(+), 60 deletions(-) create mode 100644 src/lib/components/functional/popups/ServerUploadWarning.svelte diff --git a/messages/en.json b/messages/en.json index de1c67d3..92c73e96 100644 --- a/messages/en.json +++ b/messages/en.json @@ -44,7 +44,7 @@ "partial_support": "This format can only be converted as {direction}.", "direction_input": "input (from)", "direction_output": "output (to)", - "video_server_processing": "Video uploads to a server for processing by default, learn how to set it up locally here." + "video_server_processing": "When possible, VERT converts videos locally in your browser. Some conversions may require server-side processing, learn how to set it up locally here." } }, "convert": { @@ -62,9 +62,10 @@ "large_file_warning": "Due to browser / device limitations, video to audio conversion is disabled for this file as it is larger than {limit}GB. We recommend using Firefox or Safari for files of this size since they have less limitations.", "external_warning": { "title": "External server warning", - "text": "If you choose to convert into a video format, those files will be uploaded to an external server to be converted. Do you want to continue?", - "yes": "Yes", - "no": "No" + "text": "Converting {filename} with its current settings is not supported locally and requires server-side processing. Do you want to continue?", + "yes": "Convert on server", + "no": "Cancel conversion", + "dont_show_again": "Don't show again" }, "panel": { "convert_all": "Convert all", @@ -149,6 +150,7 @@ "errors": { "cant_convert": "We can't convert this file.", "converter_fallback": { + "title": "Change converter?", "body": "Conversion failed for {filename}. Would you like to try the next compatible converter {converter}?", "yes": "Next converter", "no": "Cancel conversion", diff --git a/src/lib/components/functional/popups/FallbackToast.svelte b/src/lib/components/functional/popups/FallbackToast.svelte index 3f27151e..a96afcc1 100644 --- a/src/lib/components/functional/popups/FallbackToast.svelte +++ b/src/lib/components/functional/popups/FallbackToast.svelte @@ -2,7 +2,7 @@ import { m } from "$lib/paraglide/messages"; type Additional = { - fileName: string; + filename: string; nextConverter: string; onNext: () => void | Promise; onCancel: () => void; @@ -10,13 +10,13 @@ let { additional }: { additional: Additional } = $props(); - export const title = "An error occurred"; + export const title = m["convert.errors.converter_fallback.title"]();

{m["convert.errors.converter_fallback.body"]({ - filename: additional.fileName, + filename: additional.filename, converter: additional.nextConverter, })}

diff --git a/src/lib/components/functional/popups/ServerUploadWarning.svelte b/src/lib/components/functional/popups/ServerUploadWarning.svelte new file mode 100644 index 00000000..86957a47 --- /dev/null +++ b/src/lib/components/functional/popups/ServerUploadWarning.svelte @@ -0,0 +1,47 @@ + + + + +
+

+ {m["convert.external_warning.text"]({ + filename: toast.additional.filename, + })} +

+
+ +
+ + +
+
+
diff --git a/src/lib/components/functional/popups/VertdError.svelte b/src/lib/components/functional/popups/VertdError.svelte index 43733fe5..f1e40031 100644 --- a/src/lib/components/functional/popups/VertdError.svelte +++ b/src/lib/components/functional/popups/VertdError.svelte @@ -5,7 +5,7 @@ from?: string; to?: string; errorMessage?: string; - fileName?: string; + filename?: string; } @@ -21,7 +21,7 @@ let submitting = $state(false); - export const title = "An error occurred"; + export const title = m["convert.errors.vertd_generic_title"](); const remove = () => { ToastManager.remove(toast.id); diff --git a/src/lib/components/visual/Tooltip.svelte b/src/lib/components/visual/Tooltip.svelte index 3ca218f7..493569a9 100644 --- a/src/lib/components/visual/Tooltip.svelte +++ b/src/lib/components/visual/Tooltip.svelte @@ -112,8 +112,8 @@