[
  {
    "path": ".dockerignore",
    "content": "node_modules/\nbuild/\ndist/\n.svelte-kit/\n.output/\n.vercel/\n.vscode/\n\nLICENSE\nREADME.md\nDockerfile\ndocker-compose.yml\n.npmrc\n.prettier*\n.gitignore\n.env.*\n.env\n\n.DS_Store\nThumbs.db"
  },
  {
    "path": ".env.example",
    "content": "# The hostname used for analytics tracking (currently only used by Plausible)\nPUB_HOSTNAME=localhost:5173\n\n# URL for your Plausible Analytics instance (leave empty to disable analytics)\nPUB_PLAUSIBLE_URL=https://plausible.example.com\n\n# Application environment: \"production\", \"development\", or \"nightly\"\nPUB_ENV=development\n\n# URL of the vertd daemon for video conversion (default: official VERT instance)\nPUB_VERTD_URL=https://vertd.vert.sh\n\n# Set to true to disable all external requests (vertd, Stripe, Plausible, etc.)\n# Useful for privacy-focused deployments or air-gapped environments\n# Note: the ffmpeg worker is still downloaded via a CDN (cdn.jsdelivr.net)\nPUB_DISABLE_ALL_EXTERNAL_REQUESTS=false\n\n# Set to true to disable blocking video conversions of an uploaded file when repeated failures\n# occur within an hour. Useful for local deployments where secure context (HTTPS) may not be\n# available - required for calculating file hashes of videos to block temporarily.\nPUB_DISABLE_FAILURE_BLOCKS=false\n\n# Stripe donation settings\n# Please keep these values the same, they support VERT's development!\nPUB_DONATION_URL=https://donations.vert.sh\nPUB_STRIPE_KEY=pk_live_51RDVmAGSxPVad6bQwzVNnbc28nlmzA30krLWk1fefCMpUPiSRPkavMMbGqa8A3lUaOCMlsUEVy2CWDYg0ip3aPpL00ZJlsMkf2"
  },
  {
    "path": ".github/workflows/docker.yml",
    "content": "name: Docker Image CI\n\non:\n    push:\n        branches: [\"main\"]\n        tags: [\"v*\"]\n        paths:\n            - \"src/**\"\n            - \"static/**\"\n            - \"Dockerfile\"\n            - \".dockerignore\"\n    pull_request:\n        branches: [\"main\"]\n        paths:\n            - \"src/**\"\n            - \"static/**\"\n            - \"Dockerfile\"\n            - \".dockerignore\"\n    workflow_dispatch:\n\njobs:\n    build-and-push:\n        runs-on: ubuntu-latest\n        permissions:\n            contents: read\n            packages: write\n\n        steps:\n            - uses: actions/checkout@v4\n\n            - name: Set up Docker Buildx\n              uses: docker/setup-buildx-action@v3\n\n            - name: Login to GitHub Container Registry\n              if: github.event_name != 'pull_request'\n              uses: docker/login-action@v3\n              with:\n                  registry: ghcr.io\n                  username: ${{ github.actor }}\n                  password: ${{ secrets.GITHUB_TOKEN }}\n\n            - name: Extract metadata\n              id: meta\n              uses: docker/metadata-action@v5\n              with:\n                  images: ghcr.io/${{ github.repository }}\n                  tags: |\n                      type=ref,event=branch\n                      type=ref,event=pr\n                      type=semver,pattern={{version}}\n                      type=semver,pattern={{major}}.{{minor}}\n                      type=sha,format=short\n                      type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}\n\n            - name: Build and push\n              uses: docker/build-push-action@v5\n              with:\n                  context: .\n                  push: ${{ github.event_name != 'pull_request' }}\n                  platforms: linux/amd64,linux/arm64\n                  tags: ${{ steps.meta.outputs.tags }}\n                  labels: ${{ steps.meta.outputs.labels }}\n                  cache-from: type=gha\n                  cache-to: type=gha,mode=max\n                  build-args: |\n                      PUB_ENV=production\n                      PUB_HOSTNAME=${{ vars.PUB_HOSTNAME || '' }}\n                      PUB_PLAUSIBLE_URL=${{ vars.PUB_PLAUSIBLE_URL || '' }}\n                      PUB_VERTD_URL=https://vertd.vert.sh\n                      PUB_DISABLE_ALL_EXTERNAL_REQUESTS=false\n                      PUB_DONATION_URL=https://donations.vert.sh\n                      PUB_STRIPE_KEY=pk_live_51RDVmAGSxPVad6bQwzVNnbc28nlmzA30krLWk1fefCMpUPiSRPkavMMbGqa8A3lUaOCMlsUEVy2CWDYg0ip3aPpL00ZJlsMkf2\n"
  },
  {
    "path": ".github/workflows/pages.yml",
    "content": "name: Deploy to GitHub Pages\n\non:\n    push:\n        branches: \"main\"\n\njobs:\n    build_site:\n        runs-on: ubuntu-latest\n        steps:\n            - name: Checkout\n              uses: actions/checkout@v4\n\n            - name: Install Bun\n              uses: oven-sh/setup-bun@v2\n\n            - name: Install dependencies\n              run: bun i\n\n            - name: build\n              env:\n                  BASE_PATH: \"/${{ github.event.repository.name }}\"\n                  PUB_HOSTNAME: \"vert.sh\"\n                  PUB_PLAUSIBLE_URL: \"https://ats.vert.sh\"\n                  PUB_ENV: \"production\"\n                  PUB_VERTD_URL: \"https://vertd.vert.sh\"\n                  PUB_DISABLE_ALL_EXTERNAL_REQUESTS: \"false\"\n                  PUB_DISABLE_FAILURE_BLOCKS: \"false\"\n                  PUB_DONATION_URL: \"https://donations.vert.sh\"\n                  PUB_STRIPE_KEY: \"pk_live_51RDVmAGSxPVad6bQwzVNnbc28nlmzA30krLWk1fefCMpUPiSRPkavMMbGqa8A3lUaOCMlsUEVy2CWDYg0ip3aPpL00ZJlsMkf2\"\n              run: bun run build\n\n            - name: Upload Artifacts\n              uses: actions/upload-pages-artifact@v3\n              with:\n                  path: \"build/\"\n\n    deploy:\n        needs: build_site\n        runs-on: ubuntu-latest\n\n        permissions:\n            pages: write\n            id-token: write\n\n        environment:\n            name: github-pages\n            url: ${{ steps.deployment.outputs.page_url }}\n\n        steps:\n            - name: Deploy\n              id: deployment\n              uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n\n# Output\n.output\n.vercel\n/.svelte-kit\n/build\n\n# OS\n.DS_Store\nThumbs.db\n\n# Env\n.env\n.env.*\n!.env.example\n!.env.test\n\n# Vite\nvite.config.js.timestamp-*\nvite.config.ts.timestamp-*\n\n# IDE\n.idea\n"
  },
  {
    "path": ".npmignore",
    "content": "src/routes\nsrc/app.d.ts\nsrc/app.html"
  },
  {
    "path": ".prettierignore",
    "content": "# Package Managers\npackage-lock.json\npnpm-lock.yaml\nyarn.lock\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n\t\"useTabs\": true,\n\t\"tabWidth\": 4,\n\t\"singleQuote\": false,\n\t\"plugins\": [\"prettier-plugin-svelte\"],\n    \"overrides\": [{ \"files\": \"*.svelte\", \"options\": { \"parser\": \"svelte\" } }]\n}\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"inlang.vs-code-extension\"\n  ]\n}"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n\t\"css.customData\": [\".vscode/tailwind.json\"]\n}\n"
  },
  {
    "path": ".vscode/tailwind.json",
    "content": "{\n\t\"version\": 1.1,\n\t\"atDirectives\": [\n\t\t{\n\t\t\t\"name\": \"@tailwind\",\n\t\t\t\"description\": \"Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.\",\n\t\t\t\"references\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"Tailwind Documentation\",\n\t\t\t\t\t\"url\": \"https://tailwindcss.com/docs/functions-and-directives#tailwind\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\": \"@apply\",\n\t\t\t\"description\": \"Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.\",\n\t\t\t\"references\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"Tailwind Documentation\",\n\t\t\t\t\t\"url\": \"https://tailwindcss.com/docs/functions-and-directives#apply\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\": \"@responsive\",\n\t\t\t\"description\": \"You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\\n```css\\n@responsive {\\n  .alert {\\n    background-color: #E53E3E;\\n  }\\n}\\n```\\n\",\n\t\t\t\"references\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"Tailwind Documentation\",\n\t\t\t\t\t\"url\": \"https://tailwindcss.com/docs/functions-and-directives#responsive\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\": \"@screen\",\n\t\t\t\"description\": \"The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\\n```css\\n@screen sm {\\n  /* ... */\\n}\\n```\\n…gets transformed into this:\\n```css\\n@media (min-width: 640px) {\\n  /* ... */\\n}\\n```\\n\",\n\t\t\t\"references\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"Tailwind Documentation\",\n\t\t\t\t\t\"url\": \"https://tailwindcss.com/docs/functions-and-directives#screen\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\": \"@variants\",\n\t\t\t\"description\": \"Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\\n```css\\n@variants hover, focus {\\n   .btn-brand {\\n    background-color: #3182CE;\\n  }\\n}\\n```\\n\",\n\t\t\t\"references\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"Tailwind Documentation\",\n\t\t\t\t\t\"url\": \"https://tailwindcss.com/docs/functions-and-directives#variants\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM oven/bun AS builder\n\nWORKDIR /app\n\nARG PUB_ENV\nARG PUB_HOSTNAME\nARG PUB_PLAUSIBLE_URL\nARG PUB_VERTD_URL\nARG PUB_DISABLE_ALL_EXTERNAL_REQUESTS\nARG PUB_DONATION_URL\nARG PUB_STRIPE_KEY\nARG PUB_DISABLE_FAILURE_BLOCKS=false\n\nENV PUB_ENV=${PUB_ENV}\nENV PUB_HOSTNAME=${PUB_HOSTNAME}\nENV PUB_PLAUSIBLE_URL=${PUB_PLAUSIBLE_URL}\nENV PUB_VERTD_URL=${PUB_VERTD_URL}\nENV PUB_DISABLE_ALL_EXTERNAL_REQUESTS=${PUB_DISABLE_ALL_EXTERNAL_REQUESTS}\nENV PUB_DONATION_URL=${PUB_DONATION_URL}\nENV PUB_STRIPE_KEY=${PUB_STRIPE_KEY}\nENV PUB_DISABLE_FAILURE_BLOCKS=${PUB_DISABLE_FAILURE_BLOCKS}\n\nCOPY package.json ./\n\nRUN apt-get update && \\\n\tapt-get install -y --no-install-recommends git && \\\n\trm -rf /var/lib/apt/lists/*\n\nRUN bun install\n\nCOPY . ./\n\nRUN bun run build\n\nFROM nginx:stable-alpine\n\nEXPOSE 80/tcp\n\nCOPY ./nginx/default.conf /etc/nginx/conf.d/default.conf\n\nCOPY --from=builder /app/build /usr/share/nginx/html\n\nHEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\\n\tCMD curl --fail --silent --output /dev/null http://localhost || exit 1\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published\n    by the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <img src=\"https://github.com/user-attachments/assets/bf441748-0ec5-4c8a-b3e5-11301ee3f0bd\" alt=\"VERT's logo\" height=\"100\">\n</p>\n<h1 align=\"center\"><a href=\"https://vert.sh\">VERT.sh</a></h1>\n\nVERT is a file conversion utility that uses WebAssembly to convert files on your device instead of a cloud. Check out the live instance at [vert.sh](https://vert.sh).\n\nVERT is built in Svelte and TypeScript.\n\n## Screenshots\n\n|                     Upload page                      |                     Conversion page                      |\n| :--------------------------------------------------: | :------------------------------------------------------: |\n| ![VERT upload page](docs/images/screenshot-home.png) | ![VERT convert page](docs/images/screenshot-convert.png) |\n\n## Features\n\n- Convert files directly on your device using WebAssembly\\*\n- No file or file size limits\n- Convert images, audio, documents, and video\\*\n- Supports over **250+** file formats\n- Conversion settings\n- User-friendly interface built with Svelte\n\n<sup>\\* Non-local video conversion is available with our official instance, but the [daemon](https://github.com/VERT-sh/vertd) is easily self-hostable to maintain privacy and fully local functionality.</sup>\n\n## Documentation\n\n- [FAQ](./docs/FAQ.md)\n- [Getting Started](./docs/GETTING_STARTED.md)\n- [Using Docker](./docs/DOCKER.md)\n- [Video Conversion](./docs/VIDEO_CONVERSION.md)\n\n## License\n\nThis project is licensed under the AGPL-3.0 License, please see the [LICENSE](LICENSE) file for details.\n\n## Star History\n\n<a href=\"https://www.star-history.com/#VERT-sh/VERT&Date\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=VERT-sh/VERT&type=Date&theme=dark\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/svg?repos=VERT-sh/VERT&type=Date\" />\n   <img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=VERT-sh/VERT&type=Date\" />\n </picture>\n</a>\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  vert:\n    container_name: vert\n    image: ghcr.io/vert-sh/vert:latest\n    build:\n      context: .\n      args:\n        PUB_HOSTNAME: ${PUB_HOSTNAME:-localhost:5173}\n        PUB_PLAUSIBLE_URL: ${PUB_PLAUSIBLE_URL:-}\n        PUB_ENV: ${PUB_ENV:-production}\n        PUB_DISABLE_ALL_EXTERNAL_REQUESTS: ${PUB_DISABLE_ALL_EXTERNAL_REQUESTS:-false}\n        PUB_VERTD_URL: ${PUB_VERTD_URL:-}\n        PUB_DONATION_URL: ${PUB_DONATION_URL:-https://donations.vert.sh}\n        PUB_STRIPE_KEY: ${PUB_STRIPE_KEY:-pk_live_51RDVmAGSxPVad6bQwzVNnbc28nlmzA30krLWk1fefCMpUPiSRPkavMMbGqa8A3lUaOCMlsUEVy2CWDYg0ip3aPpL00ZJlsMkf2}\n    restart: unless-stopped\n    ports:\n      - ${PORT:-3000}:80\n"
  },
  {
    "path": "docs/DOCKER.md",
    "content": "## Using Docker\n\nThis file covers how to run VERT under a Docker container.\n\n- [Manually building the image](#manually-building-the-image)\n- [Using an image from the GitHub Container Registry](#using-an-image-from-the-github-container-registry)\n\n### Manually building the image\n\nFirst, clone the repository:\n\n```shell\ngit clone https://github.com/VERT-sh/VERT\ncd VERT/\n```\n\nThen build a Docker image with:\n\n```shell\ndocker build -t vert-sh/vert \\\n    --build-arg PUB_ENV=production \\\n    --build-arg PUB_HOSTNAME=vert.sh \\\n    --build-arg PUB_PLAUSIBLE_URL=https://plausible.example.com \\\n    --build-arg PUB_VERTD_URL=https://vertd.vert.sh \\\n    --build-arg PUB_DONATION_URL=https://donations.vert.sh \\\n\t--build-arg PUB_DISABLE_ALL_EXTERNAL_REQUESTS=false \\\n    --build-arg PUB_STRIPE_KEY=\"\" .\n```\n\nYou can then run it by using:\n\n```shell\ndocker run -d \\\n    --restart unless-stopped \\\n    -p 3000:80 \\\n    --name \"vert\" \\\n    vert-sh/vert\n```\n\nThis will do the following:\n\n- Use the previously built image as the container `vert`, in detached mode\n- Continuously restart the container until manually stopped\n- Map `3000/tcp` (host) to `80/tcp` (container)\n\nWe also have a [`docker-compose.yml`](/docker-compose.yml) file available. Use `docker compose up` if you want to start the stack, or `docker compose down` to bring it down. You can pass `--build` to `docker compose up` to rebuild the Docker image (useful if you've changed any of the environment variables) as well as `-d` to start it in detached mode. You can read more about Docker Compose in general [here](https://docs.docker.com/compose/intro/compose-application-model/).\n\n### Using an image from the GitHub Container Registry\n\nWhile there's an image you can pull instead of cloning the repo and building the image yourself, you will not be able to update any of the environment variables (e.g. `PUB_PLAUSIBLE_URL`) as they're baked directly into the image and not obtained during runtime. If you're okay with this, you can simply run this command instead:\n\n```shell\ndocker run -d \\\n    --restart unless-stopped \\\n    -p 3000:80 \\\n    --name \"vert\" \\\n    ghcr.io/vert-sh/vert:latest\n```\n"
  },
  {
    "path": "docs/FAQ.md",
    "content": "## FAQ\n\nThis file covers frequently asked questions.\n\n- [Why VERT?](#why-vert)\n- [What happens with video files?](#what-happens-with-video-files)\n- [Can I host my own video file converter?](#can-i-host-my-own-video-file-converter)\n- [What about analytics?](#what-about-analytics)\n- [What libraries does VERT use?](#what-libraries-does-vert-use)\n- [Is it possible to fully prevent VERT from making requests to external services?](#is-it-possible-to-fully-prevent-vert-from-making-requests-to-external-services)\n\n### Why VERT?\n\n**File converters have always disappointed us.** They're ugly, riddled with ads, and most importantly; slow. We decided to solve this problem once and for all by making an alternative that solves all those problems, and more.\n\nAll non-video files are converted completely on-device; this means that there's no delay between sending and receiving the files from a server, and we never get to snoop on the files you convert.\n\n### What happens with video files?\n\nVideo files get uploaded to our lightning-fast RTX 4000 Ada server. Your videos stay on there for an hour if you do not convert them. If you do convert the file, the video will stay on the server for an hour, or until it is downloaded. The file will then be deleted from our server.\n\n### Can I host my own video file converter?\n\nYes. Check out the [Video Conversion](./VIDEO_CONVERSION.md) page.\n\n### What about analytics?\n\nWe use [Plausible](https://plausible.io/privacy-focused-web-analytics), a privacy-focused analytics tool, to gather completely anonymous statistics. All data is anonymized and aggregated, and no identifiable information is ever sent or stored. You can view the analytics [here](https://ats.vert.sh/vert.sh) and choose to opt out in the [Settings](https://vert.sh/settings/) page.\n\n### Is it possible to fully prevent VERT from making requests to external services?\n\nYes! If you would prefer VERT to not make any requests to external services (video conversion, analytics, among others), you can set the `PUB_DISABLE_ALL_EXTERNAL_REQUESTS` environment variable to `true` **during build time**.\n\nThe only external request VERT will make with this option is to `cdn.jsdelivr.net`, which is used to download FFmpeg's WebAssembly build.\n\n### What libraries does VERT use?\nVERT uses FFmpeg for audio and video conversion, imagemagick for images and Pandoc for documents. A big thanks to them for maintaining such excellent libraries for so many years."
  },
  {
    "path": "docs/GETTING_STARTED.md",
    "content": "## Getting Started\n\nThis file covers how to get started with VERT.\n\n- [Prerequisites](#prerequisites)\n- [Installation](#installation)\n- [Running Locally](#running-locally)\n- [Building for Production](#building-for-production)\n- [Using Docker](#using-docker)\n\n### Prerequisites\n\nMake sure you have the following installed:\n- [Bun](https://bun.sh/)\n\n### Installation\n\nFirst, clone the repository:\n```sh\ngit clone https://github.com/VERT-sh/VERT\ncd VERT/\n```\n\nInstall dependencies:\n```sh\nbun i\n```\n\nAnd finally, make sure you create a `.env` file in the root of the project. We've included a [`.env.example`](../.env.example) file which you can use to get started.\n\n### Running Locally\n\nTo run the project locally, run `bun dev`.\n\nThis will start a development server. Open your browser and navigate to `http://localhost:5173` to see the application.\n\n### Building for Production\n\nTo build the project for production, run `bun run build`.\n\nThis will build the site to the `build` folder. You should then use a web server like [nginx](https://nginx.org) to serve the files inside that folder.\n\n### Using Docker\n\nCheck the dedicated [Docker](./DOCKER.md) page."
  },
  {
    "path": "docs/VIDEO_CONVERSION.md",
    "content": "## Video conversion\n\nThis file covers how video conversion works when using VERT.\n\nOn VERT, video uploads to a server for processing by default. This is because video conversion is hard to do in a browser as it uses a lot of resources, and will end up running very slowly (if it even works at all).\n\nOur answer to this is [`vertd`](https://github.com/VERT-sh/vertd), which is a simple FFmpeg wrapper built in Rust. If you don't understand all that technical jargon, it basically allows you to convert videos using the full capacity of your computer, which results in much faster conversion. It runs on your computer (or a server somewhere, if you know what you're doing), and the VERT web interface reaches out to it in order to convert your videos.\n\nWe host an official instance of [`vertd`](https://github.com/VERT-sh/vertd) so you do not have to host it yourself for convenience, but considering you're here, you probably want to host it for yourself. Essentially:\n\n- Download the latest release of `vertd` for your machine [here](https://github.com/VERT-sh/vertd/releases)\n- Run the server\n- Connect the VERT UI to your local `vertd` instance by entering its IP & port\n    - By default, `vertd` runs a HTTP server on port `24153`, so you would put `http://localhost:24153` in the \"Instance URL\" setting found in VERT's settings (assuming you are running it on your own PC)\n"
  },
  {
    "path": "eslint.config.js",
    "content": "import prettier from 'eslint-config-prettier';\nimport js from '@eslint/js';\nimport svelte from 'eslint-plugin-svelte';\nimport globals from 'globals';\nimport ts from 'typescript-eslint';\n\nexport default ts.config(\n\tjs.configs.recommended,\n\t...ts.configs.recommended,\n\t...svelte.configs['flat/recommended'],\n\tprettier,\n\t...svelte.configs['flat/prettier'],\n\t{\n\t\tlanguageOptions: {\n\t\t\tglobals: {\n\t\t\t\t...globals.browser,\n\t\t\t\t...globals.node\n\t\t\t}\n\t\t}\n\t},\n\t{\n\t\tfiles: ['**/*.svelte'],\n\n\t\tlanguageOptions: {\n\t\t\tparserOptions: {\n\t\t\t\tparser: ts.parser\n\t\t\t}\n\t\t}\n\t},\n\t{\n\t\tignores: ['build/', '.svelte-kit/', 'dist/']\n\t}\n);\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  description = \"VERT.sh\";\n\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs?ref=nixos-unstable\";\n    flake-utils.url = \"github:numtide/flake-utils\";\n  };\n\n    outputs = { self, nixpkgs, flake-utils }:\n    flake-utils.lib.eachDefaultSystem (system:\n      let\n        pkgs = nixpkgs.legacyPackages.${system};\n      in\n      {\n        devShells.default = pkgs.mkShell {\n          buildInputs = with pkgs; [\n            bun\n            nodejs\n\n            # are these needed?\n            nodePackages.prettier\n            nodePackages.eslint\n          ];\n        };\n      });\n}\n"
  },
  {
    "path": "messages/ba.json",
    "content": "{\n  \"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n  \"navbar\": {\n    \"upload\": \"Učitaj\",\n    \"convert\": \"Konvertuj\",\n    \"settings\": \"Postavke\",\n    \"about\": \"O nama\",\n    \"toggle_theme\": \"Promijeni temu\"\n  },\n  \"footer\": {\n    \"copyright\": \"© {year} VERT.\",\n    \"source_code\": \"Izvorni kod\",\n    \"discord_server\": \"Discord server\",\n    \"privacy_policy\": \"Politika privatnosti\"\n  },\n  \"upload\": {\n    \"title\": \"Konverter datoteka koji ćete voljeti.\",\n    \"subtitle\": \"Sva obrada slika, zvuka i dokumenata obavlja se na vašem uređaju. Video zapisi se konvertuju na našim izuzetno brzim serverima. Bez ograničenja veličine, bez reklama i potpuno otvorenog koda.\",\n    \"uploader\": {\n      \"text\": \"Prevucite ili kliknite da {action}\",\n      \"convert\": \"konvertujete\"\n    },\n    \"cards\": {\n      \"title\": \"VERT podržava...\",\n      \"images\": \"Slike\",\n      \"audio\": \"Audio\",\n      \"documents\": \"Dokumente\",\n      \"video\": \"Video\",\n      \"video_server_processing\": \"Server podržava\",\n      \"local_supported\": \"Lokalno podržano\",\n      \"status\": {\n        \"text\": \"<b>Status:</b> {status}\",\n        \"ready\": \"spreman\",\n        \"not_ready\": \"nije spreman\",\n        \"not_initialized\": \"nije inicijaliziran\",\n        \"downloading\": \"preuzimam...\",\n        \"initializing\": \"inicijaliziram...\",\n        \"unknown\": \"nepoznat status\"\n      },\n      \"supported_formats\": \"Podržani formati:\"\n    },\n    \"tooltip\": {\n      \"partial_support\": \"Ovaj format može biti konvertovan samo kao {direction}.\",\n      \"direction_input\": \"ulazni (iz)\",\n      \"direction_output\": \"izlazni (u)\",\n      \"video_server_processing\": \"Video se podrazumijevano otprema na server radi obrade, ovdje možete naučiti kako to postaviti lokalno.\"\n    }\n  },\n  \"convert\": {\n    \"archive_file\": {\n      \"extract\": \"Raspakuj arhivu\",\n      \"extracting\": \"Otkrivena arhiva: {filename}\",\n      \"extracted\": \"Izvučeno {extract_count} datoteka iz {filename}. {ignore_count} stavki je ignorisano.\",\n      \"detected\": \"Otkrivene {type} datoteke u {filename}.\",\n      \"audio\": \"audio\",\n      \"video\": \"video\",\n      \"doc\": \"dokument\",\n      \"image\": \"slika\",\n      \"extract_error\": \"Greška pri raspakivanju {filename}: {error}\"\n    },\n    \"large_file_warning\": \"Zbog ograničenja preglednika/uređaja, konverzija videa u audio je onemogućena za ovu datoteku jer je veća od {limit}GB. Preporučujemo Firefox ili Safari za datoteke ove veličine jer imaju manje ograničenja.\",\n    \"external_warning\": {\n      \"title\": \"Upozorenje o vanjskom serveru\",\n      \"text\": \"Ako odaberete konverziju u video format, te datoteke će biti otpremljene na vanjski server. Želite li nastaviti?\",\n      \"yes\": \"Da\",\n      \"no\": \"Ne\"\n    },\n    \"panel\": {\n      \"convert_all\": \"Konvertuj sve\",\n      \"download_all\": \"Preuzmi sve kao .zip\",\n      \"remove_all\": \"Ukloni sve datoteke\",\n      \"set_all_to\": \"Postavi sve na\",\n      \"na\": \"N/A\"\n    },\n    \"dropdown\": {\n      \"audio\": \"Audio\",\n      \"video\": \"Video\",\n      \"doc\": \"Dokument\",\n      \"image\": \"Slika\",\n      \"placeholder\": \"Pretraži format\",\n      \"no_formats\": \"Nema dostupnih formata\",\n      \"no_results\": \"Nema rezultata koji odgovaraju pretrazi\"\n    },\n    \"tooltips\": {\n      \"unknown_file\": \"Nepoznat tip datoteke\",\n      \"audio_file\": \"Audio datoteka\",\n      \"video_file\": \"Video datoteka\",\n      \"document_file\": \"Dokument\",\n      \"image_file\": \"Slika\",\n      \"convert_file\": \"Konvertuj ovu datoteku\",\n      \"download_file\": \"Preuzmi ovu datoteku\"\n    },\n    \"errors\": {\n      \"cant_convert\": \"Ne možemo konvertovati ovu datoteku.\",\n      \"vertd_server\": \"šta to radiš..? treba da pokreneš vertd server!\",\n      \"vertd_generic_view\": \"Prikaži detalje greške\",\n      \"vertd_generic_body\": \"Došlo je do greške prilikom pokušaja konverzije videa. Želite li poslati svoj video programerima da pomognete u rješavanju problema? Samo će vaš video biti poslan. Nikakvi identifikatori neće biti otpremljeni.\",\n      \"vertd_generic_title\": \"Greška pri konverziji videa\",\n      \"vertd_generic_yes\": \"Pošalji video\",\n      \"vertd_generic_no\": \"Ne šalji\",\n      \"vertd_failed_to_keep\": \"Neuspjelo čuvanje videa na serveru: {error}\",\n      \"vertd_details\": \"Prikaži detalje greške\",\n      \"vertd_details_body\": \"Ako pritisnete pošalji, <b>vaš video će također biti priložen</b> uz log greške koji se uvijek automatski šalje nama na pregled. Sljedeće informacije su log koji automatski dobijamo:\",\n      \"vertd_details_footer\": \"Ove informacije se koriste isključivo za rješavanje problema i nikada neće biti dijeljene. Pogledajte našu [privacy_link]politiku privatnosti[/privacy_link] za više detalja.\",\n      \"vertd_details_job_id\": \"<b>ID zadatka:</b> {jobId}\",\n      \"vertd_details_from\": \"<b>Iz formata:</b> {from}\",\n      \"vertd_details_to\": \"<b>U format:</b> {to}\",\n      \"vertd_details_error_message\": \"<b>Poruka greške:</b> [view_link]Pogledaj log[/view_link]\",\n      \"vertd_details_close\": \"Zatvori\",\n      \"vertd_ratelimit\": \"Vaš video '{filename}' nije uspio biti konvertovan nekoliko puta. Kako bismo spriječili preopterećenje servera, dalji pokušaji konverzije za ovu datoteku su privremeno blokirani.\",\n      \"unsupported_format\": \"Podržane su samo slike, video, audio i dokumenti\",\n      \"format_output_only\": \"Ovaj format se trenutno može koristiti samo kao izlaz, ne kao ulaz.\",\n      \"vertd_not_found\": \"Nije moguće pronaći vertd instancu za pokretanje konverzije videa. Da li je URL ispravno postavljen?\",\n      \"worker_downloading\": \"{type} konverter se trenutno inicijalizira, molimo sačekajte.\",\n      \"worker_error\": \"{type} konverter je imao grešku tokom inicijalizacije, pokušajte kasnije ponovo.\",\n      \"worker_timeout\": \"{type} konverteru treba duže nego očekivano da se inicijalizira, molimo sačekajte još malo ili osvježite stranicu.\",\n      \"audio\": \"audio\",\n      \"doc\": \"dokument\",\n      \"image\": \"slika\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Postavke\",\n    \"errors\": {\n      \"save_failed\": \"Neuspješno spremanje postavki!\"\n    },\n    \"appearance\": {\n      \"title\": \"Izgled\",\n      \"brightness_theme\": \"Tema osvjetljenja\",\n      \"brightness_description\": \"Želite li blještavi dan ili tihu, usamljenu noć?\",\n      \"light\": \"Svijetla\",\n      \"dark\": \"Tamna\",\n      \"effect_settings\": \"Efekti\",\n      \"effect_description\": \"Želite li zanimljive efekte ili mirnije iskustvo?\",\n      \"enable\": \"Uključi\",\n      \"disable\": \"Isključi\"\n    },\n    \"conversion\": {\n      \"title\": \"Konverzija\",\n      \"advanced_settings\": \"Napredne postavke\",\n      \"filename_format\": \"Format imena datoteke\",\n      \"filename_description\": \"Ovo određuje ime datoteke pri preuzimanju, <b>bez ekstenzije</b>. Možete koristiti sljedeće šablone: <b>%name%</b> originalno ime, <b>%extension%</b> originalna ekstenzija, <b>%date%</b> datum konverzije.\",\n      \"placeholder\": \"VERT_%name%\",\n      \"default_format\": \"Podrazumijevani format konverzije\",\n      \"default_format_enable\": \"Uključi\",\n      \"default_format_disable\": \"Isključi\",\n      \"default_format_description\": \"Ovo mijenja podrazumijevani format koji se odabere kada učitate datoteku ovog tipa.\",\n      \"default_format_image\": \"Slike\",\n      \"default_format_video\": \"Video\",\n      \"default_format_audio\": \"Audio\",\n      \"default_format_document\": \"Dokumenti\",\n      \"metadata\": \"Metadata\",\n      \"metadata_description\": \"Određuje da li se podaci (EXIF, info o pjesmi itd.) čuvaju u konvertovanim datotekama.\",\n      \"keep\": \"Zadrži\",\n      \"remove\": \"Ukloni\",\n      \"quality\": \"Kvalitet konverzije\",\n      \"quality_description\": \"Mijenja podrazumijevani kvalitet izlazne datoteke. Veće vrijednosti znače duže vrijeme konverzije i veću veličinu.\",\n      \"quality_video\": \"Mijenja izlazni kvalitet videa.\",\n      \"quality_audio\": \"Audio (kbps)\",\n      \"quality_images\": \"Slika (%)\",\n      \"rate\": \"Sample rate (Hz)\"\n    },\n    \"vertd\": {\n      \"title\": \"Konverzija videa\",\n      \"status\": \"status:\",\n      \"loading\": \"učitavam...\",\n      \"available\": \"dostupan, commit id {commitId}\",\n      \"unavailable\": \"nedostupan (da li je URL tačan?)\",\n      \"description\": \"<code>vertd</code> je serverski omotač za FFmpeg, omogućava brzo konvertovanje videa koristeći vaš GPU putem VERT web interfejsa.\",\n      \"hosting_info\": \"Imamo javnu instancu radi praktičnosti, ali možete lako hostati svoju. Preuzmite server [vertd_link]ovdje[/vertd_link].\",\n      \"instance\": \"Instanca\",\n      \"url_placeholder\": \"Primjer: http://localhost:24153\",\n      \"conversion_speed\": \"Brzina konverzije\",\n      \"speed_description\": \"Opisuje odnos između brzine i kvaliteta. Brže = niži kvalitet ali kraće vrijeme.\",\n      \"speeds\": {\n        \"very_slow\": \"Vrlo sporo\",\n        \"slower\": \"Sporije\",\n        \"slow\": \"Sporo\",\n        \"medium\": \"Srednje\",\n        \"fast\": \"Brzo\",\n        \"ultra_fast\": \"Ultra brzo\"\n      },\n      \"auto_instance\": \"Auto (preporučeno)\",\n      \"eu_instance\": \"Falkenstein, Njemačka\",\n      \"us_instance\": \"Washington, SAD\",\n      \"custom_instance\": \"Prilagođeno\"\n    },\n    \"privacy\": {\n      \"title\": \"Privatnost i podaci\",\n      \"plausible_title\": \"Plausible analitika\",\n      \"plausible_description\": \"Koristimo [plausible_link]Plausible[/plausible_link], alat fokusiran na privatnost. Podaci su potpuno anonimni i agregirani. Analitiku možete vidjeti [analytics_link]ovdje[/analytics_link] i isključiti ispod.\",\n      \"opt_in\": \"Uključi\",\n      \"opt_out\": \"Isključi\",\n      \"cache_title\": \"Upravljanje cacheom\",\n      \"cache_description\": \"Konverter se kešira u vašem pregledniku radi boljih performansi.\",\n      \"refresh_cache\": \"Osvježi cache\",\n      \"clear_cache\": \"Obriši cache\",\n      \"files_cached\": \"{size} ({count} datoteka)\",\n      \"loading_cache\": \"Učitavam...\",\n      \"total_size\": \"Ukupna veličina\",\n      \"files_cached_label\": \"Keširane datoteke\",\n      \"cache_cleared\": \"Cache uspješno obrisan!\",\n      \"cache_clear_error\": \"Neuspješno brisanje cachea.\",\n      \"site_data_title\": \"Upravljanje podacima stranice\",\n      \"site_data_description\": \"Obriši sve podatke stranice uključujući postavke i cache i resetuj VERT.\",\n      \"clear_all_data\": \"Obriši sve podatke\",\n      \"clear_all_data_confirm_title\": \"Obrisati sve podatke stranice?\",\n      \"clear_all_data_confirm\": \"Resetovat će sve postavke i cache i osvježiti stranicu. Ova akcija je nepovratna.\",\n      \"clear_all_data_cancel\": \"Otkaži\",\n      \"all_data_cleared\": \"Svi podaci obrisani! Osvježavam stranicu...\",\n      \"all_data_clear_error\": \"Neuspješno brisanje svih podataka.\"\n    },\n    \"language\": {\n      \"title\": \"Jezik\",\n      \"description\": \"Odaberite željeni jezik VERT interfejsa.\"\n    }\n  },\n  \"about\": {\n    \"title\": \"O nama\",\n    \"why\": {\n      \"title\": \"Zašto VERT?\",\n      \"description\": \"<b>Konverteri datoteka su nas uvijek razočaravali.</b> Ružni su, puni reklama i, najvažnije, spori. Odlučili smo to riješiti jednom zauvijek.<br/><br/>Sve ne-video datoteke se obrađuju lokalno, što znači da nema slanja datoteka na server — i mi nikad ne vidimo vaše podatke.<br/><br/>Video se otprema na naš brzi RTX 4000 Ada server. Vaši video snimci ostaju tamo sat vremena ako ih ne konvertujete. Ako ih konvertujete, ostaju sat ili dok ih preuzmete, nakon čega se brišu.\"\n    },\n    \"sponsors\": {\n      \"title\": \"Sponzori\",\n      \"description\": \"Želite nas podržati? Kontaktirajte nekog od developera na [discord_link]Discordu[/discord_link] ili pošaljite email na\",\n      \"email_copied\": \"Email kopiran!\"\n    },\n    \"resources\": {\n      \"title\": \"Resursi\",\n      \"discord\": \"Discord\",\n      \"source\": \"Izvor\",\n      \"email\": \"Email\"\n    },\n    \"donate\": {\n      \"title\": \"Donirajte VERT-u\",\n      \"description\": \"Vaša podrška pomaže da nastavimo razvijati i unapređivati VERT.\",\n      \"one_time\": \"Jednokratno\",\n      \"monthly\": \"Mjesečno\",\n      \"custom\": \"Prilagođeno\",\n      \"pay_now\": \"Plati sada\",\n      \"donate_amount\": \"Doniraj ${amount} USD\",\n      \"thank_you\": \"Hvala na donaciji!\",\n      \"payment_failed\": \"Plaćanje nije uspjelo: {message}{period}. Novac nije skinut s vašeg računa.\",\n      \"donation_error\": \"Došlo je do greške pri obradi donacije. Pokušajte ponovo kasnije.\",\n      \"payment_error\": \"Greška pri dohvaćanju podataka o plaćanju. Pokušajte ponovo.\",\n      \"donation_notice_official\": \"Donacije ovdje idu za zvaničnu VERT instancu (vert.sh) i pomažu razvoj projekta.\",\n      \"donation_notice_unofficial\": \"Donacije ovdje idu operateru ove VERT instance. Ako želite podržati zvanične developere, posjetite [official_link]vert.sh[/official_link].\"\n    },\n    \"credits\": {\n      \"title\": \"Zasluge\",\n      \"contact_team\": \"Ako želite kontaktirati razvojni tim, koristite email iz kartice \\\"Resursi\\\".\",\n      \"notable_contributors\": \"Istaknuti doprinosioci\",\n      \"notable_description\": \"Želimo zahvaliti ovim osobama na velikim doprinosima VERT-u.\",\n      \"github_contributors\": \"GitHub doprinosioci\",\n      \"github_description\": \"Veliko hvala svima! [github_link]Želite pomoći i vi?[/github_link]\",\n      \"no_contributors\": \"Izgleda da još niko nije doprinio... [contribute_link]budite prvi![/contribute_link]\",\n      \"libraries\": \"Biblioteke\",\n      \"libraries_description\": \"Veliko hvala FFmpeg-u (audio, video), ImageMagick-u (slike) i Pandoc-u (dokumenti). VERT se na njima temelji.\",\n      \"roles\": {\n        \"lead_developer\": \"Glavni developer; backend konverzije, UI implementacija\",\n        \"developer\": \"Developer; UI implementacija\",\n        \"designer\": \"Dizajner; UX, brending, marketing\",\n        \"docker_ci\": \"Održavanje Docker & CI podrške\",\n        \"former_cofounder\": \"Bivši suosnivač i dizajner\"\n      }\n    },\n    \"errors\": {\n      \"github_contributors\": \"Greška pri dohvaćanju GitHub doprinosilaca\"\n    }\n  },\n  \"workers\": {\n    \"errors\": {\n      \"general\": \"Greška pri konverziji {file}: {message}\",\n      \"cancel\": \"Greška pri otkazivanju konverzije za {file}: {message}\",\n      \"magick\": \"Greška u Magick workeru, konverzija slika možda neće raditi ispravno.\",\n      \"ffmpeg\": \"Greška pri učitavanju FFmpeg-a, neke funkcije možda neće raditi.\",\n      \"pandoc\": \"Greška pri učitavanju Pandoc workera, dokumenti možda neće biti konvertovani.\",\n      \"no_audio\": \"Nije pronađen audio zapis.\",\n      \"invalid_rate\": \"Nevažeća sample rate vrijednost: {rate}Hz\",\n      \"file_too_large\": \"Ova datoteka prelazi {limit}GB ograničenje preglednika/uređaja. Pokušajte u Firefoxu ili Safariju.\"\n    }\n  },\n  \"privacy\": {\n    \"title\": \"Politika privatnosti\",\n    \"summary\": {\n      \"title\": \"Sažetak\",\n      \"description\": \"VERT-ova politika privatnosti je vrlo jednostavna: ne prikupljamo niti pohranjujemo ikakve podatke o vama. Ne koristimo kolačiće ni trackere, analitika je potpuno privatna, a konverzije (osim videa) rade lokalno. Video se briše nakon preuzimanja ili nakon sat vremena, osim ako nam ne date dozvolu da ga čuvamo radi rješavanja problema. Koristimo Coolify za hosting i Plausible za anonimnu analitiku. Stripe obrađuje donacije i može prikupiti podatke za prevenciju prevara.<br/><br/>Ovo vrijedi za zvaničnu instancu [vert_link]vert.sh[/vert_link]; treće strane mogu raditi drugačije.\"\n    },\n    \"conversions\": {\n      \"title\": \"Konverzije\",\n      \"description\": \"Većina konverzija (slike, dokumenti, audio) se obavlja lokalno putem WebAssembly alata (ImageMagick, Pandoc, FFmpeg). Vaše datoteke ne napuštaju uređaj.<br/><br/>Video konverzije se obavljaju na našim serverima jer zahtijevaju više snage. Video se briše nakon preuzimanja ili sat vremena, osim ako nam eksplicitno ne dozvolite duže čuvanje radi otklanjanja grešaka.\"\n    },\n    \"donations\": {\n      \"title\": \"Donacije\",\n      \"description\": \"Koristimo Stripe na stranici [about_link]o nama[/about_link] za donacije. Stripe može prikupiti određene informacije radi prevencije prevara, opisano u [stripe_link]njihovoj dokumentaciji[/stripe_link]. Eksterni zahtjevi se šalju tek nakon vašeg klika.\"\n    },\n    \"conversion_errors\": {\n      \"title\": \"Greške pri konverziji\",\n      \"description\": \"Kada konverzija videa ne uspije, možemo prikupiti anonimne informacije radi dijagnostike:\",\n      \"list_job_id\": \"ID zadatka (anonimizirano ime datoteke)\",\n      \"list_format_from\": \"Format iz kojeg se konvertuje\",\n      \"list_format_to\": \"Format u koji se konvertuje\",\n      \"list_stderr\": \"FFmpeg stderr (poruka greške)\",\n      \"list_video\": \"Stvarni video zapis (samo uz vašu dozvolu)\",\n      \"footer\": \"Ove informacije se koriste samo za dijagnostiku. Sam video se prikuplja samo uz vašu dozvolu.\"\n    },\n    \"analytics\": {\n      \"title\": \"Analitika\",\n      \"description\": \"Koristimo vlastitu Plausible instancu za potpuno anonimnu analitiku. Plausible ne koristi kolačiće i usklađen je sa svim glavnim zakonima o privatnosti. Možete isključiti analitiku u sekciji \\\"Privatnost i podaci\\\" u [settings_link]postavkama[/settings_link] i pročitati više [plausible_link]ovdje[/plausible_link].\"\n    },\n    \"local_storage\": {\n      \"title\": \"Lokalno skladištenje\",\n      \"description\": \"Vaše postavke se čuvaju u local storage-u preglednika, a lista GitHub doprinosilaca u session storage-u. Nijedan lični podatak se ne skladišti.<br/><br/>WebAssembly alati (FFmpeg, ImageMagick, Pandoc) se također čuvaju lokalno. Možete ih vidjeti ili obrisati u sekciji \\\"Privatnost i podaci\\\" u [settings_link]postavkama[/settings_link].\"\n    },\n    \"contact\": {\n      \"title\": \"Kontakt\",\n      \"description\": \"Za pitanja, pišite nam na: [email_link]hello@vert.sh[/email_link]. Ako koristite treću stranu, kontaktirajte njihovog hostera.\"\n    },\n    \"last_updated\": \"Posljednje ažuriranje: 2025-10-29\"\n  }\n}\n"
  },
  {
    "path": "messages/de.json",
    "content": "{\n\t\"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n\t\"navbar\": {\n\t\t\"upload\": \"Hochladen\",\n\t\t\"convert\": \"Konvertieren\",\n\t\t\"settings\": \"Optionen\",\n\t\t\"about\": \"Über\",\n\t\t\"toggle_theme\": \"Design wechseln\"\n\t},\n\t\"footer\": {\n\t\t\"copyright\": \"© {year} VERT.\",\n\t\t\"source_code\": \"Quellcode\",\n\t\t\"discord_server\": \"Discord-Server\",\n\t\t\"privacy_policy\": \"Datenschutzerklärung\"\n\t},\n\t\"upload\": {\n\t\t\"title\": \"Der Dateikonverter, den du lieben wirst.\",\n\t\t\"subtitle\": \"Die Verarbeitung aller Bild-, Audio- und Dokumentdateien findet direkt auf deinem Gerät statt. Videos werden auf unseren blitzschnellen Servern konvertiert. Kein Dateigrößenlimit, keine Werbung und vollständig Open Source.\",\n\t\t\"uploader\": {\n\t\t\t\"text\": \"Dateien hier ablegen oder klicken zum {action}\",\n\t\t\t\"convert\": \"Konvertieren\"\n\t\t},\n\t\t\"cards\": {\n\t\t\t\"title\": \"VERT unterstützt...\",\n\t\t\t\"images\": \"Bilder\",\n\t\t\t\"audio\": \"Audio\",\n\t\t\t\"documents\": \"Dokumente\",\n\t\t\t\"video\": \"Video\",\n\t\t\t\"video_server_processing\": \"Server-gestützt\",\n\t\t\t\"local_supported\": \"Lokal unterstützt\",\n\t\t\t\"status\": {\n\t\t\t\t\"text\": \"<b>Status:</b> {status}\",\n\t\t\t\t\"ready\": \"Bereit\",\n\t\t\t\t\"not_ready\": \"Nicht bereit\",\n\t\t\t\t\"not_initialized\": \"Nicht initialisiert\",\n\t\t\t\t\"downloading\": \"Herunterladen...\",\n\t\t\t\t\"initializing\": \"Initialisieren...\",\n\t\t\t\t\"unknown\": \"Unbekannter Status\"\n\t\t\t},\n\t\t\t\"supported_formats\": \"Unterstützte Formate:\"\n\t\t},\n\t\t\"tooltip\": {\n\t\t\t\"partial_support\": \"Dieses Format kann nur als {direction} konvertiert werden.\",\n\t\t\t\"direction_input\": \"Eingabe (von)\",\n\t\t\t\"direction_output\": \"Ausgabe (nach)\",\n\t\t\t\"video_server_processing\": \"Videos werden standardmäßig zur Verarbeitung auf einen Server hochgeladen. Erfahre hier, wie du die Verarbeitung lokal einrichten kannst.\"\n\t\t}\n\t},\n\t\"convert\": {\n\t\t\"archive_file\": {\n\t\t\t\"extract\": \"Archiv entpacken\",\n\t\t\t\"extracting\": \"Archiv erkannt: {filename}\",\n\t\t\t\"extracted\": \"{extract_count} Dateien aus {filename} entpackt. {ignore_count} Elemente wurden ignoriert.\",\n\t\t\t\"detected\": \"{type}-Dateien in {filename} erkannt.\",\n\t\t\t\"audio\": \"Audio\",\n\t\t\t\"video\": \"Video\",\n\t\t\t\"doc\": \"Dokument\",\n\t\t\t\"image\": \"Bild\",\n\t\t\t\"extract_error\": \"Fehler beim Entpacken von {filename}: {error}\"\n\t\t},\n\t\t\"large_file_warning\": \"Aufgrund von Browser-/Gerätebeschränkungen ist die Video-zu-Audio-Konvertierung für diese Datei deaktiviert, da sie größer als {limit}GB ist. Wir empfehlen Firefox oder Safari für Dateien dieser Größe, da diese weniger Einschränkungen haben.\",\n\t\t\"external_warning\": {\n\t\t\t\"title\": \"Warnung: Externer Server\",\n\t\t\t\"text\": \"Wenn du in ein Videoformat konvertierst, werden diese Dateien zur Verarbeitung auf einen externen Server hochgeladen. Möchtest du fortfahren?\",\n\t\t\t\"yes\": \"Ja\",\n\t\t\t\"no\": \"Nein\"\n\t\t},\n\t\t\"panel\": {\n\t\t\t\"convert_all\": \"Alle konvertieren\",\n\t\t\t\"download_all\": \"Alle als .zip laden\",\n\t\t\t\"remove_all\": \"Alle entfernen\",\n\t\t\t\"set_all_to\": \"Alle konvertieren nach\",\n\t\t\t\"na\": \"N/V\"\n\t\t},\n\t\t\"dropdown\": {\n\t\t\t\"audio\": \"Audio\",\n\t\t\t\"video\": \"Video\",\n\t\t\t\"doc\": \"Dokument\",\n\t\t\t\"image\": \"Bild\",\n\t\t\t\"placeholder\": \"Format suchen\",\n\t\t\t\"no_formats\": \"Keine Formate verfügbar\",\n\t\t\t\"no_results\": \"Keine Ergebnisse\"\n\t\t},\n\t\t\"tooltips\": {\n\t\t\t\"unknown_file\": \"Unbekannter Dateityp\",\n\t\t\t\"audio_file\": \"Audiodatei\",\n\t\t\t\"video_file\": \"Videodatei\",\n\t\t\t\"document_file\": \"Dokumentdatei\",\n\t\t\t\"image_file\": \"Bilddatei\",\n\t\t\t\"convert_file\": \"Diese Datei konvertieren\",\n\t\t\t\"download_file\": \"Diese Datei herunterladen\"\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"cant_convert\": \"Wir können diese Datei nicht konvertieren.\",\n\t\t\t\"vertd_server\": \"Was machst du da..? Du solltest den vertd-Server ausführen!\",\n\t\t\t\"vertd_generic_view\": \"Fehlerdetails anzeigen\",\n\t\t\t\"vertd_generic_body\": \"Ein Fehler ist aufgetreten, während versucht wurde, dein Video zu konvertieren. Möchtest du dieses Video an die Entwickler senden, um bei der Behebung dieses Fehlers zu helfen? Nur die Videodatei wird gesendet. Es werden keine Identifikatoren hochgeladen.\",\n\t\t\t\"vertd_generic_title\": \"Videokonvertierungsfehler\",\n\t\t\t\"vertd_generic_yes\": \"Video senden\",\n\t\t\t\"vertd_generic_no\": \"Nicht senden\",\n\t\t\t\"vertd_failed_to_keep\": \"Das Video konnte nicht auf dem Server behalten werden: {error}\",\n\t\t\t\"vertd_details\": \"Fehlerdetails anzeigen\",\n\t\t\t\"vertd_details_body\": \"Wenn du auf Senden drückst, wird <b>dein Video ebenfalls angehängt</b>, zusammen mit dem Fehlerprotokoll, das uns immer zur Überprüfung gemeldet wird. Die folgenden Informationen sind das Protokoll, das wir automatisch erhalten:\",\n\t\t\t\"vertd_details_footer\": \"Diese Informationen werden nur zur Fehlerbehebung verwendet und niemals weitergegeben. Sieh dir unsere [privacy_link]Datenschutzerklärung[/privacy_link] für weitere Details an.\",\n\t\t\t\"vertd_details_job_id\": \"<b>Job-ID:</b> {jobId}\",\n\t\t\t\"vertd_details_from\": \"<b>Von Format:</b> {from}\",\n\t\t\t\"vertd_details_to\": \"<b>Zu Format:</b> {to}\",\n\t\t\t\"vertd_details_error_message\": \"<b>Fehlermeldung:</b> [view_link]Fehlerprotokolle anzeigen[/view_link]\",\n\t\t\t\"vertd_details_close\": \"Schließen\",\n\t\t\t\"vertd_ratelimit\": \"Dein Video, '{filename}', konnte mehrmals nicht konvertiert werden. Um eine Überlastung des Servers zu vermeiden, wurden weitere Konvertierungsversuche für diese Datei vorübergehend blockiert. Bitte versuche es später erneut.\",\n\t\t\t\"unsupported_format\": \"Es werden nur Bild-, Video-, Audio- und Dokumentdateien unterstützt.\",\n\t\t\t\"format_output_only\": \"Dieses Format kann derzeit nur als Ausgabe (konvertiert zu), nicht als Eingabe verwendet werden.\",\n\t\t\t\"vertd_not_found\": \"Konnte die vertd-Instanz nicht finden, um die Videokonvertierung zu starten. Bist du sicher, dass die Instanz-URL korrekt eingestellt ist?\",\n\t\t\t\"worker_downloading\": \"Der {type}-Konverter wird gerade initialisiert, bitte warte einen Moment.\",\n\t\t\t\"worker_error\": \"Beim Initialisieren des {type}-Konverters ist ein Fehler aufgetreten, bitte versuche es später erneut.\",\n\t\t\t\"worker_timeout\": \"Die Initialisierung des {type}-Konverters dauert länger als erwartet, bitte warte noch einen Moment oder lade die Seite neu.\",\n\t\t\t\"audio\": \"Audio\",\n\t\t\t\"doc\": \"Dokument\",\n\t\t\t\"image\": \"Bild\"\n\t\t}\n\t},\n\t\"settings\": {\n\t\t\"title\": \"Optionen\",\n\t\t\"errors\": {\n\t\t\t\"save_failed\": \"Speichern der Einstellungen fehlgeschlagen!\"\n\t\t},\n\t\t\"appearance\": {\n\t\t\t\"title\": \"Erscheinungsbild\",\n\t\t\t\"brightness_theme\": \"Farbschema\",\n\t\t\t\"brightness_description\": \"Möchtest du einen sonnigen Blendeffekt oder eine ruhige, einsame Nacht?\",\n\t\t\t\"light\": \"Hell\",\n\t\t\t\"dark\": \"Dunkel\",\n\t\t\t\"effect_settings\": \"Effekteinstellungen\",\n\t\t\t\"effect_description\": \"Möchtest du schicke Effekte oder eine eher statische Erfahrung?\",\n\t\t\t\"enable\": \"Animiert\",\n\t\t\t\"disable\": \"Statisch\"\n\t\t},\n\t\t\"conversion\": {\n\t\t\t\"title\": \"Konvertierung\",\n\t\t\t\"advanced_settings\": \"Erweiterte Einstellungen\",\n\t\t\t\"filename_format\": \"Dateinamensformat\",\n\t\t\t\"filename_description\": \"Dies bestimmt den Namen der Datei beim Herunterladen, <b>ohne die Dateiendung.</b> Du kannst folgende Platzhalter verwenden: <b>%name%</b> für den ursprünglichen Dateinamen, <b>%extension%</b> für die ursprüngliche Dateiendung und <b>%date%</b> für das Datum der Konvertierung.\",\n\t\t\t\"placeholder\": \"VERT_%name%\",\n\t\t\t\"default_format\": \"Standard-Format\",\n\t\t\t\"default_format_enable\": \"Aktivieren\",\n\t\t\t\"default_format_disable\": \"Deaktivieren\",\n\t\t\t\"default_format_description\": \"Dies ändert das Format, das standardmäßig ausgewählt wird, wenn du eine Datei dieses Typs hochlädst.\",\n\t\t\t\"default_format_image\": \"Bilder\",\n\t\t\t\"default_format_video\": \"Videos\",\n\t\t\t\"default_format_audio\": \"Audio\",\n\t\t\t\"default_format_document\": \"Dokumente\",\n\t\t\t\"metadata\": \"Metadaten\",\n\t\t\t\"metadata_description\": \"Dies legt fest, ob Metadaten (EXIF, Song-Infos etc.) der Originaldatei in den konvertierten Dateien erhalten bleiben.\",\n\t\t\t\"keep\": \"Behalten\",\n\t\t\t\"remove\": \"Entfernen\",\n\t\t\t\"quality\": \"Qualität\",\n\t\t\t\"quality_description\": \"Dies ändert die Standard-Qualität der konvertierten Dateien. Höhere Werte können zu längeren Konvertierungszeiten und größeren Dateien führen.\",\n\t\t\t\"quality_video\": \"Dies ändert die Standard-Qualität der konvertierten Videodateien. Höhere Werte können zu längeren Konvertierungszeiten und größeren Dateien führen.\",\n\t\t\t\"quality_audio\": \"Audio (kbps)\",\n\t\t\t\"quality_images\": \"Bild (%)\",\n\t\t\t\"rate\": \"Abtastrate (Hz)\"\n\t\t},\n\t\t\"vertd\": {\n\t\t\t\"title\": \"Videokonvertierung\",\n\t\t\t\"status\": \"Status:\",\n\t\t\t\"loading\": \"lädt...\",\n\t\t\t\"available\": \"verfügbar, Commit-ID {commitId}\",\n\t\t\t\"unavailable\": \"nicht verfügbar (ist die URL korrekt?)\",\n\t\t\t\"description\": \"Das Projekt <code>vertd</code> ist ein Server-Wrapper für FFmpeg. Dies ermöglicht es dir, Videos bequem über die Weboberfläche von VERT zu konvertieren und dabei die Leistung deiner GPU für maximale Geschwindigkeit zu nutzen.\",\n\t\t\t\"hosting_info\": \"Wir hosten eine öffentliche Instanz für deine Bequemlichkeit, aber es ist einfach, eine eigene auf deinem PC oder Server zu hosten, wenn du weißt, was du tust. Du kannst die Server-Binärdateien [vertd_link]hier[/vertd_link] herunterladen – der Einrichtungsprozess wird in Zukunft noch einfacher!\",\n\t\t\t\"instance\": \"Instanz\",\n\t\t\t\"url_placeholder\": \"Beispiel: http://localhost:24153\",\n\t\t\t\"conversion_speed\": \"Konvertierungsgeschwindigkeit\",\n\t\t\t\"speed_description\": \"Dies beschreibt den Kompromiss zwischen Geschwindigkeit und Qualität. Schnellere Einstellungen führen zu geringerer Qualität, erledigen die Aufgabe aber schneller.\",\n\t\t\t\"speeds\": {\n\t\t\t\t\"very_slow\": \"Sehr langsam\",\n\t\t\t\t\"slower\": \"Langsamer\",\n\t\t\t\t\"slow\": \"Langsam\",\n\t\t\t\t\"medium\": \"Mittel\",\n\t\t\t\t\"fast\": \"Schnell\",\n\t\t\t\t\"ultra_fast\": \"Ultraschnell\"\n\t\t\t},\n\t\t\t\"auto_instance\": \"Automatisch (empfohlen)\",\n\t\t\t\"eu_instance\": \"Falkenstein, Deutschland\",\n\t\t\t\"us_instance\": \"Washington, USA\",\n\t\t\t\"custom_instance\": \"Benutzerdefiniert\"\n\t\t},\n\t\t\"privacy\": {\n\t\t\t\"title\": \"Datenschutz & Daten\",\n\t\t\t\"plausible_title\": \"Plausible Analytics\",\n\t\t\t\"plausible_description\": \"Wir verwenden [plausible_link]Plausible[/plausible_link], ein datenschutzorientiertes Analysetool, um vollständig anonyme Statistiken zu sammeln. Alle Daten werden anonymisiert und aggregiert; es werden niemals identifizierbare Informationen gesendet oder gespeichert. Du kannst die Analysen [analytics_link]hier[/analytics_link] einsehen und dich unten abmelden.\",\n\t\t\t\"opt_in\": \"Einwilligen\",\n\t\t\t\"opt_out\": \"Ablehnen\",\n\t\t\t\"cache_title\": \"Cache-Verwaltung\",\n\t\t\t\"cache_description\": \"Wir speichern die Konverter-Dateien in deinem Browser-Cache, damit du sie nicht jedes Mal neu herunterladen musst. Das verbessert die Leistung und spart Datenvolumen.\",\n\t\t\t\"refresh_cache\": \"Cache aktualisieren\",\n\t\t\t\"clear_cache\": \"Cache leeren\",\n\t\t\t\"files_cached\": \"{size} ({count} Dateien)\",\n\t\t\t\"loading_cache\": \"Lädt...\",\n\t\t\t\"total_size\": \"Gesamtgröße\",\n\t\t\t\"files_cached_label\": \"Gecachte Dateien\",\n\t\t\t\"cache_cleared\": \"Cache erfolgreich geleert!\",\n\t\t\t\"cache_clear_error\": \"Fehler beim Leeren des Caches.\",\n\t\t\t\"site_data_title\": \"Seitendaten-Verwaltung\",\n\t\t\t\"site_data_description\": \"Lösche alle Seitendaten einschließlich Einstellungen und gecachten Dateien, um VERT auf den Standardzustand zurückzusetzen und die Seite neu zu laden.\",\n\t\t\t\"clear_all_data\": \"Alle Seitendaten löschen\",\n\t\t\t\"clear_all_data_confirm_title\": \"Alle Seitendaten löschen?\",\n\t\t\t\"clear_all_data_confirm\": \"Dies setzt alle Einstellungen und den Cache zurück und lädt die Seite neu. Diese Aktion kann nicht rückgängig gemacht werden.\",\n\t\t\t\"clear_all_data_cancel\": \"Abbrechen\",\n\t\t\t\"all_data_cleared\": \"Alle Daten gelöscht! Seite wird neu geladen...\",\n\t\t\t\"all_data_clear_error\": \"Fehler beim Löschen der Seitendaten.\"\n\t\t},\n\t\t\"language\": {\n\t\t\t\"title\": \"Sprache\",\n\t\t\t\"description\": \"Wähle deine bevorzugte Sprache für die VERT-Benutzeroberfläche.\"\n\t\t}\n\t},\n\t\"about\": {\n\t\t\"title\": \"Über\",\n\t\t\"why\": {\n\t\t\t\"title\": \"Warum VERT?\",\n\t\t\t\"description\": \"<b>Dateikonverter haben uns schon immer enttäuscht.</b> Sie sind hässlich, voller Werbung und vor allem langsam. Wir haben beschlossen, dieses Problem ein für alle Mal zu lösen, indem wir eine Alternative schaffen, die all diese Probleme und noch mehr behebt.<br/><br/>Alle Nicht-Videodateien werden vollständig auf deinem Gerät konvertiert; das bedeutet, es gibt keine Verzögerung beim Senden und Empfangen der Dateien von einem Server, und wir können niemals die von dir konvertierten Dateien einsehen.<br/><br/>Videodateien werden auf unseren blitzschnellen RTX 4000 Ada Server hochgeladen. Deine Videos bleiben dort für eine Stunde, wenn du sie nicht konvertierst. Wenn du die Datei konvertierst, bleibt das Video für eine Stunde auf dem Server oder bis es heruntergeladen wird. Anschließend wird die Datei von unserem Server gelöscht.\"\n\t\t},\n\t\t\"sponsors\": {\n\t\t\t\"title\": \"Sponsoren\",\n\t\t\t\"description\": \"Möchtest du uns unterstützen? Kontaktiere einen Entwickler auf dem [discord_link]Discord[/discord_link]-Server oder sende eine E-Mail an\",\n\t\t\t\"email_copied\": \"E-Mail in die Zwischenablage kopiert!\"\n\t\t},\n\t\t\"resources\": {\n\t\t\t\"title\": \"Ressourcen\",\n\t\t\t\"discord\": \"Discord\",\n\t\t\t\"source\": \"Quellcode\",\n\t\t\t\"email\": \"E-Mail\"\n\t\t},\n\t\t\"donate\": {\n\t\t\t\"title\": \"An VERT spenden\",\n\t\t\t\"description\": \"Mit deiner Unterstützung können wir VERT weiter pflegen und verbessern.\",\n\t\t\t\"one_time\": \"Einmalig\",\n\t\t\t\"monthly\": \"Monatlich\",\n\t\t\t\"custom\": \"Benutzerdefiniert\",\n\t\t\t\"pay_now\": \"Jetzt zahlen\",\n\t\t\t\"donate_amount\": \"${amount} USD spenden\",\n\t\t\t\"thank_you\": \"Vielen Dank für deine Spende!\",\n\t\t\t\"payment_failed\": \"Zahlung fehlgeschlagen: {message}{period} Dir wurde nichts berechnet.\",\n\t\t\t\"donation_error\": \"Bei der Verarbeitung deiner Spende ist ein Fehler aufgetreten. Bitte versuche es später erneut.\",\n\t\t\t\"payment_error\": \"Fehler beim Abrufen der Zahlungsdetails. Bitte versuche es später erneut.\",\n\t\t\t\"donation_notice_official\": \"Deine Spenden hier gehen an die offizielle VERT-Instanz (vert.sh) und helfen, die Entwicklung des Projekts zu unterstützen.\",\n\t\t\t\"donation_notice_unofficial\": \"Deine Spenden hier gehen an den Betreiber dieser VERT-Instanz. Wenn du die offiziellen VERT-Entwickler unterstützen möchtest, besuche bitte [official_link]vert.sh[/official_link].\"\n\t\t},\n\t\t\"credits\": {\n\t\t\t\"title\": \"Credits\",\n\t\t\t\"contact_team\": \"Wenn du das Entwicklungsteam kontaktieren möchtest, verwende bitte die E-Mail-Adresse auf der Karte „Ressourcen“.\",\n\t\t\t\"notable_contributors\": \"Nennenswerte Beiträge\",\n\t\t\t\"notable_description\": \"Wir möchten diesen Personen für ihre wichtigen Beiträge zu VERT danken.\",\n\t\t\t\"github_contributors\": \"GitHub-Mitwirkende\",\n\t\t\t\"github_description\": \"Ein großes Dankeschön an alle für ihre Hilfe! [github_link]Möchtest du auch helfen?[/github_link]\",\n\t\t\t\"no_contributors\": \"Scheint, als hätte noch niemand beigetragen... [contribute_link]Sei der Erste![/contribute_link]\",\n\t\t\t\"libraries\": \"Bibliotheken\",\n\t\t\t\"libraries_description\": \"Ein großes Dankeschön an FFmpeg (Audio, Video), ImageMagick (Bilder) und Pandoc (Dokumente) für die Pflege solch exzellenter Bibliotheken über so viele Jahre. VERT verlässt sich auf sie, um dir deine Konvertierungen zu ermöglichen.\",\n\t\t\t\"roles\": {\n\t\t\t\t\"lead_developer\": \"Lead Developer; Backend, UI-Implementierung\",\n\t\t\t\t\"developer\": \"Developer; UI-Implementierung\",\n\t\t\t\t\"designer\": \"Designer; UX, Branding, Marketing\",\n\t\t\t\t\"docker_ci\": \"Docker & CI-Support\",\n\t\t\t\t\"former_cofounder\": \"Ehemaliger Co-Founder & Designer\"\n\t\t\t}\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"github_contributors\": \"Fehler beim Abrufen der GitHub-Mitwirkenden\"\n\t\t}\n\t},\n\t\"workers\": {\n\t\t\"errors\": {\n\t\t\t\"general\": \"Fehler beim Konvertieren von {file}: {message}\",\n\t\t\t\"cancel\": \"Fehler beim Abbrechen der Konvertierung für {file}: {message}\",\n\t\t\t\"magick\": \"Fehler im Magick-Prozess, die Bildkonvertierung funktioniert möglicherweise nicht wie erwartet.\",\n\t\t\t\"ffmpeg\": \"Fehler beim Laden von FFmpeg, einige Funktionen sind möglicherweise nicht verfügbar.\",\n\t\t\t\"pandoc\": \"Fehler beim Laden von Pandoc, die Dokumentkonvertierung funktioniert möglicherweise nicht wie erwartet.\",\n\t\t\t\"no_audio\": \"Kein Audiostream gefunden.\",\n\t\t\t\"invalid_rate\": \"Ungültige Abtastrate angegeben: {rate}Hz\",\n\t\t\t\"file_too_large\": \"Diese Datei überschreitet das Browser-/Gerätelimit von {limit}GB. Versuche es mit Firefox oder Safari, die typischerweise höhere Limits haben.\"\n\t\t}\n\t},\n\t\"privacy\": {\n\t\t\"title\": \"Datenschutzerklärung\",\n\t\t\"summary\": {\n\t\t\t\"title\": \"Zusammenfassung\",\n\t\t\t\"description\": \"Die Datenschutzrichtlinie von VERT ist sehr einfach: Wir sammeln oder speichern keinerlei Daten über dich. Wir verwenden keine Cookies oder Tracker, Analysen sind vollständig privat, und alle Konvertierungen (außer Videos) finden lokal in deinem Browser statt. Videos werden nach dem Herunterladen oder nach einer Stunde gelöscht, es sei denn, du gibst uns ausdrücklich die Erlaubnis zur Speicherung; dies wird nur zur Fehlerbehebung verwendet. VERT hostet selbst eine Coolify-Instanz für die Website und vertd (für Videokonvertierung) sowie eine Plausible-Instanz für vollständig anonyme und aggregierte Analysen. Wir nutzen Stripe zur Verarbeitung von Spenden, was einige Daten zur Betrugsprävention sammeln kann.<br/><br/>Beachte, dass dies möglicherweise nur für die offizielle VERT-Instanz unter [vert_link]vert.sh[/vert_link] gilt; Drittanbieter-Instanzen könnten deine Daten anders behandeln.\"\n\t\t},\n\t\t\"conversions\": {\n\t\t\t\"title\": \"Konvertierungen\",\n\t\t\t\"description\": \"Die meisten Konvertierungen (Bilder, Dokumente, Audio) erfolgen vollständig lokal auf deinem Gerät unter Verwendung von WebAssembly-Versionen der entsprechenden Tools (z. B. ImageMagick, Pandoc, FFmpeg). Das bedeutet, dass deine Dateien dein Gerät nie verlassen und wir niemals Zugriff darauf haben.<br/><br/>Videokonvertierungen werden auf unseren Servern durchgeführt, da sie mehr Rechenleistung erfordern und im Browser noch nicht schnell genug durchgeführt werden können. Videos, die du mit VERT konvertierst, werden nach dem Herunterladen oder nach einer Stunde gelöscht, es sei denn, du gibst uns ausdrücklich die Erlaubnis, sie länger zu speichern, rein zu Fehlerbehebungszwecken.\"\n\t\t},\n\t\t\"donations\": {\n\t\t\t\"title\": \"Spenden\",\n\t\t\t\"description\": \"Wir verwenden Stripe auf der [about_link]Über[/about_link]-Seite, um Spenden zu sammeln. Stripe kann bestimmte Informationen über die Zahlung und das Gerät zur Betrugsprävention sammeln, wie in [stripe_link]ihrer Dokumentation zur erweiterten Betrugserkennung[/stripe_link] beschrieben. Externe Netzwerkanfragen an Stripe werden verzögert und erst gestellt, wenn du auf den Button zum Bezahlen klickst.\"\n\t\t},\n\t\t\"conversion_errors\": {\n\t\t\t\"title\": \"Konvertierungsfehler\",\n\t\t\t\"description\": \"Wenn eine Videokonvertierung fehlschlägt, sammeln wir möglicherweise einige anonyme Daten, um das Problem zu diagnostizieren. Diese Daten können beinhalten:\",\n\t\t\t\"list_job_id\": \"Die Job-ID, welche der anonymisierte Dateiname ist\",\n\t\t\t\"list_format_from\": \"Das Format, aus dem du konvertiert hast\",\n\t\t\t\"list_format_to\": \"Das Format, in das du konvertiert hast\",\n\t\t\t\"list_stderr\": \"Die FFmpeg stderr-Ausgabe deines Jobs (Fehlermeldung)\",\n\t\t\t\"list_video\": \"Die eigentliche Videodatei (nur bei ausdrücklicher Erlaubnis)\",\n\t\t\t\"footer\": \"Diese Informationen werden ausschließlich zur Diagnose von Konvertierungsproblemen verwendet. Die eigentliche Videodatei wird nur gesammelt, wenn du uns die Erlaubnis dazu gibst, und dann auch nur zur Fehlerbehebung verwendet.\"\n\t\t},\n\t\t\"analytics\": {\n\t\t\t\"title\": \"Analysen\",\n\t\t\t\"description\": \"Wir hosten selbst eine Plausible-Instanz für vollständig anonyme und aggregierte Analysen. Plausible verwendet keine Cookies und entspricht allen wichtigen Datenschutzbestimmungen (DSGVO/CCPA/PECR). Du kannst dich im Abschnitt „Datenschutz & Daten“ in den [settings_link]Einstellungen[/settings_link] von den Analysen abmelden und [plausible_link]hier[/plausible_link] mehr über die Datenschutzpraktiken von Plausible lesen.\"\n\t\t},\n\t\t\"local_storage\": {\n\t\t\t\"title\": \"Lokaler Speicher\",\n\t\t\t\"description\": \"Wir verwenden den lokalen Speicher (Local Storage) deines Browsers, um deine Einstellungen zu speichern, und den Sitzungsspeicher (Session Storage), um die Liste der GitHub-Mitwirkenden für den Bereich „Über“ vorübergehend zu speichern und wiederholte GitHub-API-Anfragen zu reduzieren. Es werden keine persönlichen Daten gespeichert oder übertragen.<br/><br/>Die WebAssembly-Versionen der von uns verwendeten Konvertierungstools (FFmpeg, ImageMagick, Pandoc) werden ebenfalls lokal in deinem Browser gespeichert, wenn du die Website zum ersten Mal besuchst, damit du sie nicht bei jedem Besuch erneut herunterladen musst. Es werden keine persönlichen Daten gespeichert oder übertragen. Du kannst diese Daten jederzeit im Abschnitt „Datenschutz & Daten“ in den [settings_link]Einstellungen[/settings_link] einsehen oder löschen.\"\n\t\t},\n\t\t\"contact\": {\n\t\t\t\"title\": \"Kontakt\",\n\t\t\t\"description\": \"Für Fragen sende uns eine E-Mail an: [email_link]hello@vert.sh[/email_link]. Wenn du eine Drittanbieter-Instanz von VERT verwendest, kontaktiere bitte stattdessen den Hoster dieser Instanz.\"\n\t\t},\n\t\t\"last_updated\": \"Zuletzt aktualisiert: 29.10.2025\"\n\t}\n}\n"
  },
  {
    "path": "messages/el.json",
    "content": "{\n\t\"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n\t\"navbar\": {\n\t\t\"upload\": \"Μεταφόρτωση\",\n\t\t\"convert\": \"Μετατροπή\",\n\t\t\"settings\": \"Ρυθμίσεις\",\n\t\t\"about\": \"Σχετικά\",\n\t\t\"toggle_theme\": \"Εναλλαγή θέματος\"\n\t},\n\t\"footer\": {\n\t\t\"copyright\": \"© {year} VERT.\",\n\t\t\"source_code\": \"Κώδικας\",\n\t\t\"discord_server\": \"Discord\"\n\t},\n\t\"upload\": {\n\t\t\"title\": \"Ο μετατροπέας αρχείων που θα λατρέψετε.\",\n\t\t\"subtitle\": \"Όλη η επεξεργασία εικόνων, ήχου και εγγράφων γίνεται στη συσκευή σας. Τα βίντεο μετατρέπονται στους κεραυνοβόλα γρήγορους διακομιστές μας. Χωρίς όριο μεγέθους αρχείου, χωρίς διαφημίσεις και εντελώς ανοιχτού κώδικα.\",\n\t\t\"uploader\": {\n\t\t\t\"text\": \"Σύρετε ή κάντε κλικ για {action}\",\n\t\t\t\"convert\": \"μετατροπή\"\n\t\t},\n\t\t\"cards\": {\n\t\t\t\"title\": \"Το VERT υποστηρίζει...\",\n\t\t\t\"images\": \"Εικόνες\",\n\t\t\t\"audio\": \"Ήχο\",\n\t\t\t\"documents\": \"Έγγραφα\",\n\t\t\t\"video\": \"Βίντεο\",\n\t\t\t\"video_server_processing\": \"Υποστηρίζεται από σέρβερ\",\n\t\t\t\"local_supported\": \"Τοπική υποστήριξη\",\n\t\t\t\"status\": {\n\t\t\t\t\"text\": \"<b>Κατάσταση:</b> {status}\",\n\t\t\t\t\"ready\": \"έτοιμο\",\n\t\t\t\t\"not_ready\": \"μη έτοιμο\",\n\t\t\t\t\"not_initialized\": \"μη αρχικοποιημένο\",\n\t\t\t\t\"downloading\": \"λήψη...\",\n\t\t\t\t\"initializing\": \"αρχικοποίηση...\",\n\t\t\t\t\"unknown\": \"άγνωστη κατάσταση\"\n\t\t\t},\n\t\t\t\"supported_formats\": \"Υποστηριζόμενες μορφές:\"\n\t\t},\n\t\t\"tooltip\": {\n\t\t\t\"partial_support\": \"Αυτή η μορφή μπορεί να μετατραπεί μόνο ως {direction}.\",\n\t\t\t\"direction_input\": \"είσοδος (από)\",\n\t\t\t\"direction_output\": \"έξοδος (προς)\",\n\t\t\t\"video_server_processing\": \"Τα βίντεο μεταφορτώνονται σε σέρβερ για επεξεργασία από προεπιλογή, μάθετε πώς να το ρυθμίσετε τοπικά εδώ.\"\n\t\t}\n\t},\n\t\"convert\": {\n\t\t\"external_warning\": {\n\t\t\t\"title\": \"Προειδοποίηση εξωτερικού σέρβερ\",\n\t\t\t\"text\": \"Εάν επιλέξετε να μετατρέψετε σε μορφή βίντεο, αυτά τα αρχεία θα μεταφορτωθούν σε εξωτερικό σέρβερ για μετατροπή. Θέλετε να συνεχίσετε;\",\n\t\t\t\"yes\": \"Ναι\",\n\t\t\t\"no\": \"Όχι\"\n\t\t},\n\t\t\"panel\": {\n\t\t\t\"convert_all\": \"Μετατροπή όλων\",\n\t\t\t\"download_all\": \"Λήψη όλων ως .zip\",\n\t\t\t\"remove_all\": \"Αφαίρεση όλων των αρχείων\",\n\t\t\t\"set_all_to\": \"Ορισμός όλων σε\",\n\t\t\t\"na\": \"Μ/Δ\"\n\t\t},\n\t\t\"dropdown\": {\n\t\t\t\"audio\": \"Ήχος\",\n\t\t\t\"video\": \"Βίντεο\",\n\t\t\t\"doc\": \"Έγγραφο\",\n\t\t\t\"image\": \"Εικόνα\",\n\t\t\t\"placeholder\": \"Αναζήτηση μορφής\"\n\t\t},\n\t\t\"tooltips\": {\n\t\t\t\"unknown_file\": \"Άγνωστος τύπος αρχείου\",\n\t\t\t\"audio_file\": \"Αρχείο ήχου\",\n\t\t\t\"video_file\": \"Αρχείο βίντεο\",\n\t\t\t\"document_file\": \"Αρχείο εγγράφου\",\n\t\t\t\"image_file\": \"Αρχείο εικόνας\",\n\t\t\t\"convert_file\": \"Μετατροπή αυτού του αρχείου\",\n\t\t\t\"download_file\": \"Λήψη αυτού του αρχείου\"\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"cant_convert\": \"Δεν μπορούμε να μετατρέψουμε αυτό το αρχείο.\",\n\t\t\t\"vertd_server\": \"τι κάνεις...; υποτίθεται ότι πρέπει να εκτελέσεις τον σέρβερ vertd!\",\n\t\t\t\"vertd_generic_body\": \"Παρουσιάστηκε σφάλμα κατά την προσπάθεια μετατροπής του βίντεό σας. Θέλετε να υποβάλετε αυτό το βίντεο στους προγραμματιστές για να βοηθήσετε στη διόρθωση αυτού του σφάλματος; Θα αποσταλεί μόνο το αρχείο βίντεό σας. Δεν θα μεταφορτωθούν αναγνωριστικά.\",\n\t\t\t\"vertd_generic_title\": \"Σφάλμα μετατροπής βίντεο\",\n\t\t\t\"vertd_generic_yes\": \"Υποβολή βίντεο\",\n\t\t\t\"vertd_generic_no\": \"Μην υποβάλετε\",\n\t\t\t\"vertd_failed_to_keep\": \"Αποτυχία διατήρησης του βίντεο στον σέρβερ: {error}\",\n\t\t\t\"unsupported_format\": \"Υποστηρίζονται μόνο αρχεία εικόνας, βίντεο, ήχου και εγγράφων\",\n\t\t\t\"vertd_not_found\": \"Δεν ήταν δυνατή η εύρεση της παρουσίας vertd για την έναρξη της μετατροπής βίντεο. Είστε βέβαιοι ότι η διεύθυνση URL έχει ρυθμιστεί σωστά;\",\n\t\t\t\"worker_downloading\": \"Ο μετατροπέας {type} αρχικοποιείται αυτή τη στιγμή, παρακαλώ περιμένετε λίγο.\",\n\t\t\t\"worker_error\": \"Ο μετατροπέας {type} αντιμετώπισε σφάλμα κατά την αρχικοποίηση, παρακαλώ δοκιμάστε ξανά αργότερα.\",\n\t\t\t\"worker_timeout\": \"Ο μετατροπέας {type} χρειάζεται περισσότερο χρόνο από το αναμενόμενο για να αρχικοποιηθεί, παρακαλώ περιμένετε λίγο ακόμη ή ανανεώστε τη σελίδα.\",\n\t\t\t\"audio\": \"ήχου\",\n\t\t\t\"doc\": \"εγγράφου\",\n\t\t\t\"image\": \"εικόνας\"\n\t\t}\n\t},\n\t\"settings\": {\n\t\t\"title\": \"Ρυθμίσεις\",\n\t\t\"errors\": {\n\t\t\t\"save_failed\": \"Αποτυχία αποθήκευσης ρυθμίσεων!\"\n\t\t},\n\t\t\"appearance\": {\n\t\t\t\"title\": \"Εμφάνιση\",\n\t\t\t\"brightness_theme\": \"Θέμα φωτεινότητας\",\n\t\t\t\"brightness_description\": \"Θέλετε μια ηλιόλουστη λάμψη ή μια ήσυχη μοναχική νύχτα;\",\n\t\t\t\"light\": \"Φωτεινό\",\n\t\t\t\"dark\": \"Σκούρο\",\n\t\t\t\"effect_settings\": \"Ρυθμίσεις εφέ\",\n\t\t\t\"effect_description\": \"Θα θέλατε φανταχτερά εφέ ή μια πιο στατική εμπειρία;\",\n\t\t\t\"enable\": \"Ενεργοποίηση\",\n\t\t\t\"disable\": \"Απενεργοποίηση\"\n\t\t},\n\t\t\"conversion\": {\n\t\t\t\"title\": \"Μετατροπή\",\n\t\t\t\"advanced_settings\": \"Προηγμένες ρυθμίσεις\",\n\t\t\t\"filename_format\": \"Μορφή ονόματος αρχείου\",\n\t\t\t\"filename_description\": \"Αυτό θα καθορίσει το όνομα του αρχείου κατά τη λήψη, <b>χωρίς να περιλαμβάνει την επέκταση αρχείου.</b> Μπορείτε να τοποθετήσετε τα ακόλουθα πρότυπα στη μορφή, τα οποία θα αντικατασταθούν με τις σχετικές πληροφορίες: <b>%name%</b> για το αρχικό όνομα αρχείου, <b>%extension%</b> για την αρχική επέκταση αρχείου και <b>%date%</b> για μια συμβολοσειρά ημερομηνίας του πότε μετατράπηκε το αρχείο.\",\n\t\t\t\"placeholder\": \"VERT_%name%\",\n\t\t\t\"default_format\": \"Προεπιλεγμένη μορφή μετατροπής\",\n\t\t\t\"default_format_description\": \"Αυτό θα αλλάξει την προεπιλεγμένη μορφή που επιλέγεται όταν ανεβάζετε ένα αρχείο αυτού του τύπου.\",\n\t\t\t\"default_format_image\": \"Εικόνες\",\n\t\t\t\"default_format_video\": \"Βίντεο\",\n\t\t\t\"default_format_audio\": \"Ήχος\",\n\t\t\t\"default_format_document\": \"Έγγραφα\",\n\t\t\t\"metadata\": \"Μεταδεδομένα αρχείου\",\n\t\t\t\"metadata_description\": \"Αυτό αλλάζει το αν τυχόν μεταδεδομένα (EXIF, πληροφορίες τραγουδιού κ.λπ.) στο αρχικό αρχείο διατηρούνται στα μετατρεπόμενα αρχεία.\",\n\t\t\t\"keep\": \"Διατήρηση\",\n\t\t\t\"remove\": \"Αφαίρεση\",\n\t\t\t\"quality\": \"Ποιότητα μετατροπής\",\n\t\t\t\"quality_description\": \"Αυτό αλλάζει την προεπιλεγμένη ποιότητα εξόδου των μετατρεπόμενων αρχείων (στην κατηγορία του). Υψηλότερες τιμές μπορεί να οδηγήσουν σε μεγαλύτερους χρόνους μετατροπής και μέγεθος αρχείου.\",\n\t\t\t\"quality_video\": \"Αυτό αλλάζει την προεπιλεγμένη ποιότητα εξόδου των μετατρεπόμενων αρχείων βίντεο. Υψηλότερες τιμές μπορεί να οδηγήσουν σε μεγαλύτερους χρόνους μετατροπής και μέγεθος αρχείου.\",\n\t\t\t\"quality_audio\": \"Ήχος (kbps)\",\n\t\t\t\"quality_images\": \"Εικόνα (%)\",\n\t\t\t\"rate\": \"Ρυθμός δειγματοληψίας (Hz)\"\n\t\t},\n\t\t\"vertd\": {\n\t\t\t\"title\": \"Μετατροπή βίντεο\",\n\t\t\t\"status\": \"κατάσταση:\",\n\t\t\t\"loading\": \"φόρτωση...\",\n\t\t\t\"available\": \"διαθέσιμο, αναγνωριστικό έκδοσης {commitId}\",\n\t\t\t\"unavailable\": \"μη διαθέσιμο (είναι σωστή η διεύθυνση url;)\",\n\t\t\t\"description\": \"Το έργο <code>vertd</code> είναι ένα περιτύλιγμα σέρβερ για το FFmpeg. Αυτό σας επιτρέπει να μετατρέπετε βίντεο μέσω της ευκολίας της διεπαφής ιστού του VERT, ενώ εξακολουθείτε να μπορείτε να αξιοποιήσετε τη δύναμη της GPU σας για να το κάνετε όσο το δυνατόν πιο γρήγορα.\",\n\t\t\t\"hosting_info\": \"Φιλοξενούμε μια δημόσια σελίδα για τη διευκόλυνσή σας, αλλά είναι αρκετά εύκολο να φιλοξενήσετε τη δική σας στον υπολογιστή ή τον σέρβερ σας αν γνωρίζετε τι κάνετε. Μπορείτε να κατεβάσετε τα δυαδικά αρχεία του σέρβερ [vertd_link]εδώ[/vertd_link] - η διαδικασία ρύθμισης θα γίνει ευκολότερη στο μέλλον, οπότε μείνετε συντονισμένοι!\",\n\t\t\t\"instance\": \"Παρουσία\",\n\t\t\t\"url_placeholder\": \"Παράδειγμα: http://localhost:24153\",\n\t\t\t\"conversion_speed\": \"Ταχύτητα μετατροπής\",\n\t\t\t\"speed_description\": \"Αυτό περιγράφει τον συμβιβασμό μεταξύ ταχύτητας και ποιότητας. Ταχύτερες ταχύτητες θα έχουν ως αποτέλεσμα χαμηλότερη ποιότητα, αλλά θα ολοκληρώσουν τη δουλειά γρηγορότερα.\",\n\t\t\t\"speeds\": {\n\t\t\t\t\"very_slow\": \"Πολύ αργή\",\n\t\t\t\t\"slower\": \"Αργότερη\",\n\t\t\t\t\"slow\": \"Αργή\",\n\t\t\t\t\"medium\": \"Μέτρια\",\n\t\t\t\t\"fast\": \"Γρήγορη\",\n\t\t\t\t\"ultra_fast\": \"Πολύ γρήγορη\"\n\t\t\t},\n\t\t\t\"auto_instance\": \"Αυτόματη (συνιστάται)\",\n\t\t\t\"eu_instance\": \"Falkenstein, Γερμανία\",\n\t\t\t\"us_instance\": \"Washington, ΗΠΑ\",\n\t\t\t\"custom_instance\": \"Προσαρμοσμένη\"\n\t\t},\n\t\t\"privacy\": {\n\t\t\t\"title\": \"Απόρρητο & δεδομένα\",\n\t\t\t\"plausible_title\": \"Αναλυτικά στοιχεία Plausible\",\n\t\t\t\"plausible_description\": \"Χρησιμοποιούμε το [plausible_link]Plausible[/plausible_link], ένα εργαλείο αναλυτικών που εστιάζει στο απόρρητο, για τη συλλογή εντελώς ανώνυμων στατιστικών. Όλα τα δεδομένα είναι ανωνυμοποιημένα και συγκεντρωτικά και δεν αποστέλλονται ούτε αποθηκεύονται ποτέ αναγνωρίσιμες πληροφορίες. Μπορείτε να δείτε τα αναλυτικά στοιχεία [analytics_link]εδώ[/analytics_link] και να επιλέξετε να εξαιρεθείτε παρακάτω.\",\n\t\t\t\"opt_in\": \"Συμμετοχή\",\n\t\t\t\"opt_out\": \"Εξαίρεση\",\n\t\t\t\"cache_title\": \"Διαχείριση προσωρινής μνήμης\",\n\t\t\t\"cache_description\": \"Αποθηκεύουμε προσωρινά τα αρχεία μετατροπέα στο πρόγραμμα περιήγησής σας, ώστε να μην χρειάζεται να τα κατεβάζετε ξανά κάθε φορά, βελτιώνοντας την απόδοση και μειώνοντας τη χρήση δεδομένων.\",\n\t\t\t\"refresh_cache\": \"Ανανέωση προσωρινής μνήμης\",\n\t\t\t\"clear_cache\": \"Εκκαθάριση προσωρινής μνήμης\",\n\t\t\t\"files_cached\": \"{size} ({count} αρχεία)\",\n\t\t\t\"loading_cache\": \"Φόρτωση...\",\n\t\t\t\"total_size\": \"Συνολικό μέγεθος\",\n\t\t\t\"files_cached_label\": \"Αρχεία σε προσωρινή μνήμη\",\n\t\t\t\"cache_cleared\": \"Η προσωρινή μνήμη εκκαθαρίστηκε επιτυχώς!\",\n\t\t\t\"cache_clear_error\": \"Αποτυχία εκκαθάρισης προσωρινής μνήμης.\"\n\t\t},\n\t\t\"language\": {\n\t\t\t\"title\": \"Γλώσσα\",\n\t\t\t\"description\": \"Επιλέξτε την προτιμώμενη γλώσσα σας για το περιβάλλον του VERT.\"\n\t\t}\n\t},\n\t\"about\": {\n\t\t\"title\": \"Σχετικά\",\n\t\t\"why\": {\n\t\t\t\"title\": \"Γιατί το VERT;\",\n\t\t\t\"description\": \"<b>Οι μετατροπείς αρχείων μας απογοήτευαν πάντα.</b> Είναι άσχημοι, γεμάτοι διαφημίσεις και το πιο σημαντικό· αργοί. Αποφασίσαμε να λύσουμε αυτό το πρόβλημα μια για πάντα δημιουργώντας μια εναλλακτική που λύνει όλα αυτά τα προβλήματα και περισσότερα.<br/><br/>Όλα τα αρχεία που δεν είναι βίντεο μετατρέπονται εντελώς στη συσκευή σας· αυτό σημαίνει ότι δεν υπάρχει καθυστέρηση μεταξύ της αποστολής και της λήψης των αρχείων από έναν σέρβερ και δεν αποκτούμε ποτέ πρόσβαση στα αρχεία που μετατρέπετε.<br/><br/>Τα αρχεία βίντεο μεταφορτώνονται στον αστραπιαία γρήγορο σέρβερ μας RTX 4000 Ada. Τα βίντεό σας παραμένουν εκεί για μία ώρα εάν δεν τα μετατρέψετε. Εάν μετατρέψετε το αρχείο, το βίντεο θα παραμείνει στον σέρβερ για μία ώρα ή μέχρι να ληφθεί. Στη συνέχεια, το αρχείο θα διαγραφεί από τον σέρβερ μας.\"\n\t\t},\n\t\t\"sponsors\": {\n\t\t\t\"title\": \"Χορηγοί\",\n\t\t\t\"description\": \"Θέλετε να μας υποστηρίξετε; Επικοινωνήστε με έναν προγραμματιστή στον σέρβερ [discord_link]Discord[/discord_link] ή στείλτε email στη διεύθυνση\",\n\t\t\t\"email_copied\": \"Το email αντιγράφηκε στο πρόχειρο!\"\n\t\t},\n\t\t\"resources\": {\n\t\t\t\"title\": \"Πόροι\",\n\t\t\t\"discord\": \"Discord\",\n\t\t\t\"source\": \"Πηγαίος κώδικας\",\n\t\t\t\"email\": \"Email\"\n\t\t},\n\t\t\"donate\": {\n\t\t\t\"title\": \"Δωρεά στο VERT\",\n\t\t\t\"description\": \"Με την υποστήριξή σας, μπορούμε να συνεχίσουμε να συντηρούμε και να βελτιώνουμε το VERT.\",\n\t\t\t\"one_time\": \"Εφάπαξ\",\n\t\t\t\"monthly\": \"Μηνιαία\",\n\t\t\t\"custom\": \"Προσαρμοσμένη\",\n\t\t\t\"pay_now\": \"Πληρωμή τώρα\",\n\t\t\t\"donate_amount\": \"Δωρεά ${amount} USD\",\n\t\t\t\"thank_you\": \"Σας ευχαριστούμε για τη δωρεά σας!\",\n\t\t\t\"payment_failed\": \"Η πληρωμή απέτυχε: {message}{period} Δεν χρεώθηκε ο λογαριασμός σας.\",\n\t\t\t\"donation_error\": \"Παρουσιάστηκε σφάλμα κατά την επεξεργασία της δωρεάς σας. Παρακαλώ δοκιμάστε ξανά αργότερα.\",\n\t\t\t\"payment_error\": \"Σφάλμα κατά την ανάκτηση στοιχείων πληρωμής. Παρακαλώ δοκιμάστε ξανά αργότερα.\"\n\t\t},\n\t\t\"credits\": {\n\t\t\t\"title\": \"Τίτλοι\",\n\t\t\t\"contact_team\": \"Εάν θέλετε να επικοινωνήσετε με την ομάδα ανάπτυξης, χρησιμοποιήστε το email που βρίσκεται στην κάρτα «Πόροι».\",\n\t\t\t\"notable_contributors\": \"Αξιόλογοι συνεισφέροντες\",\n\t\t\t\"notable_description\": \"Θα θέλαμε να ευχαριστήσουμε αυτά τα άτομα για τις σημαντικές συνεισφορές τους στο VERT.\",\n\t\t\t\"github_contributors\": \"Συνεισφέροντες στο GitHub\",\n\t\t\t\"github_description\": \"Μεγάλες ευχαριστίες σε όλα αυτά τα άτομα που βοήθησαν! [github_link]Θέλετε να βοηθήσετε κι εσείς;[/github_link]\",\n\t\t\t\"no_contributors\": \"Φαίνεται ότι κανείς δεν έχει συνεισφέρει ακόμα... [contribute_link]γίνετε ο πρώτος που θα συνεισφέρει![/contribute_link]\",\n\t\t\t\"libraries\": \"Βιβλιοθήκες\",\n\t\t\t\"libraries_description\": \"Μεγάλες ευχαριστίες στα FFmpeg (ήχος, βίντεο), ImageMagick (εικόνες) και Pandoc (έγγραφα) που διατηρούν τέτοιες εξαιρετικές βιβλιοθήκες για τόσα χρόνια. Το VERT βασίζεται σε αυτές για να σας παρέχει τις μετατροπές σας.\",\n\t\t\t\"roles\": {\n\t\t\t\t\"lead_developer\": \"Επικεφαλής προγραμματιστής· backend μετατροπής, υλοποίηση UI\",\n\t\t\t\t\"developer\": \"Προγραμματιστής· υλοποίηση UI\",\n\t\t\t\t\"designer\": \"Σχεδιαστής· UX, branding, μάρκετινγκ\",\n\t\t\t\t\"docker_ci\": \"Συντήρηση υποστήριξης Docker & CI\",\n\t\t\t\t\"former_cofounder\": \"Πρώην συνιδρυτής & σχεδιαστής\"\n\t\t\t}\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"github_contributors\": \"Σφάλμα κατά την ανάκτηση συνεισφερόντων του GitHub\"\n\t\t}\n\t},\n\t\"workers\": {\n\t\t\"errors\": {\n\t\t\t\"general\": \"Σφάλμα κατά τη μετατροπή του {file}: {message}\",\n\t\t\t\"cancel\": \"Σφάλμα κατά την ακύρωση της μετατροπής για το {file}: {message}\",\n\t\t\t\"magick\": \"Σφάλμα στο worker του Magick, η μετατροπή εικόνων μπορεί να μην λειτουργεί όπως αναμένεται.\",\n\t\t\t\"ffmpeg\": \"Σφάλμα κατά τη φόρτωση του ffmpeg, ορισμένες λειτουργίες μπορεί να μην λειτουργούν.\",\n\t\t\t\"no_audio\": \"Δεν βρέθηκε ροή ήχου.\",\n\t\t\t\"invalid_rate\": \"Καθορίστηκε μη έγκυρος ρυθμός δειγματοληψίας: {rate}Hz\"\n\t\t}\n\t}\n}"
  },
  {
    "path": "messages/en.json",
    "content": "{\n\t\"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n\t\"navbar\": {\n\t\t\"upload\": \"Upload\",\n\t\t\"convert\": \"Convert\",\n\t\t\"settings\": \"Settings\",\n\t\t\"about\": \"About\",\n\t\t\"toggle_theme\": \"Toggle theme\"\n\t},\n\t\"footer\": {\n\t\t\"copyright\": \"© {year} VERT.\",\n\t\t\"source_code\": \"Source code\",\n\t\t\"discord_server\": \"Discord server\",\n\t\t\"privacy_policy\": \"Privacy policy\"\n\t},\n\t\"upload\": {\n\t\t\"title\": \"The file converter you'll love.\",\n\t\t\"subtitle\": \"All image, audio, and document processing is done on your device. Videos are converted on our lightning-fast servers. No file size limit, no ads, and completely open source.\",\n\t\t\"uploader\": {\n\t\t\t\"text\": \"Drop or click to {action}\",\n\t\t\t\"convert\": \"convert\"\n\t\t},\n\t\t\"cards\": {\n\t\t\t\"title\": \"VERT supports...\",\n\t\t\t\"images\": \"Images\",\n\t\t\t\"audio\": \"Audio\",\n\t\t\t\"documents\": \"Documents\",\n\t\t\t\"video\": \"Video\",\n\t\t\t\"video_server_processing\": \"Server supported\",\n\t\t\t\"local_supported\": \"Local supported\",\n\t\t\t\"status\": {\n\t\t\t\t\"text\": \"<b>Status:</b> {status}\",\n\t\t\t\t\"ready\": \"ready\",\n\t\t\t\t\"not_ready\": \"not ready\",\n\t\t\t\t\"not_initialized\": \"not initialized\",\n\t\t\t\t\"downloading\": \"downloading...\",\n\t\t\t\t\"initializing\": \"initializing...\",\n\t\t\t\t\"unknown\": \"unknown status\"\n\t\t\t},\n\t\t\t\"supported_formats\": \"Supported formats:\"\n\t\t},\n\t\t\"tooltip\": {\n\t\t\t\"partial_support\": \"This format can only be converted as {direction}.\",\n\t\t\t\"direction_input\": \"input (from)\",\n\t\t\t\"direction_output\": \"output (to)\",\n\t\t\t\"video_server_processing\": \"Video uploads to a server for processing by default, learn how to set it up locally here.\"\n\t\t}\n\t},\n\t\"convert\": {\n\t\t\"archive_file\": {\n\t\t\t\"extract\": \"Extract archive\",\n\t\t\t\"extracting\": \"Detected archive: {filename}\",\n\t\t\t\"extracted\": \"Extracted {extract_count} files from {filename}. {ignore_count} items were ignored.\",\n\t\t\t\"detected\": \"Detected {type} files in {filename}.\",\n\t\t\t\"audio\": \"audio\",\n\t\t\t\"video\": \"video\",\n\t\t\t\"doc\": \"document\",\n\t\t\t\"image\": \"image\",\n\t\t\t\"extract_error\": \"Error extracting {filename}: {error}\"\n\t\t},\n\t\t\"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.\",\n\t\t\"external_warning\": {\n\t\t\t\"title\": \"External server warning\",\n\t\t\t\"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?\",\n\t\t\t\"yes\": \"Yes\",\n\t\t\t\"no\": \"No\"\n\t\t},\n\t\t\"panel\": {\n\t\t\t\"convert_all\": \"Convert all\",\n\t\t\t\"download_all\": \"Download all as .zip\",\n\t\t\t\"remove_all\": \"Remove all files\",\n\t\t\t\"set_all_to\": \"Set all to\",\n\t\t\t\"na\": \"N/A\"\n\t\t},\n\t\t\"dropdown\": {\n\t\t\t\"audio\": \"Audio\",\n\t\t\t\"video\": \"Video\",\n\t\t\t\"doc\": \"Document\",\n\t\t\t\"image\": \"Image\",\n\t\t\t\"placeholder\": \"Search format\",\n\t\t\t\"no_formats\": \"No formats available\",\n\t\t\t\"no_results\": \"No formats match your search\"\n\t\t},\n\t\t\"tooltips\": {\n\t\t\t\"unknown_file\": \"Unknown file type\",\n\t\t\t\"audio_file\": \"Audio file\",\n\t\t\t\"video_file\": \"Video file\",\n\t\t\t\"document_file\": \"Document file\",\n\t\t\t\"image_file\": \"Image file\",\n\t\t\t\"convert_file\": \"Convert this file\",\n\t\t\t\"download_file\": \"Download this file\"\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"cant_convert\": \"We can't convert this file.\",\n\t\t\t\"vertd_server\": \"what are you doing..? you're supposed to run the vertd server!\",\n\t\t\t\"vertd_generic_view\": \"View error details\",\n\t\t\t\"vertd_generic_body\": \"An error occurred whilst whilst trying convert your video. Would you like to submit this video to the developers to help fix this bug? Only your video file will be sent. No identifiers will be uploaded.\",\n\t\t\t\"vertd_generic_title\": \"Video conversion error\",\n\t\t\t\"vertd_generic_yes\": \"Submit video\",\n\t\t\t\"vertd_generic_no\": \"Don't submit\",\n\t\t\t\"vertd_failed_to_keep\": \"Failed to keep the video on the server: {error}\",\n\t\t\t\"vertd_details\": \"View error details\",\n\t\t\t\"vertd_details_body\": \"If you press submit, <b>your video will also be attached</b> alongside the error log which is always reported to us for review. The following information is the log that we automatically receive:\",\n\t\t\t\"vertd_details_footer\": \"This information will only be used for troubleshooting purposes and will never be shared. View our [privacy_link]privacy policy[/privacy_link] for more details.\",\n\t\t\t\"vertd_details_job_id\": \"<b>Job ID:</b> {jobId}\",\n\t\t\t\"vertd_details_from\": \"<b>From format:</b> {from}\",\n\t\t\t\"vertd_details_to\": \"<b>To format:</b> {to}\",\n\t\t\t\"vertd_details_error_message\": \"<b>Error message:</b> [view_link]View error logs[/view_link]\",\n\t\t\t\"vertd_details_close\": \"Close\",\n\t\t\t\"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.\",\n\t\t\t\"unsupported_format\": \"Only image, video, audio, and document files are supported\",\n\t\t\t\"format_output_only\": \"This format can currently only be used as output (converted to), not as input.\",\n\t\t\t\"vertd_not_found\": \"Could not find the vertd instance to start video conversion. Are you sure the instance URL is set correctly?\",\n\t\t\t\"worker_downloading\": \"The {type} converter is currently being initialized, please wait a few moments.\",\n\t\t\t\"worker_error\": \"The {type} converter had an error during initialization, please try again later.\",\n\t\t\t\"worker_timeout\": \"The {type} converter is taking longer than expected to initialize, please wait a few more moments or refresh the page.\",\n\t\t\t\"audio\": \"audio\",\n\t\t\t\"doc\": \"document\",\n\t\t\t\"image\": \"image\"\n\t\t}\n\t},\n\t\"settings\": {\n\t\t\"title\": \"Settings\",\n\t\t\"errors\": {\n\t\t\t\"save_failed\": \"Failed to save settings!\"\n\t\t},\n\t\t\"appearance\": {\n\t\t\t\"title\": \"Appearance\",\n\t\t\t\"brightness_theme\": \"Brightness theme\",\n\t\t\t\"brightness_description\": \"Want a sunny flash-bang, or a quiet lonely night?\",\n\t\t\t\"light\": \"Light\",\n\t\t\t\"dark\": \"Dark\",\n\t\t\t\"effect_settings\": \"Effect settings\",\n\t\t\t\"effect_description\": \"Would you like fancy effects, or a more static experience?\",\n\t\t\t\"enable\": \"Enable\",\n\t\t\t\"disable\": \"Disable\"\n\t\t},\n\t\t\"conversion\": {\n\t\t\t\"title\": \"Conversion\",\n\t\t\t\"advanced_settings\": \"Advanced settings\",\n\t\t\t\"filename_format\": \"File name format\",\n\t\t\t\"filename_description\": \"This will determine the name of the file on download, <b>not including the file extension.</b> You can put these following templates in the format, which will be replaced with the relevant information: <b>%name%</b> for the original file name, <b>%extension%</b> for the original file extension, and <b>%date%</b> for a date string of when the file was converted.\",\n\t\t\t\"placeholder\": \"VERT_%name%\",\n\t\t\t\"default_format\": \"Default conversion format\",\n\t\t\t\"default_format_enable\": \"Enable\",\n\t\t\t\"default_format_disable\": \"Disable\",\n\t\t\t\"default_format_description\": \"This will change the default format selected when you upload a file of this file type.\",\n\t\t\t\"default_format_image\": \"Images\",\n\t\t\t\"default_format_video\": \"Videos\",\n\t\t\t\"default_format_audio\": \"Audio\",\n\t\t\t\"default_format_document\": \"Documents\",\n\t\t\t\"metadata\": \"File metadata\",\n\t\t\t\"metadata_description\": \"This changes whether any metadata (EXIF, song info, etc.) on the original file is preserved in converted files.\",\n\t\t\t\"keep\": \"Keep\",\n\t\t\t\"remove\": \"Remove\",\n\t\t\t\"quality\": \"Conversion quality\",\n\t\t\t\"quality_description\": \"This changes the default output quality of the converted files (in its category). Higher values may result in longer conversion times and file size.\",\n\t\t\t\"quality_video\": \"This changes the default output quality of the converted video files. Higher values may result in longer conversion times and file size.\",\n\t\t\t\"quality_audio\": \"Audio (kbps)\",\n\t\t\t\"quality_images\": \"Image (%)\",\n\t\t\t\"rate\": \"Sample rate (Hz)\"\n\t\t},\n\t\t\"vertd\": {\n\t\t\t\"title\": \"Video conversion\",\n\t\t\t\"status\": \"status:\",\n\t\t\t\"loading\": \"loading...\",\n\t\t\t\"available\": \"available, commit id {commitId}\",\n\t\t\t\"unavailable\": \"unavailable (is the url right?)\",\n\t\t\t\"description\": \"The <code>vertd</code> project is a server wrapper for FFmpeg. This allows you to convert videos through the convenience of VERT's web interface, while still being able to harness the power of your GPU to do it as quickly as possible.\",\n\t\t\t\"hosting_info\": \"We host a public instance for your convenience, but it is quite easy to host your own on your PC or server if you know what you are doing. You can download the server binaries [vertd_link]here[/vertd_link] - the process of setting this up will become easier in the future, so stay tuned!\",\n\t\t\t\"instance\": \"Instance\",\n\t\t\t\"url_placeholder\": \"Example: http://localhost:24153\",\n\t\t\t\"conversion_speed\": \"Conversion speed\",\n\t\t\t\"speed_description\": \"This describes the tradeoff between speed and quality. Faster speeds will result in lower quality, but will get the job done quicker.\",\n\t\t\t\"speeds\": {\n\t\t\t\t\"very_slow\": \"Very Slow\",\n\t\t\t\t\"slower\": \"Slower\",\n\t\t\t\t\"slow\": \"Slow\",\n\t\t\t\t\"medium\": \"Medium\",\n\t\t\t\t\"fast\": \"Fast\",\n\t\t\t\t\"ultra_fast\": \"Ultra Fast\"\n\t\t\t},\n\t\t\t\"auto_instance\": \"Auto (recommended)\",\n\t\t\t\"eu_instance\": \"Falkenstein, Germany\",\n\t\t\t\"us_instance\": \"Washington, USA\",\n\t\t\t\"custom_instance\": \"Custom\"\n\t\t},\n\t\t\"privacy\": {\n\t\t\t\"title\": \"Privacy & data\",\n\t\t\t\"plausible_title\": \"Plausible analytics\",\n\t\t\t\"plausible_description\": \"We use [plausible_link]Plausible[/plausible_link], a privacy-focused analytics tool, to gather completely anonymous statistics. All data is anonymized and aggregated, and no identifiable information is ever sent or stored. You can view the analytics [analytics_link]here[/analytics_link] and choose to opt out below.\",\n\t\t\t\"opt_in\": \"Opt-in\",\n\t\t\t\"opt_out\": \"Opt-out\",\n\t\t\t\"cache_title\": \"Cache management\",\n\t\t\t\"cache_description\": \"We cache the converter files on your browser so you don't have to re-download them every time, improving performance and reducing data usage.\",\n\t\t\t\"refresh_cache\": \"Refresh cache\",\n\t\t\t\"clear_cache\": \"Clear cache\",\n\t\t\t\"files_cached\": \"{size} ({count} files)\",\n\t\t\t\"loading_cache\": \"Loading...\",\n\t\t\t\"total_size\": \"Total Size\",\n\t\t\t\"files_cached_label\": \"Files Cached\",\n\t\t\t\"cache_cleared\": \"Cache cleared successfully!\",\n\t\t\t\"cache_clear_error\": \"Failed to clear cache.\",\n\t\t\t\"site_data_title\": \"Site data management\",\n\t\t\t\"site_data_description\": \"Clear all site data including settings and cached files, resetting VERT to its default state and reloading the page.\",\n\t\t\t\"clear_all_data\": \"Clear all site data\",\n\t\t\t\"clear_all_data_confirm_title\": \"Clear all site data?\",\n\t\t\t\"clear_all_data_confirm\": \"This will reset all settings & cache, then reload the page. This action cannot be undone.\",\n\t\t\t\"clear_all_data_cancel\": \"Cancel\",\n\t\t\t\"all_data_cleared\": \"All site data cleared! Reloading page...\",\n\t\t\t\"all_data_clear_error\": \"Failed to clear all site data.\"\n\t\t},\n\t\t\"language\": {\n\t\t\t\"title\": \"Language\",\n\t\t\t\"description\": \"Select your preferred language for the VERT interface.\"\n\t\t}\n\t},\n\t\"about\": {\n\t\t\"title\": \"About\",\n\t\t\"why\": {\n\t\t\t\"title\": \"Why VERT?\",\n\t\t\t\"description\": \"<b>File converters have always disappointed us.</b> They're ugly, riddled with ads, and most importantly; slow. We decided to solve this problem once and for all by making an alternative that solves all those problems, and more.<br/><br/>All non-video files are converted completely on-device; this means that there's no delay between sending and receiving the files from a server, and we never get to snoop on the files you convert.<br/><br/>Video files get uploaded to our lightning-fast RTX 4000 Ada server. Your videos stay on there for an hour if you do not convert them. If you do convert the file, the video will stay on the server for an hour, or until it is downloaded. The file will then be deleted from our server.\"\n\t\t},\n\t\t\"sponsors\": {\n\t\t\t\"title\": \"Sponsors\",\n\t\t\t\"description\": \"Want to support us? Contact a developer in the [discord_link]Discord[/discord_link] server, or send an email to\",\n\t\t\t\"email_copied\": \"Email copied to clipboard!\"\n\t\t},\n\t\t\"resources\": {\n\t\t\t\"title\": \"Resources\",\n\t\t\t\"discord\": \"Discord\",\n\t\t\t\"source\": \"Source\",\n\t\t\t\"email\": \"Email\"\n\t\t},\n\t\t\"donate\": {\n\t\t\t\"title\": \"Donate to VERT\",\n\t\t\t\"description\": \"With your support, we can keep maintaining and improving VERT.\",\n\t\t\t\"one_time\": \"One-time\",\n\t\t\t\"monthly\": \"Monthly\",\n\t\t\t\"custom\": \"Custom\",\n\t\t\t\"pay_now\": \"Pay now\",\n\t\t\t\"donate_amount\": \"Donate ${amount} USD\",\n\t\t\t\"thank_you\": \"Thank you for your donation!\",\n\t\t\t\"payment_failed\": \"Payment failed: {message}{period} You have not been charged.\",\n\t\t\t\"donation_error\": \"An error occurred while processing your donation. Please try again later.\",\n\t\t\t\"payment_error\": \"Error fetching payment details. Please try again later.\",\n\t\t\t\"donation_notice_official\": \"Your donations here go to the official VERT instance (vert.sh), and helps to support the development of the project.\",\n\t\t\t\"donation_notice_unofficial\": \"Your donations here go to the operator of this VERT instance. If you wish to support the official VERT developers, please visit [official_link]vert.sh[/official_link] instead.\"\n\t\t},\n\t\t\"credits\": {\n\t\t\t\"title\": \"Credits\",\n\t\t\t\"contact_team\": \"If you would like to contact the development team, please use the email found on the \\\"Resources\\\" card.\",\n\t\t\t\"notable_contributors\": \"Notable contributors\",\n\t\t\t\"notable_description\": \"We'd like to thank these people for their major contributions to VERT.\",\n\t\t\t\"github_contributors\": \"GitHub contributors\",\n\t\t\t\"github_description\": \"Big thanks to all these people for helping out! [github_link]Want to help too?[/github_link]\",\n\t\t\t\"no_contributors\": \"Seems like no one has contributed yet... [contribute_link]be the first to contribute![/contribute_link]\",\n\t\t\t\"libraries\": \"Libraries\",\n\t\t\t\"libraries_description\": \"A big thanks to FFmpeg (audio, video), ImageMagick (images) and Pandoc (documents) for maintaining such excellent libraries for so many years. VERT relies on them to provide you with your conversions.\",\n\t\t\t\"roles\": {\n\t\t\t\t\"lead_developer\": \"Lead developer; conversion backend, UI implementation\",\n\t\t\t\t\"developer\": \"Developer; UI implementation\",\n\t\t\t\t\"designer\": \"Designer; UX, branding, marketing\",\n\t\t\t\t\"docker_ci\": \"Maintaining Docker & CI support\",\n\t\t\t\t\"former_cofounder\": \"Former co-founder & designer\"\n\t\t\t}\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"github_contributors\": \"Error fetching GitHub contributors\"\n\t\t}\n\t},\n\t\"workers\": {\n\t\t\"errors\": {\n\t\t\t\"general\": \"Error converting {file}: {message}\",\n\t\t\t\"cancel\": \"Error canceling conversion for {file}: {message}\",\n\t\t\t\"magick\": \"Error in Magick worker, image conversion may not work as expected.\",\n\t\t\t\"ffmpeg\": \"Error loading FFmpeg, some features may not work as expected.\",\n\t\t\t\"pandoc\": \"Error loading Pandoc worker, document conversion may not work as expected.\",\n\t\t\t\"no_audio\": \"No audio stream found.\",\n\t\t\t\"invalid_rate\": \"Invalid sample rate specified: {rate}Hz\",\n\t\t\t\"file_too_large\": \"This file exceeds the {limit}GB browser / device limit. Try Firefox or Safari to convert this large file, which typically have higher limits.\"\n\t\t}\n\t},\n\t\"privacy\": {\n\t\t\"title\": \"Privacy Policy\",\n\t\t\"summary\": {\n\t\t\t\"title\": \"Summary\",\n\t\t\t\"description\": \"VERT's privacy policy is very simple: we do not collect or store any data on you at all. We don't use cookies or trackers, analytics are completely private, and all conversions (except videos) happen locally on your browser. Videos are deleted after being downloaded, or an hour, unless explicitly given permission by you to be stored; it will only be used for the purpose of troubleshooting. VERT self-hosts a Coolify instance for hosting the website and vertd (for video conversion), and a Plausible instance for completely anonymous and aggregated analytics. We use Stripe to process donations, which may collect some data used for fraud prevention.<br/><br/>Note this may only apply to the official VERT instance at [vert_link]vert.sh[/vert_link]; third-party instances may handle your data differently.\"\n\t\t},\n\t\t\"conversions\": {\n\t\t\t\"title\": \"Conversions\",\n\t\t\t\"description\": \"Most conversions (images, documents, audio) happen entirely locally on your device using WebAssembly versions of the relevant tools (e.g. ImageMagick, Pandoc, FFmpeg). This means your files never leave your device and we will never have access to them.<br/><br/>Video conversions are performed on our servers because they require more processing power and cannot be done very quickly on the browser yet. Videos you convert with VERT are deleted after being downloaded, or after one hour, unless you explicitly give permission for us to store them longer purely for troubleshooting purposes.\"\n\t\t},\n\t\t\"donations\": {\n\t\t\t\"title\": \"Donations\",\n\t\t\t\"description\": \"We use Stripe on the [about_link]about[/about_link] page to collect donations. Stripe may collect certain information about the payment and device for fraud prevention as described in [stripe_link]their documentation on advanced fraud detection[/stripe_link]. External network requests to Stripe are deferred, and are only made after you click the button to pay.\"\n\t\t},\n\t\t\"conversion_errors\": {\n\t\t\t\"title\": \"Conversion Errors\",\n\t\t\t\"description\": \"When a video conversion fails, we may collect some anonymous data to help us diagnose the issue. This data may include:\",\n\t\t\t\"list_job_id\": \"The job ID, which is the anonymized file name\",\n\t\t\t\"list_format_from\": \"The format you converted from\",\n\t\t\t\"list_format_to\": \"The format you converted to\",\n\t\t\t\"list_stderr\": \"The FFmpeg stderr output of your job (error message)\",\n\t\t\t\"list_video\": \"The actual video file (if given explicit permission)\",\n\t\t\t\"footer\": \"This information is used solely for the purpose of diagnosing conversion issues. The actual video file will only ever be collected if you give us permission to do so, where it will only be used for troubleshooting.\"\n\t\t},\n\t\t\"analytics\": {\n\t\t\t\"title\": \"Analytics\",\n\t\t\t\"description\": \"We self-host a Plausible instance for completely anonymous and aggregated analytics. Plausible does not use cookies and complies with all major privacy regulations (GDPR/CCPA/PECR). You can opt out of analytics in the \\\"Privacy & data\\\" section in [settings_link]settings[/settings_link] and read more about Plausible's privacy practices [plausible_link]here[/plausible_link].\"\n\t\t},\n\t\t\"local_storage\": {\n\t\t\t\"title\": \"Local Storage\",\n\t\t\t\"description\": \"We use your browser's local storage to save your settings, and your browser's session storage to temporarily store the GitHub contributors list for the \\\"About\\\" section to reduce repeated GitHub API requests. No personal data is stored or transmitted.<br/><br/>The WebAssembly versions of the conversion tools we use (FFmpeg, ImageMagick, Pandoc) are also stored locally on your browser when you first visit the website, so you don't need to redownload them each visit. No personal data is stored or transmitted. You may view or delete this data at any time in the \\\"Privacy & data\\\" section in [settings_link]settings[/settings_link].\"\n\t\t},\n\t\t\"contact\": {\n\t\t\t\"title\": \"Contact\",\n\t\t\t\"description\": \"For questions, email us at: [email_link]hello@vert.sh[/email_link]. If you are using a third-party instance of VERT, please contact the hoster of that instance instead.\"\n\t\t},\n\t\t\"last_updated\": \"Last updated: 2025-10-29\"\n\t},\n\t\"toast\": {\n\t\t\"insecure_context\": \"You are visiting VERT in an insecure context (e.g. accessing over HTTP instead of HTTPS). Some features may not work as expected.\"\n\t}\n}\n"
  },
  {
    "path": "messages/es.json",
    "content": "{\n\t\"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n\t\"navbar\": {\n\t\t\"upload\": \"Subir\",\n\t\t\"convert\": \"Convertir\",\n\t\t\"settings\": \"Ajustes\",\n\t\t\"about\": \"Acerca de\",\n\t\t\"toggle_theme\": \"Cambiar tema\"\n\t},\n\t\"footer\": {\n\t\t\"copyright\": \"© {year} VERT.\",\n\t\t\"source_code\": \"Código fuente\",\n\t\t\"discord_server\": \"Servidor de Discord\"\n\t},\n\t\"upload\": {\n\t\t\"title\": \"El convertidor de archivos que te encantará.\",\n\t\t\"subtitle\": \"Todo el procesamiento de imágenes, audio y documentos es hecho en tu dispositivo. Los vídeos son convertidos en nuestros servidores ultra rápidos. Sin límite de tamaño de archivo, sin anuncios y de código abierto.\",\n\t\t\"uploader\": {\n\t\t\t\"text\": \"Arrastra o haz clic para {action}\",\n\t\t\t\"convert\": \"convertir\"\n\t\t},\n\t\t\"cards\": {\n\t\t\t\"title\": \"VERT soporta...\",\n\t\t\t\"images\": \"Imágenes\",\n\t\t\t\"audio\": \"Audio\",\n\t\t\t\"documents\": \"Documentos\",\n\t\t\t\"video\": \"Vídeo\",\n\t\t\t\"video_server_processing\": \"Soportado por el servidor\",\n\t\t\t\"local_supported\": \"Soportado localmente\",\n\t\t\t\"status\": {\n\t\t\t\t\"text\": \"<b>Estado:</b> {status}\",\n\t\t\t\t\"ready\": \"listo\",\n\t\t\t\t\"not_ready\": \"no listo\",\n\t\t\t\t\"not_initialized\": \"no inicializado\",\n\t\t\t\t\"downloading\": \"descargando...\",\n\t\t\t\t\"initializing\": \"inicializando...\",\n\t\t\t\t\"unknown\": \"estado desconocido\"\n\t\t\t},\n\t\t\t\"supported_formats\": \"Formatos soportados:\"\n\t\t},\n\t\t\"tooltip\": {\n\t\t\t\"partial_support\": \"Este formato solo se puede convertir a {direction}.\",\n\t\t\t\"direction_input\": \"entrada (desde)\",\n\t\t\t\"direction_output\": \"salida (hacia)\",\n\t\t\t\"video_server_processing\": \"Por defecto, los vídeos se suben a un servidor para ser procesados. Aprende cómo instalarlo localmente aquí.\"\n\t\t}\n\t},\n\t\"convert\": {\n\t\t\"external_warning\": {\n\t\t\t\"title\": \"Advertencia del servidor externo\",\n\t\t\t\"text\": \"Si eliges convertir a un formato de video, esos archivos se cargarán en un servidor externo para convertirlos. ¿Quieres continuar?\",\n\t\t\t\"yes\": \"Sí\",\n\t\t\t\"no\": \"No\"\n\t\t},\n\t\t\"panel\": {\n\t\t\t\"convert_all\": \"Convertir todo\",\n\t\t\t\"download_all\": \"Comprimir todo\",\n\t\t\t\"remove_all\": \"Quitar todos los archivos\",\n\t\t\t\"set_all_to\": \"Cambiar todos a\",\n\t\t\t\"na\": \"N/A\"\n\t\t},\n\t\t\"dropdown\": {\n\t\t\t\"audio\": \"Audio\",\n\t\t\t\"video\": \"Vídeo\",\n\t\t\t\"doc\": \"Documento\",\n\t\t\t\"image\": \"Imagen\",\n\t\t\t\"placeholder\": \"Buscar formato\"\n\t\t},\n\t\t\"tooltips\": {\n\t\t\t\"unknown_file\": \"Formato de archivo desconocido\",\n\t\t\t\"audio_file\": \"Audio\",\n\t\t\t\"video_file\": \"Vídeo\",\n\t\t\t\"document_file\": \"Documento\",\n\t\t\t\"image_file\": \"Imagen\",\n\t\t\t\"convert_file\": \"Convertir este archivo\",\n\t\t\t\"download_file\": \"Descargar este archivo\"\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"cant_convert\": \"No podemos convertir este archivo.\",\n\t\t\t\"vertd_server\": \"¿Qué estás haciendo..? ¡Debes ejecutar el servidor de vertd!\",\n\t\t\t\"unsupported_format\": \"Solo aceptamos imágenes, vídeos, audios y documentos.\",\n\t\t\t\"vertd_not_found\": \"No se encontró la instancia de vertd para iniciar la conversión de vídeos. ¿Estás seguro de que la URL es correcta?\",\n\t\t\t\"worker_downloading\": \"El convertidor {type} se está inicializando actualmente, espere unos momentos.\",\n\t\t\t\"worker_error\": \"El convertidor {type} tuvo un error durante la inicialización, inténtelo nuevamente más tarde.\",\n\t\t\t\"worker_timeout\": \"El convertidor {type} está tardando más de lo esperado en inicializarse. Espere unos momentos más o actualice la página.\",\n\t\t\t\"audio\": \"audio\",\n\t\t\t\"doc\": \"documento\",\n\t\t\t\"image\": \"imagen\"\n\t\t}\n\t},\n\t\"settings\": {\n\t\t\"title\": \"Ajustes\",\n\t\t\"errors\": {\n\t\t\t\"save_failed\": \"¡No se han podido guardar los ajustes!\"\n\t\t},\n\t\t\"appearance\": {\n\t\t\t\"title\": \"Apariencia\",\n\t\t\t\"brightness_theme\": \"Tema\",\n\t\t\t\"brightness_description\": \"¿Prefieres una flash-bang soleada o una silenciosa y solitaria noche?\",\n\t\t\t\"light\": \"Claro\",\n\t\t\t\"dark\": \"Oscuro\",\n\t\t\t\"effect_settings\": \"Efectos\",\n\t\t\t\"effect_description\": \"¿Prefieres efectos en la interfaz o una experiencia más estática?\",\n\t\t\t\"enable\": \"Habilitar\",\n\t\t\t\"disable\": \"Deshabilitar\"\n\t\t},\n\t\t\"conversion\": {\n\t\t\t\"title\": \"Conversión\",\n\t\t\t\"advanced_settings\": \"Configuraciones avanzadas\",\n\t\t\t\"filename_format\": \"Formato del nombre de archivo\",\n\t\t\t\"filename_description\": \"Esto va a determinar el nombre del archivo al ser descargado <b>sin incluir la extensión</b>. Puedes poner las siguientes plantillas en el formato, las cuales serán reemplazadas con la información que les corresponde: <b>%name%</b> para el nombre original, <b>%extension%</b> para la extensión original del archivo y <b>%date%</b> para la fecha de cuando el archivo fue convertido.\",\n\t\t\t\"placeholder\": \"VERT_%name%\",\n\t\t\t\"default_format\": \"Formato de conversión predeterminado\",\n\t\t\t\"default_format_description\": \"Esto cambiará el formato predeterminado seleccionado cuando subes un archivo de este tipo.\",\n\t\t\t\"default_format_image\": \"Imágenes\",\n\t\t\t\"default_format_video\": \"Vídeos\",\n\t\t\t\"default_format_audio\": \"Audio\",\n\t\t\t\"default_format_document\": \"Documentos\",\n\t\t\t\"metadata\": \"Metadatos del archivo\",\n\t\t\t\"metadata_description\": \"Esto cambia si los metadatos (EXIF, información de la canción, etc.) del archivo original se conservan en los archivos convertidos.\",\n\t\t\t\"keep\": \"Mantener\",\n\t\t\t\"remove\": \"Eliminar\",\n\t\t\t\"quality\": \"Calidad de la conversión\",\n\t\t\t\"quality_description\": \"Esto cambia la calidad por defecto de los archivos convertidos (en su categoría). Valores más altos pueden resultar en tiempos de conversión y tamaños de archivo más largos.\",\n\t\t\t\"quality_video\": \"Esto cambia la calidad por defecto de los vídeos convertidos. Valores más altos pueden resultar en tiempos de conversión y tamaños de archivo más largos.\",\n\t\t\t\"quality_audio\": \"Audio (kbps)\",\n\t\t\t\"quality_images\": \"Imagen (%)\",\n\t\t\t\"rate\": \"Tasa de muestreo (Hz)\"\n\t\t},\n\t\t\"vertd\": {\n\t\t\t\"title\": \"Conversión de vídeo\",\n\t\t\t\"status\": \"estado:\",\n\t\t\t\"loading\": \"cargando...\",\n\t\t\t\"available\": \"disponible, id del commit {commitId}\",\n\t\t\t\"unavailable\": \"no disponible (¿has comprobado la url?)\",\n\t\t\t\"description\": \"<code>vertd</code> es un proyecto que actúa como un servidor intermediario (\\\"wrapper\\\") para FFmpeg. Permite convertir vídeos sin dejar de lado la conveniente interfaz web de VERT y, a la vez, aprovecha la potencia de tu GPU para hacerlo lo más rápido posible.\",\n\t\t\t\"hosting_info\": \"Alojamos una instancia pública para tu conveniencia, pero es bastante fácil alojar una propia en tu PC o servidor si sabes lo que estás haciendo. Puedes descargar los binarios del servidor [vertd_link]aquí[/vertd_link]. ¡El proceso de instalación será más fácil en el futuro, así que mantente atento!\",\n\t\t\t\"instance\": \"Instancia\",\n\t\t\t\"url_placeholder\": \"Ejemplo: http://localhost:24153\",\n\t\t\t\"conversion_speed\": \"Velocidad de conversión\",\n\t\t\t\"speed_description\": \"Esto describe el equilibrio entre velocidad y calidad. Velocidades más rápidas resultarán en una calidad más baja, pero harán el trabajo más rápido.\",\n\t\t\t\"speeds\": {\n\t\t\t\t\"very_slow\": \"Extremadamente lento\",\n\t\t\t\t\"slower\": \"Muy lento\",\n\t\t\t\t\"slow\": \"Lento\",\n\t\t\t\t\"medium\": \"Medio\",\n\t\t\t\t\"fast\": \"Rápido\",\n\t\t\t\t\"ultra_fast\": \"Súper rápido\"\n\t\t\t},\n\t\t\t\"auto_instance\": \"Automático (recomendado)\",\n\t\t\t\"eu_instance\": \"Falkenstein, Alemania\",\n\t\t\t\"us_instance\": \"Washington, EE. UU.\",\n\t\t\t\"custom_instance\": \"Personalizado\"\n\t\t},\n\t\t\"privacy\": {\n\t\t\t\"title\": \"Privacidad\",\n\t\t\t\"plausible_title\": \"Analíticas de Plausible\",\n\t\t\t\"plausible_description\": \"Usamos [plausible_link]Plausible[/plausible_link], una herramienta de analíticas orientada a la privacidad para recopilar estadísticas completamente anónimas. Toda la información que recopilamos es anonimizada y agregada, y en ningún momento se envía ni se almacena información que permita identificarte. Puedes ver las estadísticas [analytics_link]aquí[/analytics_link] y excluirte de ellas a continuación:\",\n\t\t\t\"opt_in\": \"Participar\",\n\t\t\t\"opt_out\": \"No participar\",\n\t\t\t\"cache_title\": \"Administración de caché\",\n\t\t\t\"cache_description\": \"Guardamos en caché los archivos del convertidor en su navegador para que no tenga que volver a descargarlos cada vez, mejorando el rendimiento y reduciendo el uso de datos.\",\n\t\t\t\"refresh_cache\": \"Actualizar caché\",\n\t\t\t\"clear_cache\": \"Borrar caché\",\n\t\t\t\"files_cached\": \"{size} ({count} archivos)\",\n\t\t\t\"loading_cache\": \"Cargando...\",\n\t\t\t\"total_size\": \"Tamaño total\",\n\t\t\t\"files_cached_label\": \"Archivos en caché\",\n\t\t\t\"cache_cleared\": \"¡Caché borrada exitosamente!\"\n\t\t},\n\t\t\"language\": {\n\t\t\t\"title\": \"Lenguaje\",\n\t\t\t\"description\": \"Selecciona el lenguaje que prefieres usar para la interfaz de VERT.\"\n\t\t}\n\t},\n\t\"about\": {\n\t\t\"title\": \"Acerca de\",\n\t\t\"why\": {\n\t\t\t\"title\": \"¿Por qué VERT?\",\n\t\t\t\"description\": \"<b>Los conversores de archivos siempre nos han decepcionado.</b> Son feos, están llenos de anuncios y, lo más importante, son lentos. Decidimos solucionar este problema de una vez por todas creando una alternativa que resuelve todo eso, y más.<br/><br/>Todos los archivos (exceptuando vídeos) se convierten directamente en tu dispositivo; esto significa que no hay demoras por subir o bajar archivos de un servidor, y nunca tenemos acceso a los archivos que conviertes.<br/><br/>Los vídeos se suben a nuestro servidor ultra rápido equipado con una RTX 4000 Ada. Tus vídeos permanecen allí durante una hora si no los conviertes. Si los conviertes, el archivo se guarda durante una hora, o hasta que lo descargues. Luego, el archivo se elimina del servidor.\"\n\t\t},\n\t\t\"sponsors\": {\n\t\t\t\"title\": \"Patrocinadores\",\n\t\t\t\"description\": \"¿Quieres apoyarnos? Contacta a un desarrollador en el servidor de [discord_link]Discord[/discord_link] o envía un correo a\",\n\t\t\t\"email_copied\": \"¡Email copiado al portapapeles!\"\n\t\t},\n\t\t\"resources\": {\n\t\t\t\"title\": \"Recursos\",\n\t\t\t\"discord\": \"Discord\",\n\t\t\t\"source\": \"Fuente\",\n\t\t\t\"email\": \"Email\"\n\t\t},\n\t\t\"donate\": {\n\t\t\t\"title\": \"Donar a VERT\",\n\t\t\t\"description\": \"Con tu apoyo, podemos seguir manteniendo y mejorando VERT.\",\n\t\t\t\"one_time\": \"Una sola vez\",\n\t\t\t\"monthly\": \"Mensual\",\n\t\t\t\"custom\": \"Personalizado\",\n\t\t\t\"pay_now\": \"Pagar ahora\",\n\t\t\t\"donate_amount\": \"Donar ${amount} USD\",\n\t\t\t\"thank_you\": \"¡Gracias por tu donación!\",\n\t\t\t\"payment_failed\": \"Pago fallido: {message}{period} No se ha efectuado ningún cargo.\",\n\t\t\t\"donation_error\": \"Ha ocurrido un error al procesar tu donación. Por favor, inténtalo de nuevo más tarde.\",\n\t\t\t\"payment_error\": \"Ha ocurrido un error al obtener los detalles del pago. Por favor, inténtalo de nuevo más tarde.\"\n\t\t},\n\t\t\"credits\": {\n\t\t\t\"title\": \"Créditos\",\n\t\t\t\"contact_team\": \"Si te gustaría contactar al equipo de desarrollo, por favor usa el email que se encuentra en la tarjeta de \\\"Recursos\\\".\",\n\t\t\t\"notable_contributors\": \"Colaboradores destacados\",\n\t\t\t\"notable_description\": \"Queremos dar las gracias a las siguientes personas por sus importantes contribuciones a VERT.\",\n\t\t\t\"github_contributors\": \"Contribuidores de GitHub\",\n\t\t\t\"github_description\": \"¡Muchas gracias a todos los que han contribuido! [github_link]¿Quieres contribuir también?[/github_link]\",\n\t\t\t\"no_contributors\": \"Parece que nadie ha contribuido todavía... [contribute_link]¡Sé el primero en hacerlo![/contribute_link]\",\n\t\t\t\"libraries\": \"Librerías\",\n\t\t\t\"libraries_description\": \"Muchas gracias a FFmpeg (audio, vídeo), ImageMagick (imágenes) y Pandoc (documentos) por mantener librerías excelentes por tantos años. VERT depende de ellas para proporcionar tus conversiones.\",\n\t\t\t\"roles\": {\n\t\t\t\t\"lead_developer\": \"Líder de desarrollo; implementación del backend de conversión e interfaz\",\n\t\t\t\t\"developer\": \"Desarrollador; implementación de la interfaz\",\n\t\t\t\t\"designer\": \"Diseñador; UX, branding y marketing\",\n\t\t\t\t\"docker_ci\": \"Mantenimiento del soporte para Docker y CI\",\n\t\t\t\t\"former_cofounder\": \"Excofundador; diseñador\"\n\t\t\t}\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"github_contributors\": \"Ocurrió un error mientras se obtenían los contribuidores de GitHub.\"\n\t\t}\n\t},\n\t\"workers\": {\n\t\t\"errors\": {\n\t\t\t\"general\": \"Ocurrió un error mientras se convertía {file}: {message}\",\n            \"cancel\": \"Error al cancelar la conversión para {file}: {message}\",\n\t\t\t\"magick\": \"Ocurrió un error en el módulo de Magick, la conversión de imágenes puede que no funcione correctamente.\",\n\t\t\t\"ffmpeg\": \"No se pudo cargar FFmpeg, algunas funciones podrían no funcionar.\",\n\t\t\t\"no_audio\": \"No se encontró una pista de audio.\",\n\t\t\t\"invalid_rate\": \"La tasa de muestreo especificada no es válida: {rate}Hz\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "messages/fr.json",
    "content": "{\n\t\"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n\t\"navbar\": {\n\t\t\"upload\": \"Transférer\",\n\t\t\"convert\": \"Convertir\",\n\t\t\"settings\": \"Paramètres\",\n\t\t\"about\": \"A propos\",\n\t\t\"toggle_theme\": \"Changer de thème\"\n\t},\n\t\"footer\": {\n\t\t\"copyright\": \"© {year} VERT.\",\n\t\t\"source_code\": \"Code source\",\n\t\t\"discord_server\": \"Serveur Discord\"\n\t},\n\t\"upload\": {\n\t\t\"title\": \"Le convertisseur de fichiers que vous allez adorer.\",\n\t\t\"subtitle\": \"Tout le traitement des images, des fichiers audio et des documents s'effectue sur votre appareil. Les vidéos sont converties sur nos serveurs ultra-rapides. Aucune limite de taille de fichier, aucune publicité et entièrement open source.\",\n\t\t\"uploader\": {\n\t\t\t\"text\": \"Déposer ou cliquer pour {action}\",\n\t\t\t\"convert\": \"convertir\"\n\t\t},\n\t\t\"cards\": {\n\t\t\t\"title\": \"VERT supports...\",\n\t\t\t\"images\": \"Images\",\n\t\t\t\"audio\": \"Audio\",\n\t\t\t\"documents\": \"Documents\",\n\t\t\t\"video\": \"Video\",\n\t\t\t\"video_server_processing\": \"Serveur pris en charge\",\n\t\t\t\"local_supported\": \"Prise en charge locale\",\n\t\t\t\"status\": {\n\t\t\t\t\"text\": \"<b>Status:</b> {status}\",\n\t\t\t\t\"ready\": \"Prêt\",\n\t\t\t\t\"not_ready\": \"Pas encore prêt\",\n\t\t\t\t\"not_initialized\": \"non initialisé\",\n\t\t\t\t\"downloading\": \"en cours de téléchargement...\",\n\t\t\t\t\"initializing\": \"initialisation...\",\n\t\t\t\t\"unknown\": \"status inconnu\"\n\t\t\t},\n\t\t\t\"supported_formats\": \"Formats supportés:\"\n\t\t},\n\t\t\"tooltip\": {\n\t\t\t\"partial_support\": \"Ce format ne peut être converti qu'en {direction}.\",\n\t\t\t\"direction_input\": \"Entrée (de)\",\n\t\t\t\"direction_output\": \"Sortie (vers)\",\n\t\t\t\"video_server_processing\": \"Les vidéos sont téléchargées sur un serveur pour un traitement par défaut, découvrez comment les configurer localement ici.\"\n\t\t}\n\t},\n\t\"convert\": {\n\t\t\"panel\": {\n\t\t\t\"convert_all\": \"Convertir tout\",\n\t\t\t\"download_all\": \"Télécharger l'ensemble au format .zip\",\n\t\t\t\"remove_all\": \"Supprimer tous les fichiers\",\n\t\t\t\"set_all_to\": \"Tout configurer sur\",\n\t\t\t\"na\": \"N/A\"\n\t\t},\n\t\t\"dropdown\": {\n\t\t\t\"audio\": \"Audio\",\n\t\t\t\"video\": \"Video\",\n\t\t\t\"doc\": \"Document\",\n\t\t\t\"image\": \"Image\",\n\t\t\t\"placeholder\": \"Format de recherche\"\n\t\t},\n\t\t\"tooltips\": {\n\t\t\t\"unknown_file\": \"Type de fichier inconnu\",\n\t\t\t\"audio_file\": \"Fichier audio\",\n\t\t\t\"video_file\": \"Fichier vidéo\",\n\t\t\t\"document_file\": \"Fichier document\",\n\t\t\t\"image_file\": \"Fichier image\",\n\t\t\t\"convert_file\": \"Convertir ce fichier\",\n\t\t\t\"download_file\": \"Télécharger ce fichier\"\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"cant_convert\": \"Nous ne pouvons pas convertir ce fichier\",\n\t\t\t\"vertd_server\": \"Que fais-tu ? Tu es censé exécuter sur le serveur vertd !\",\n\t\t\t\"unsupported_format\": \"Seuls les fichiers image, vidéo, audio et document sont pris en charge\",\n\t\t\t\"vertd_not_found\": \"Impossible de trouver l'instance vertd pour démarrer la conversion vidéo. Êtes-vous sûr que l'URL de l'instance est correctement définie ?\",\n\t\t\t\"worker_downloading\": \"Le convertisseur de {type} est en cours d'initialisation, veuillez patienter quelques instants.\",\n\t\t\t\"worker_error\": \"Le convertisseur de {type} a rencontré une erreur lors de l'initialisation, veuillez réessayer plus tard.\",\n\t\t\t\"worker_timeout\": \"Le convertisseur de {type} prend plus de temps que prévu pour s'initialiser, veuillez patienter quelques instants de plus ou actualiser la page.\",\n\t\t\t\"audio\": \"audio\",\n\t\t\t\"doc\": \"document\",\n\t\t\t\"image\": \"image\"\n\t\t}\n\t},\n\t\"settings\": {\n\t\t\"title\": \"Paramètres\",\n\t\t\"errors\": {\n\t\t\t\"save_failed\": \"Echec lors de l'enregistrement des préférences !\"\n\t\t},\n\t\t\"appearance\": {\n\t\t\t\"title\": \"Appearance\",\n\t\t\t\"brightness_theme\": \"Luminosité du thème\",\n\t\t\t\"brightness_description\": \"Envie d'une soirée ensoleillée ou d'une nuit tranquille et solitaire ?\",\n\t\t\t\"light\": \"Lumineux\",\n\t\t\t\"dark\": \"Sombre\",\n\t\t\t\"effect_settings\": \"Paramètres des effets\",\n\t\t\t\"effect_description\": \"Vous aimez les effets sophistiqués ou préférez une expérience plus statique ?\",\n\t\t\t\"enable\": \"Activer\",\n\t\t\t\"disable\": \"Désactiver\"\n\t\t},\n\t\t\"conversion\": {\n\t\t\t\"title\": \"Conversion\",\n\t\t\t\"advanced_settings\": \"Paramètres avancés\",\n\t\t\t\"filename_format\": \"Format du nom de fichier\",\n\t\t\t\"filename_description\": \"Cela déterminera le nom du fichier lors du téléchargement, <b>sans inclure l'extension du fichier.</b> Vous pouvez mettre les modèles suivants dans le format, qui seront remplacés par les informations pertinentes: <b>%name%</b> pour le nom du fichier d'origine, <b>%extension%</b> pour l'extension du fichier d'origine et <b>%date%</b> pour une chaîne de date indiquant quand le fichier a été converti.\",\n\t\t\t\"placeholder\": \"VERT_%name%\",\n\t\t\t\"default_format\": \"Format de conversion par défaut\",\n\t\t\t\"default_format_enable\": \"Activer\",\n\t\t\t\"default_format_disable\": \"Désactiver\",\n\t\t\t\"default_format_description\": \"Cela modifiera le format par défaut sélectionné lorsque vous téléchargez un fichier de ce type de format.\",\n\t\t\t\"default_format_image\": \"Images\",\n\t\t\t\"default_format_video\": \"Videos\",\n\t\t\t\"default_format_audio\": \"Audio\",\n\t\t\t\"default_format_document\": \"Documents\",\n\t\t\t\"metadata\": \"Métadonnées du fichier\",\n\t\t\t\"metadata_description\": \"Cela modifie si les métadonnées (EXIF, informations sur la chanson, etc.) du fichier d'origine sont conservées dans les fichiers convertis.\",\n\t\t\t\"keep\": \"Conserver\",\n\t\t\t\"remove\": \"Retirer\",\n\t\t\t\"quality\": \"Qualité de conversion\",\n\t\t\t\"quality_description\": \"Cela modifie la qualité de sortie par défaut des fichiers convertis (de son format). Des valeurs plus élevées peuvent entraîner des temps de conversion et une taille de fichier plus longs.\",\n\t\t\t\"quality_video\": \"Cela modifie la qualité de sortie par défaut des fichiers vidéo convertis. Des valeurs plus élevées peuvent allonger le temps de conversion et la taille du fichier.\",\n\t\t\t\"quality_audio\": \"Audio (kbps)\",\n\t\t\t\"quality_images\": \"Image (%)\",\n\t\t\t\"rate\": \"Taux d'échantillonnage (Hz)\"\n\t\t},\n\t\t\"vertd\": {\n\t\t\t\"title\": \"Conversion vidéo\",\n\t\t\t\"status\": \"status:\",\n\t\t\t\"loading\": \"Chargement...\",\n\t\t\t\"available\": \"disponible, identifiant de validation {commitId}\",\n\t\t\t\"unavailable\": \"indisponible (l'url est-elle correcte ?)\",\n\t\t\t\"description\": \"Le projet <code>vertd</code> est un serveur de wrapper utilisant FFmpeg. Il vous permet de convertir des vidéos grâce à l'interface web pratique de VERT'tout en exploitant la puissance de votre GPU pour une exécution rapide.\",\n\t\t\t\"hosting_info\": \"Nous hébergeons une instance publique pour vous faciliter la tâche, mais il est assez facile d'héberger la vôtre sur votre PC ou votre serveur si vous savez ce que vous faites. Vous pouvez télécharger les binaires pour serveur [vertd_link]ici[/vertd_link] - le processus de mise en place deviendra plus facile à l'avenir, alors restez à l'écoute !\",\n\t\t\t\"instance_url\": \"URL de l'instance\",\n\t\t\t\"url_placeholder\": \"Exemple: http://localhost:24153\",\n\t\t\t\"conversion_speed\": \"Vitesse de conversion\",\n\t\t\t\"speed_description\": \"Ceci décrit le compromis entre vitesse et qualité. Des vitesses plus élevées entraîneront une qualité moindre, mais permettront d'effectuer le travail plus rapidement.\",\n\t\t\t\"speeds\": {\n\t\t\t\t\"very_slow\": \"Très lent\",\n\t\t\t\t\"slower\": \"Plus lent\",\n\t\t\t\t\"slow\": \"Lent\",\n\t\t\t\t\"medium\": \"Moyen\",\n\t\t\t\t\"fast\": \"Rapide\",\n\t\t\t\t\"ultra_fast\": \"Ultra Rapide\"\n\t\t\t}\n\t\t},\n\t\t\"privacy\": {\n\t\t\t\"title\": \"Confidentialité\",\n\t\t\t\"plausible_title\": \"Analyses plausibles\",\n\t\t\t\"plausible_description\": \"Nous utilisons [plausible_link]Plausible[/plausible_link], un outil d'analyse axé sur la confidentialité, pour recueillir des statistiques totalement anonymes. Toutes les données sont anonymisées et agrégées, et aucune information identifiable n'est transmise ni stockée. Vous pouvez consulter les analyses [analytics_link]ici[/analytics_link] et choisir de vous désinscrire ci-dessous.\",\n\t\t\t\"opt_in\": \"Inscription\",\n\t\t\t\"opt_out\": \"Désinscription\"\n\t\t},\n\t\t\"language\": {\n\t\t\t\"title\": \"Langue\",\n\t\t\t\"description\": \"Sélectionnez votre langue préférée pour l'interface de VERT\"\n\t\t}\n\t},\n\t\"about\": {\n\t\t\"title\": \"A propos\",\n\t\t\"why\": {\n\t\t\t\"title\": \"Pourquoi VERT?\",\n\t\t\t\"description\": \"<b>Les convertisseurs de fichiers nous ont toujours déçus.</b> Ils sont laids, infestés de publicités et, surtout, lents. Nous avons décidé de résoudre ce problème une fois pour toutes en créant une alternative qui résout tous ces problèmes, et bien plus encore.<br/><br/>Tous les fichiers non vidéo sont entièrement convertis sur l'appareil; cela signifie qu'il n'y a aucun délai entre l'envoi et la réception des fichiers depuis un serveur, et nous ne pouvons jamais espionner les fichiers que vous convertissez.<br/><br/>Les fichiers vidéo sont téléchargés sur notre serveur RTX 4000 Ada ultra-rapide. Vos vidéos y restent pendant une heure si vous ne les convertissez pas. Si vous convertissez le fichier, la vidéo restera sur le serveur pendant une heure, ou jusqu'à son téléchargement. Le fichier sera ensuite supprimé de notre serveur.\"\n\t\t},\n\t\t\"sponsors\": {\n\t\t\t\"title\": \"Sponsors\",\n\t\t\t\"description\": \"Envie de nous soutenir? Contactez un développeur sur le serveur [discord_link]Discord[/discord_link], ou envoyez un courriel à\",\n\t\t\t\"email_copied\": \"Courriel copié dans le presse-papiers !\"\n\t\t},\n\t\t\"resources\": {\n\t\t\t\"title\": \"Resources\",\n\t\t\t\"discord\": \"Discord\",\n\t\t\t\"source\": \"Source\",\n\t\t\t\"email\": \"Courriel\"\n\t\t},\n\t\t\"donate\": {\n\t\t\t\"title\": \"Faire un don à VERT\",\n\t\t\t\"description\": \"Avec votre soutien, nous pouvons continuer à maintenir et à améliorer VERT.\",\n\t\t\t\"one_time\": \"Une fois\",\n\t\t\t\"monthly\": \"Mensuel\",\n\t\t\t\"custom\": \"Personnaliser\",\n\t\t\t\"pay_now\": \"Payer maintenant\",\n\t\t\t\"donate_amount\": \"Faire un don de ${amount} USD\",\n\t\t\t\"thank_you\": \"Merci pour votre don!\",\n\t\t\t\"payment_failed\": \"Paiement échoué: {message}{period} Vous n'avez pas été facturé.\",\n\t\t\t\"donation_error\": \"Une erreur s'est produite lors du traitement de votre don. Veuillez réessayer ultérieurement.\",\n\t\t\t\"payment_error\": \"Erreur lors de la récupération des informations de paiement. Veuillez réessayer ultérieurement.\"\n\t\t},\n\t\t\"credits\": {\n\t\t\t\"title\": \"Credits\",\n\t\t\t\"contact_team\": \"Si vous souhaitez contacter l'équipe de développement, veuillez utiliser le courriel figurant sur la carte \\\"Resources\\\".\",\n\t\t\t\"notable_contributors\": \"Contributeurs notables\",\n\t\t\t\"notable_description\": \"Nous tenons à remercier ces personnes pour leurs contributions majeures à VERT.\",\n\t\t\t\"github_contributors\": \"Les contributeurs de GitHub\",\n\t\t\t\"github_description\": \"Un grand merci à toutes ces personnes pour leur aide ! [github_link]Vous voulez aussi aider ?[/github_link]\",\n\t\t\t\"no_contributors\": \"Il semble que personne n'ait encore contribué... [contribute_link]soyez le premier à contribuer ![/contribute_link]\",\n\t\t\t\"libraries\": \"Bibliothèques\",\n\t\t\t\"libraries_description\": \"un grand merci à FFmpeg (audio, video), ImageMagick (images) et Pandoc (documents) pour avoir maintenu d'aussi excellentes bibliothèques pendant tant d'années, VERT compte sur eux pour vous fournir vos conversions.\",\n\t\t\t\"roles\": {\n\t\t\t\t\"lead_developer\": \"Lead developer; conversion backend, UI implementation\",\n\t\t\t\t\"developer\": \"Developer; UI implementation\",\n\t\t\t\t\"designer\": \"Designer; UX, branding, marketing\",\n\t\t\t\t\"docker_ci\": \"Maintaining Docker & CI support\",\n\t\t\t\t\"former_cofounder\": \"Former co-founder & designer\"\n\t\t\t}\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"github_contributors\": \"Erreur lors de la récupération des contributeurs GitHub\"\n\t\t}\n\t},\n\t\"workers\": {\n\t\t\"errors\": {\n\t\t\t\"general\": \"Erreur de conversion{file}: {message}\",\n\t\t\t\"cancel\": \"Erreur lors de l'annulation de la conversion pour {file}: {message}\",\n\t\t\t\"magick\": \"Erreur depuis Magick Worker, la conversion d'image peut ne pas fonctionner comme prévu.\",\n\t\t\t\"ffmpeg\": \"Erreur lors du chargement de ffmpeg, certaines fonctionnalités peuvent ne pas fonctionner.\",\n\t\t\t\"no_audio\": \"Aucun flux audio détécté.\",\n\t\t\t\"invalid_rate\": \"Taux d'échantillonnage spécifié non valide: {rate}Hz\"\n\t\t}\n\t}\n}"
  },
  {
    "path": "messages/hr.json",
    "content": "{\n\t\"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n\t\"navbar\": {\n\t\t\"upload\": \"Prenesi\",\n\t\t\"convert\": \"Pretvori\",\n\t\t\"settings\": \"Postavke\",\n\t\t\"about\": \"O Stranici\",\n\t\t\"toggle_theme\": \"Promjeni izgled\"\n\t},\n\t\"footer\": {\n\t\t\"copyright\": \"© {year} VERT.\",\n\t\t\"source_code\": \"Source kod\",\n\t\t\"discord_server\": \"Discord server\"\n\t},\n\t\"upload\": {\n\t\t\"title\": \"Pretvarač datoteka koji ćeš obožavati.\",\n\t\t\"subtitle\": \"Cijelokupna obrada slika, zvuka i dokumenata se odvija na vašem uređaju. Videozapisi se pretvaraju na našim izrazito brzim serverima. Nema nikakvih ograničenja veličine niti reklama i potpuno je open source.\",\n\t\t\"uploader\": {\n\t\t\t\"text\": \"Ubaci ili klikni da {action}\",\n\t\t\t\"convert\": \"pretvori\"\n\t\t},\n\t\t\"cards\": {\n\t\t\t\"title\": \"VERT podržava...\",\n\t\t\t\"images\": \"Slike\",\n\t\t\t\"audio\": \"Audio\",\n\t\t\t\"documents\": \"Dokumente\",\n\t\t\t\"video\": \"Video\",\n\t\t\t\"video_server_processing\": \"Server podržan\",\n\t\t\t\"local_supported\": \"Lokalno podržano\",\n\t\t\t\"status\": {\n\t\t\t\t\"text\": \"<b>Status:</b> {status}\",\n\t\t\t\t\"ready\": \"spremno\",\n\t\t\t\t\"not_ready\": \"nespremno\",\n\t\t\t\t\"not_initialized\": \"nije inicijalizirano\",\n\t\t\t\t\"downloading\": \"preuzimanje...\",\n\t\t\t\t\"initializing\": \"inicijaliziranje...\",\n\t\t\t\t\"unknown\": \"nepoznati status\"    \n\t\t\t},\n\t\t\t\"supported_formats\": \"Podržani formati:\"\n\t\t},\n\t\t\"tooltip\": {\n\t\t\t\"partial_support\": \"Ovaj format se može pretvoriti u {direction}.\",\n\t\t\t\"direction_input\": \"ulaz (iz)\",\n\t\t\t\"direction_output\": \"izlaz (u)\",\n\t\t\t\"video_server_processing\": \"Videozapisi se uobičajeno prenose na servere za obradu, nauči ovdje kako namjestiti da se događa lokalno.\"\n\t\t}\n\t},\n\t\"convert\": {\n\t\t\"panel\": {\n\t\t\t\"convert_all\": \"Pretvori sve\",\n\t\t\t\"download_all\": \"Preuzmi sve kao .zip\",\n\t\t\t\"remove_all\": \"Makni sve datoteke\",\n\t\t\t\"set_all_to\": \"Stavi sve na\",\n\t\t\t\"na\": \"N/A\"\n\t\t},\n\t\t\"dropdown\": {\n\t\t\t\"audio\": \"Audio\",\n\t\t\t\"video\": \"Video\",\n\t\t\t\"doc\": \"Dokument\",\n\t\t\t\"image\": \"Slika\",\n\t\t\t\"placeholder\": \"Potraži format\"\n\t\t},\n\t\t\"tooltips\": {\n\t\t\t\"unknown_file\": \"Nepoznat tip datoteke\",\n\t\t\t\"audio_file\": \"Audio datoteka\",\n\t\t\t\"video_file\": \"Video datoteka\",\n\t\t\t\"document_file\": \"Dokument\",\n\t\t\t\"image_file\": \"Datoteka slike\",\n\t\t\t\"convert_file\": \"Pretvori ovu datoteku\",\n\t\t\t\"download_file\": \"Preuzmi ovu datoteku\"\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"cant_convert\": \"Ne možemo pretvoriti ovu datoteku.\",\n\t\t\t\"vertd_server\": \"Sunce ti žarko, što ti radiš!? Moraš pokrenuti vertd server!\",\n\t\t\t\"unsupported_format\": \"Podržane su samo slike, videozapisi, audio i dokumenti\",\n\t\t\t\"vertd_not_found\": \"Nismo mogli pronači vertd da započnemo pretvaranje. Jeste li sigurni da je URL točno postavljen?\",    \n\t\t\t\"worker_downloading\": \"{type} pretvarač se trenutno koristi, molimo pričekajte malo.\",\n\t\t\t\"worker_error\": \"{type} pretvaraču se javila pogreška pri inicijalizaciji, molimo pokušajte ponovno kasnije.\",\n\t\t\t\"worker_timeout\": \"{type} pretvaraču treba duže nego očekivano da se inicijalizira, molimo još malo pričekajte ili osvježite stranicu.\",\n\t\t\t\"audio\": \"audio\",\n\t\t\t\"doc\": \"dokument\",\n\t\t\t\"image\": \"slika\"\n\t\t}\n\t},\n\t\"settings\": {\n\t\t\"title\": \"Postavke\",\n\t\t\"errors\": {\n\t\t\t\"save_failed\": \"Spremanje postavki nije uspjelo!\"\n\t\t},\n\t\t\"appearance\": {\n\t\t\t\"title\": \"Izgled\",\n\t\t\t\"brightness_theme\": \"Svjetlina\",\n\t\t\t\"brightness_description\": \"Želite li da Vas Sunce oslijepi ili tihu umirujuću noć?\",\n\t\t\t\"light\": \"Svijetlo\",\n\t\t\t\"dark\": \"Tamno\",\n\t\t\t\"effect_settings\": \"Efekti\",\n\t\t\t\"effect_description\": \"Želite li zapanjujuće efekte ili miran doživljaj?\",\n\t\t\t\"enable\": \"Uključeno\",\n\t\t\t\"disable\": \"Isključeno\"\n\t\t},\n\t\t\"conversion\": {\n\t\t\t\"title\": \"Pretvaranje\",\n\t\t\t\"filename_format\": \"Način imenovanja datoteke\",\n\t\t\t\"filename_description\": \"Ovo će odrediti ime datoteke pri preuzimanju, <b>ali ne i nastavak.</b> Možete staviti navedene prijedloge u način imenovanja, koji će biti zamijenjeni sa relevatnim informacijama: <b>%name%</b> za originalni naziv datoteke, <b>%extension%</b> za originalni nastavak, i <b>%date%</b> za datum kada je datoteka bila pretvorena.\",\n\t\t\t\"placeholder\": \"VERT_%name%\",\n\t\t\t\"default_format\": \"Zadan format za pretvaranje\",\n\t\t\t\"default_format_description\": \"Ovo će promijeniti zadani format koji je izabran kada prenesete datoteku te vrste.\",\n\t\t\t\"default_format_image\": \"Slike\",\n\t\t\t\"default_format_video\": \"Videozapisi\",\n\t\t\t\"default_format_audio\": \"Audio\",\n\t\t\t\"default_format_document\": \"Dokumenti\",\n\t\t\t\"metadata\": \"Metapodatci datoteke\",\n\t\t\t\"metadata_description\": \"Ovo mijenja spremaju li se ikakvi metapodatci (EXIF, informacije o pjesmi, itd.) sa originalne datoteke na pretvorenu datoteku\",\n\t\t\t\"keep\": \"Ostavi\",\n\t\t\t\"remove\": \"Obriši\",\n\t\t\t\"quality\": \"Kvaliteta pretvaranja\",\n\t\t\t\"quality_description\": \"Ovo mijenja zadanu izlaznu kvalitetu pretvorene datoteke (u svojoj kategoriji). Veći iznosi mogu uzrokovati duže vrijeme za pretvaranje i veličinu.\",\n\t\t\t\"quality_video\": \"Ovo mijenja zadanu izlaznu kvalitetu pretvoranog videozapisa. Veći iznosi mogu uzrokovati duže vrijeme za pretvaranje i veličinu.\",\n\t\t\t\"quality_audio\": \"Audio (kbps)\",\n\t\t\t\"quality_images\": \"Slika (%)\",\n\t\t\t\"rate\": \"Sample rate (Hz)\"\n\t\t},\n\t\t\"vertd\": {\n\t\t\t\"title\": \"Pretvaranje videozapisa\",\n\t\t\t\"status\": \"status:\",\n\t\t\t\"loading\": \"učitavanje...\",\n\t\t\t\"available\": \"dostupno, commit id {commitId}\",\n\t\t\t\"unavailable\": \"nedostupno (Je li URL točan?)\",\n\t\t\t\"description\": \"<code>vertd</code> projekt je serverski omot za FFmpeg. Ovo omogućuje da pretvarate videozapise sa lakoćom VERTovog web sučelja, dok još uvijek možete iskoristiti snagu vašeg GPU da odradi što brže moguće.\",    \n\t\t\t\"hosting_info\": \"Mi držimo javnu instancu za Vašu lakoću, ali je veoma lako hostati na Vašem računalu ili serveru ako znate što radite. Možete preuzeti serverske programe [vertd_link]ovdje[/vertd_link] - Proces namještanja će biti lakši u budućnosti, pa njuškajte malo za nove vijesti!\",\n\t\t\t\"instance_url\": \"URL instance\",\n\t\t\t\"url_placeholder\": \"Na primjer: http://localhost:24153\",\n\t\t\t\"conversion_speed\": \"Brzina pretvaranja\",\n\t\t\t\"speed_description\": \"Ovo opisuje kompromis između brzine i kvalitete. Većom brzinom će izaći manja kvaliteta, ali će se posao brže odraditi.\",\n\t\t\t\"speeds\": {\n\t\t\t\t\"very_slow\": \"Jako Sporo\",\n\t\t\t\t\"slower\": \"Sporije\",\n\t\t\t\t\"slow\": \"Sporo\",\n\t\t\t\t\"medium\": \"Umjereno\",\n\t\t\t\t\"fast\": \"Brzo\",\n\t\t\t\t\"ultra_fast\": \"Veoma Brzo\"\n\t\t\t}\n\t\t},\n\t\t\"privacy\": {\n\t\t\t\"title\": \"Privatnost\",\n\t\t\t\"plausible_title\": \"Plausible analitike\",\n\t\t\t\"plausible_description\": \"Mi koristimo [plausible_link]Plausible[/plausible_link], alat za analitiku koji je fokusiran na privatnost, da prikupimo potpuno anonimne statistike. Svi podatci su anonimizirani i prikupljeni bez ikakvih identificirajućih informacija spremljeno i poslano. Možete vidjeti analitike [analytics_link]ovdje[/analytics_link] i izabrati da ne sudjelujete ispod.\",\n\t\t\t\"opt_in\": \"Sudjelujem\",\n\t\t\t\"opt_out\": \"Ne sudjelujem\"\n\t\t},\n\t\t\"language\": {\n\t\t\t\"title\": \"Jezik\",\n\t\t\t\"description\": \"Izaberi svoj preferirani jezik za VERTovo sučelje.\"\n\t\t}\n\t},\n\t\"about\": {\n\t\t\"title\": \"O stranici\",\n\t\t\"why\": {\n\t\t\t\"title\": \"Zašto baš VERT?\",\n\t\t\t\"description\": \"<b>Pretvarači datoteka su nas uvijek razočarali.</b> Izuzetno su ružni, prepuni reklama, i najvažnije; spori! Odlučili smo riješiti problem jednom i zauvijek praveći alternativu koja riješava sve ove probleme, i više.<br/><br/>Sve datoteke koji nisu videozapisi su pretvoreni direktno na Vašem uređaju; To znači da nema nikakve stanke između slanja i primanja datoteka sa servera, i nikada ne dobijemo šansu gurati nos u vaše datoteke koje pretvarate. <br/><br/>Videozapisi se prenose na naše izuzetno brze RTX 4000 Ada servere. Vaši videozapisi tamo ostano sat vremena ako ih ne pretvorite. Ako ih i pretvorite, videozapis će ostati na serveru na sat vremena, ili dok se ne preuzme. Datoteka će zatim biti obrisana sa našeg servera.\"\n\t\t},\n\t\t\"sponsors\": {\n\t\t\t\"title\": \"Sponzori\",\n\t\t\t\"description\": \"Želite li nas podržati? Kontaktirajte developera na [discord_link]Discord[/discord_link] serveru, ili pošaljite mail na\",\n\t\t\t\"email_copied\": \"Email kopiran u međuspremnik!\"\n\t\t},\n\t\t\"resources\": {\n\t\t\t\"title\": \"Resursi\",\n\t\t\t\"discord\": \"Discord\",\n\t\t\t\"source\": \"Source kod\",\n\t\t\t\"email\": \"Email\"\n\t\t},\n\t\t\"donate\": {\n\t\t\t\"title\": \"Donirajte nam\",\n\t\t\t\"description\": \"Sa vašom podrškom mi možemo nastaviti održavati i poboljšavati VERT.\",\n\t\t\t\"one_time\": \"Jednokratno\",\n\t\t\t\"monthly\": \"Mjesečno\",\n\t\t\t\"custom\": \"Prilagođeno\",\n\t\t\t\"pay_now\": \"Plati sada\",\n\t\t\t\"donate_amount\": \"Doniraj ${amount} USD\",\n\t\t\t\"thank_you\": \"Hvala Vam na Vašoj donaciji!!\",\n\t\t\t\"payment_failed\": \"Plaćanje neuspjelo: {message}{period} Niste naplaćeni.\",\n\t\t\t\"donation_error\": \"Dogodila se pogreška pri obradi donacije. Molimo pokušajte kasnije.\",\n\t\t\t\"payment_error\": \"Dogodila se pogreška pri prihvaćanju detalja o naplati. Molimo pokušajte kasnije.\"\n\t\t},\n\t\t\"credits\": {\n\t\t\t\"title\": \"Zasluge\",\n\t\t\t\"contact_team\": \"Ako želite kontaktirati developere, molimo koristite email koji se nalazi u odjeljku \\\"resursi\\\".\",\n\t\t\t\"notable_contributors\": \"Značajni suradnici\",\n\t\t\t\"notable_description\": \"Želimo zahvaliti ovim ljudima za njihove ogromne doprinose VERTu.\",\n\t\t\t\"github_contributors\": \"GitHub suradnici\",\n\t\t\t\"github_description\": \"Velike zahvale svim ovim ljudima koji su nam pomogli! [github_link]Želiš nam i ti pomoći?[/github_link]\",\n\t\t\t\"no_contributors\": \"Čini se kako nitko nije još doprinio... [contribute_link]budite prvi koji će doprinjeti![/contribute_link]\",\n\t\t\t\"libraries\": \"Biblioteke\",\n\t\t\t\"libraries_description\": \"Velike zahvale prema FFmpeg (audio, video), ImageMagick (slike) i Pandoc (dokumenti) što su održavali tako odlične biblioteke svih ovih godina. VERT se oslanja na njih da bi Vam pružili pretvorbu.\",\n\t\t\t\"roles\": {\n\t\t\t\t\"lead_developer\": \"Glavni developer; Pretvarački backend, UI implementacija\",\n\t\t\t\t\"developer\": \"Developer; UI implementacija\",\n\t\t\t\t\"designer\": \"Dizajner; UX, branding, marketing\",\n\t\t\t\t\"docker_ci\": \"Održavanje Dockera i CI support\",\n\t\t\t\t\"former_cofounder\": \"Prijašnji suosnivač i dizajner\"\n\t\t\t}\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"github_contributors\": \"Pogreška pri prikupljanju GitHub suradnika\"\n\t\t}\n\t},\n\t\"workers\": {\n\t\t\"errors\": {\n\t\t\t\"general\": \"Pogreška pri pretvaranju {file}: {message}\",\n\t\t\t\"magick\": \"Pogreška sa Magick radnikom, pretvorba slike možda neće raditi kao očekivano.\",\n\t\t\t\"ffmpeg\": \"Greška pri učitavanju ffmpeg, neke značajke možda neće raditi.\",\n\t\t\t\"no_audio\": \"Nije pronađen audio.\",\n\t\t\t\"invalid_rate\": \"Upisan nevažeći sample rate: {rate}Hz!\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "messages/id.json",
    "content": "{\n\t\"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n\t\"navbar\": {\n\t\t\"upload\": \"Unggah\",\n\t\t\"convert\": \"Konversi\",\n\t\t\"settings\": \"Pengaturan\",\n\t\t\"about\": \"Tentang\",\n\t\t\"toggle_theme\": \"Ganti Tema\"\n\t},\n\t\"footer\": {\n\t\t\"copyright\": \"© {year} VERT.\",\n\t\t\"source_code\": \"Kode sumber\",\n\t\t\"discord_server\": \"Peladen Discord\"\n\t},\n\t\"upload\": {\n\t\t\"title\": \"Konverter berkas andalanmu.\",\n\t\t\"subtitle\": \"Semua gambar, audio, dan pemrosesan dokumen dilakukan pada perangkatmu. Video dikonversi pada peladen kilat kami. Tidak ada batas ukuran berkas, tidak ada iklan, dan benar-benar sumber terbuka.\",\n\t\t\"uploader\": {\n\t\t\t\"text\": \"Jatuhkan dan klik untuk {action}\",\n\t\t\t\"convert\": \"Konversi\",\n\t\t\t\"jpegify\": \"jpegify\"\n\t\t},\n\t\t\"cards\": {\n\t\t\t\"title\": \"Dapat Ditangani VERT ...\",\n\t\t\t\"images\": \"Gambar\",\n\t\t\t\"audio\": \"Audio\",\n\t\t\t\"documents\": \"Dokumen\",\n\t\t\t\"video\": \"Video\",\n\t\t\t\"video_server_processing\": \"Proses di Server\",\n\t\t\t\"local_supported\": \"Proses di Lokal\",\n\t\t\t\"status\": {\n\t\t\t\t\"text\": \"<b>Status:</b> {status}\",\n\t\t\t\t\"ready\": \"siap\",\n\t\t\t\t\"not_ready\": \"belum siap\",\n\t\t\t\t\"not_initialized\": \"tidak terinisialisasi\",\n\t\t\t\t\"downloading\": \"mengunduh...\",\n\t\t\t\t\"initializing\": \"menginisialisasi...\",\n\t\t\t\t\"unknown\": \"status tidak diketahui\"\n\t\t\t},\n\t\t\t\"supported_formats\": \"Format yang didukung:\"\n\t\t},\n\t\t\"tooltip\": {\n\t\t\t\"partial_support\": \"Format ini hanya dapat dikonversi ke {direction}.\",\n\t\t\t\"direction_input\": \"sumber asal (dari)\",\n\t\t\t\"direction_output\": \"target (ke)\",\n\t\t\t\"video_server_processing\": \"Video upload ke server untuk diproses secara baku, belajar bagaimana mengaturnya di sini.\"\n\t\t}\n\t},\n\t\"convert\": {\n\t\t\"external_warning\": {\n\t\t\t\"title\": \"Peringatan server eksternal\",\n\t\t\t\"text\": \"Jika kamu memilih untuk mengonversi ke format video, berkas tersebut akan diunggah ke server eksternal untuk dikonversi. Apakah kamu ingin melanjutkan?\",\n\t\t\t\"yes\": \"Ya\",\n\t\t\t\"no\": \"Tidak\"\n\t\t},\n\t\t\"panel\": {\n\t\t\t\"convert_all\": \"Konversi semua\",\n\t\t\t\"download_all\": \"Unduh semua sebagai .zip\",\n\t\t\t\"remove_all\": \"Hapus semua berkas\",\n\t\t\t\"set_all_to\": \"Atur semua ke\",\n\t\t\t\"na\": \"N/A\"\n\t\t},\n\t\t\"dropdown\": {\n\t\t\t\"audio\": \"Audio\",\n\t\t\t\"video\": \"Video\",\n\t\t\t\"doc\": \"Dokumen\",\n\t\t\t\"image\": \"Gambar\",\n\t\t\t\"placeholder\": \"Cari format\"\n\t\t},\n\t\t\"tooltips\": {\n\t\t\t\"unknown_file\": \"Jenis berkas tidak diketahui\",\n\t\t\t\"audio_file\": \"Berkas audio\",\n\t\t\t\"video_file\": \"Berkas video\",\n\t\t\t\"document_file\": \"Berkas dokumen\",\n\t\t\t\"image_file\": \"Berkas gambar\",\n\t\t\t\"convert_file\": \"Konversi berkas ini\",\n\t\t\t\"download_file\": \"Unduh berkas ini\"\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"cant_convert\": \"Kami tidak dapat mengonversi berkas ini.\",\n\t\t\t\"vertd_server\": \"apa yang kamu lakukan..? kamu seharusnya menjalankan peladen vertd!\",\n\t\t\t\"vertd_generic_body\": \"Terjadi galat saat mencoba mengonversi video kamu. Apakah kamu ingin mengirimkan video ini ke pengembang untuk membantu memperbaiki kutu ini? Hanya berkas video kamu yang akan dikirim. Tidak ada data identifikasi yang diunggah.\",\n\t\t\t\"vertd_generic_title\": \"Konversi video galat\",\n\t\t\t\"vertd_generic_yes\": \"Kirim video\",\n\t\t\t\"vertd_generic_no\": \"Jangan kirim\",\n\t\t\t\"vertd_failed_to_keep\": \"Gagal menyimpan video di peladen: {error}\",\n\t\t\t\"unsupported_format\": \"Hanya berkas gambar, video, audio, dan dokumen yang didukung\",\n\t\t\t\"vertd_not_found\": \"Tidak dapat menemukan layanan vertd untuk memulai konversi video. Apakah URL layanan sudah diatur dengan benar?\",\n\t\t\t\"worker_downloading\": \"Konverter {type} sedang diinisialisasi, harap tunggu beberapa saat.\",\n\t\t\t\"worker_error\": \"Konverter {type} mengalami kesalahan saat inisialisasi, coba lagi nanti.\",\n\t\t\t\"worker_timeout\": \"Konverter {type} memerlukan waktu lebih lama dari perkiraan untuk inisialisasi, harap tunggu beberapa saat lagi atau segarkan halaman.\",\n\t\t\t\"audio\": \"audio\",\n\t\t\t\"doc\": \"dokumen\",\n\t\t\t\"image\": \"gambar\"\n\t\t}\n\t},\n\t\"settings\": {\n\t\t\"title\": \"Pengaturan\",\n\t\t\"errors\": {\n\t\t\t\"save_failed\": \"Gagal menyimpan pengaturan!\"\n\t\t},\n\t\t\"appearance\": {\n\t\t\t\"title\": \"Tampilan\",\n\t\t\t\"brightness_theme\": \"Tema kecerahan\",\n\t\t\t\"brightness_description\": \"Ingin suasana terang benderang, atau malam yang sunyi?\",\n\t\t\t\"light\": \"Terang\",\n\t\t\t\"dark\": \"Gelap\",\n\t\t\t\"effect_settings\": \"Pengaturan efek\",\n\t\t\t\"effect_description\": \"Ingin efek keren, atau tampilan yang lebih sederhana?\",\n\t\t\t\"enable\": \"Aktifkan\",\n\t\t\t\"disable\": \"Nonaktifkan\"\n\t\t},\n\t\t\"conversion\": {\n\t\t\t\"title\": \"Konversi\",\n\t\t\t\"advanced_settings\": \"Pengaturan lanjutan\",\n\t\t\t\"filename_format\": \"Format nama berkas\",\n\t\t\t\"filename_description\": \"Ini akan menentukan nama berkas saat diunduh, <b>tidak termasuk ekstensi berkas.</b> Kamu dapat menggunakan template berikut dalam format, yang akan diganti dengan informasi terkait: <b>%name%</b> untuk nama berkas asli, <b>%extension%</b> untuk ekstensi berkas asli, dan <b>%date%</b> untuk tanggal saat berkas dikonversi.\",\n\t\t\t\"placeholder\": \"VERT_%name%\",\n\t\t\t\"default_format\": \"Format konversi baku\",\n\t\t\t\"default_format_description\": \"Ini akan mengubah format baku yang dipilih saat kamu mengunggah berkas dengan tipe tersebut.\",\n\t\t\t\"default_format_image\": \"Gambar\",\n\t\t\t\"default_format_video\": \"Video\",\n\t\t\t\"default_format_audio\": \"Audio\",\n\t\t\t\"default_format_document\": \"Dokumen\",\n\t\t\t\"metadata\": \"Metadata berkas\",\n\t\t\t\"metadata_description\": \"Menentukan apakah metadata (EXIF, info lagu, dll.) dari berkas asli akan dipertahankan di berkas hasil konversi.\",\n\t\t\t\"keep\": \"Pertahankan\",\n\t\t\t\"remove\": \"Hapus\",\n\t\t\t\"quality\": \"Kualitas konversi\",\n\t\t\t\"quality_description\": \"Mengubah kualitas keluaran baku berkas hasil konversi. Nilai yang lebih tinggi dapat menghasilkan waktu konversi dan ukuran berkas yang lebih besar.\",\n\t\t\t\"quality_video\": \"Mengubah kualitas keluaran baku berkas video hasil konversi. Nilai yang lebih tinggi dapat memperpanjang waktu dan ukuran berkas.\",\n\t\t\t\"quality_audio\": \"Audio (kbps)\",\n\t\t\t\"quality_images\": \"Gambar (%)\",\n\t\t\t\"rate\": \"Laju sampel (Hz)\"\n\t\t},\n\t\t\"vertd\": {\n\t\t\t\"title\": \"Konversi video\",\n\t\t\t\"status\": \"status:\",\n\t\t\t\"loading\": \"memuat...\",\n\t\t\t\"available\": \"tersedia, commit id {commitId}\",\n\t\t\t\"unavailable\": \"tidak tersedia (apakah URL sudah benar?)\",\n\t\t\t\"description\": \"Proyek <code>vertd</code> adalah server wrapper untuk FFmpeg. Ini memungkinkan kamu mengonversi video melalui antarmuka web VERT, sambil memanfaatkan kekuatan GPU untuk mempercepat proses.\",\n\t\t\t\"hosting_info\": \"Kami menyediakan instance publik untuk kemudahanmu, tetapi kamu juga bisa dengan mudah meng-host sendiri di PC atau server jika tahu caranya. Kamu dapat mengunduh binary server [vertd_link]di sini[/vertd_link] - proses penyiapan akan semakin mudah di masa depan, jadi tetap pantau!\",\n\t\t\t\"instance\": \"Instance\",\n\t\t\t\"url_placeholder\": \"Contoh: http://localhost:24153\",\n\t\t\t\"conversion_speed\": \"Kecepatan konversi\",\n\t\t\t\"speed_description\": \"Menjelaskan kompromi antara kecepatan dan kualitas. Kecepatan lebih tinggi menghasilkan kualitas lebih rendah, tetapi proses lebih cepat.\",\n\t\t\t\"speeds\": {\n\t\t\t\t\"very_slow\": \"Sangat Lambat\",\n\t\t\t\t\"slower\": \"Agak Lambat\",\n\t\t\t\t\"slow\": \"Lambat\",\n\t\t\t\t\"medium\": \"Sedang\",\n\t\t\t\t\"fast\": \"Cepat\",\n\t\t\t\t\"ultra_fast\": \"Sangat Cepat\"\n\t\t\t},\n\t\t\t\"auto_instance\": \"Otomatis (disarankan)\",\n\t\t\t\"eu_instance\": \"Falkenstein, Jerman\",\n\t\t\t\"us_instance\": \"Washington, AS\",\n\t\t\t\"custom_instance\": \"Kustom\"\n\t\t},\n\t\t\"privacy\": {\n\t\t\t\"title\": \"Privasi & data\",\n\t\t\t\"plausible_title\": \"Analitik Plausible\",\n\t\t\t\"plausible_description\": \"Kami menggunakan [plausible_link]Plausible[/plausible_link], alat analitik yang berfokus pada privasi, untuk mengumpulkan statistik anonim sepenuhnya. Semua data dianonimkan dan diagregasi, tanpa informasi yang dapat diidentifikasi. Kamu dapat melihat analitiknya [analytics_link]di sini[/analytics_link] dan memilih untuk keluar di bawah.\",\n\t\t\t\"opt_in\": \"Ikut serta\",\n\t\t\t\"opt_out\": \"Tidak ikut\",\n\t\t\t\"cache_title\": \"Manajemen cache\",\n\t\t\t\"cache_description\": \"Kami menyimpan berkas konverter di browser agar kamu tidak perlu mengunduh ulang setiap kali, meningkatkan performa dan menghemat data.\",\n\t\t\t\"refresh_cache\": \"Segarkan cache\",\n\t\t\t\"clear_cache\": \"Hapus cache\",\n\t\t\t\"files_cached\": \"{size} ({count} berkas)\",\n\t\t\t\"loading_cache\": \"Memuat...\",\n\t\t\t\"total_size\": \"Total Ukuran\",\n\t\t\t\"files_cached_label\": \"File Tersimpan\",\n\t\t\t\"cache_cleared\": \"Cache berhasil dihapus!\",\n\t\t\t\"cache_clear_error\": \"Gagal menghapus cache.\"\n\t\t},\n\t\t\"language\": {\n\t\t\t\"title\": \"Bahasa\",\n\t\t\t\"description\": \"Pilih bahasa pilihanmu untuk antarmuka VERT.\"\n\t\t}\n\t},\n\t\"about\": {\n\t\t\"title\": \"Tentang\",\n\t\t\"why\": {\n\t\t\t\"title\": \"Mengapa VERT?\",\n\t\t\t\"description\": \"<b>Konverter berkas selalu mengecewakan kami.</b> Mereka jelek, penuh iklan, dan yang paling penting; lambat. Kami memutuskan untuk menyelesaikan masalah ini sekali untuk selamanya dengan membuat alternatif yang memperbaiki semua masalah itu, dan lebih banyak lagi.<br/><br/>Semua berkas non-video dikonversi sepenuhnya di perangkat; artinya tidak ada jeda antara pengiriman dan penerimaan berkas, dan kami tidak pernah melihat berkas yang kamu konversi.<br/><br/>File video diunggah ke server RTX 4000 Ada super cepat kami. Videomu akan tetap di sana selama satu jam jika tidak dikonversi. Jika dikonversi, video akan bertahan satu jam atau hingga diunduh. Setelah itu, berkas akan dihapus dari server kami.\"\n\t\t},\n\t\t\"sponsors\": {\n\t\t\t\"title\": \"Sponsor\",\n\t\t\t\"description\": \"Ingin mendukung kami? Hubungi pengembang di server [discord_link]Discord[/discord_link], atau kirim email ke\",\n\t\t\t\"email_copied\": \"Email disalin ke clipboard!\"\n\t\t},\n\t\t\"resources\": {\n\t\t\t\"title\": \"Sumber daya\",\n\t\t\t\"discord\": \"Discord\",\n\t\t\t\"source\": \"Sumber\",\n\t\t\t\"email\": \"Email\"\n\t\t},\n\t\t\"donate\": {\n\t\t\t\"title\": \"Donasi untuk VERT\",\n\t\t\t\"description\": \"Dengan dukunganmu, kami dapat terus memelihara dan meningkatkan VERT.\",\n\t\t\t\"one_time\": \"Sekali\",\n\t\t\t\"monthly\": \"Bulanan\",\n\t\t\t\"custom\": \"Kustom\",\n\t\t\t\"pay_now\": \"Bayar sekarang\",\n\t\t\t\"donate_amount\": \"Donasi ${amount} USD\",\n\t\t\t\"thank_you\": \"Terima kasih atas donasimu!\",\n\t\t\t\"payment_failed\": \"Pembayaran gagal: {message}{period} Kamu tidak dikenai biaya.\",\n\t\t\t\"donation_error\": \"Terjadi kesalahan saat memproses donasi. Coba lagi nanti.\",\n\t\t\t\"payment_error\": \"Kesalahan mengambil detail pembayaran. Coba lagi nanti.\"\n\t\t},\n\t\t\"credits\": {\n\t\t\t\"title\": \"Kredit\",\n\t\t\t\"contact_team\": \"Jika kamu ingin menghubungi tim pengembang, gunakan email yang ada di kartu \\\"Sumber Daya\\\".\",\n\t\t\t\"notable_contributors\": \"Kontributor penting\",\n\t\t\t\"notable_description\": \"Kami ingin berterima kasih kepada orang-orang ini atas kontribusi besar mereka untuk VERT.\",\n\t\t\t\"github_contributors\": \"Kontributor GitHub\",\n\t\t\t\"github_description\": \"[jpegify_link]Terima kasih[/jpegify_link] banyak kepada semua orang yang telah membantu! [github_link]Ingin membantu juga?[/github_link]\",\n\t\t\t\"no_contributors\": \"Sepertinya belum ada yang berkontribusi... [contribute_link]jadilah yang pertama berkontribusi![/contribute_link]\",\n\t\t\t\"libraries\": \"Pustaka\",\n\t\t\t\"libraries_description\": \"Terima kasih besar kepada FFmpeg (audio, video), ImageMagick (gambar), dan Pandoc (dokumen) atas pemeliharaannya selama bertahun-tahun. VERT bergantung pada mereka untuk menyediakan konversi berkas.\",\n\t\t\t\"roles\": {\n\t\t\t\t\"lead_developer\": \"Pengembang utama; backend konversi, implementasi UI\",\n\t\t\t\t\"developer\": \"Pengembang; implementasi UI\",\n\t\t\t\t\"designer\": \"Desainer; UX, branding, pemasaran\",\n\t\t\t\t\"docker_ci\": \"Pemeliharaan Docker & CI\",\n\t\t\t\t\"former_cofounder\": \"Mantan co-founder & desainer\"\n\t\t\t}\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"github_contributors\": \"Kesalahan mengambil kontributor GitHub\"\n\t\t}\n\t},\n\t\"workers\": {\n\t\t\"errors\": {\n\t\t\t\"general\": \"Kesalahan mengonversi {file}: {message}\",\n\t\t\t\"cancel\": \"Kesalahan membatalkan konversi untuk {file}: {message}\",\n\t\t\t\"magick\": \"Kesalahan di worker Magick, konversi gambar mungkin tidak berfungsi dengan benar.\",\n\t\t\t\"ffmpeg\": \"Kesalahan memuat ffmpeg, beberapa fitur mungkin tidak berfungsi.\",\n\t\t\t\"no_audio\": \"Tidak ditemukan aliran audio.\",\n\t\t\t\"invalid_rate\": \"Laju sampel tidak valid: {rate}Hz\"\n\t\t}\n\t},\n\t\"jpegify\": {\n\t\t\"title\": \"JPEGIFY RAHASIA!!!\",\n\t\t\"subtitle\": \"(psst... jangan beri tahu siapa pun!)\",\n\t\t\"button\": \"JPEGIFY {compression}%!!!\",\n\t\t\"download\": \"Unduh\",\n\t\t\"delete\": \"Hapus\"\n\t}\n}"
  },
  {
    "path": "messages/it.json",
    "content": "{\n\t\"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n\t\"navbar\": {\n\t\t\"upload\": \"Carica\",\n\t\t\"convert\": \"Converti\",\n\t\t\"settings\": \"Impostazioni\",\n\t\t\"about\": \"Informazioni\",\n\t\t\"toggle_theme\": \"Cambia tema\"\n\t},\n\t\"footer\": {\n\t\t\"copyright\": \"© {year} VERT.\",\n\t\t\"source_code\": \"Codice sorgente\",\n\t\t\"discord_server\": \"Server Discord\"\n\t},\n\t\"upload\": {\n\t\t\"title\": \"Il convertitore di file che amerai.\",\n\t\t\"subtitle\": \"Tutta l'elaborazione di immagini, audio e documenti avviene sul tuo dispositivo. I video sono convertiti sui nostri server velocissimi. Nessun limite di dimensione, nessuna pubblicità e completamente open source.\",\n\t\t\"uploader\": {\n\t\t\t\"text\": \"Trascina o clicca per {action}\",\n\t\t\t\"convert\": \"convertire\"\n\t\t},\n\t\t\"cards\": {\n\t\t\t\"title\": \"VERT supporta...\",\n\t\t\t\"images\": \"Immagini\",\n\t\t\t\"audio\": \"Audio\",\n\t\t\t\"documents\": \"Documenti\",\n\t\t\t\"video\": \"Video\",\n\t\t\t\"video_server_processing\": \"Supportato da server\",\n\t\t\t\"local_supported\": \"Supportato in locale\",\n\t\t\t\"status\": {\n\t\t\t\t\"text\": \"<b>Stato:</b> {status}\",\n\t\t\t\t\"ready\": \"pronto\",\n\t\t\t\t\"not_ready\": \"non pronto\",\n\t\t\t\t\"not_initialized\": \"non inizializzato\",\n\t\t\t\t\"downloading\": \"download in corso...\",\n\t\t\t\t\"initializing\": \"inizializzazione in corso...\",\n\t\t\t\t\"unknown\": \"stato sconosciuto\"\n\t\t\t},\n\t\t\t\"supported_formats\": \"Formati supportati:\"\n\t\t},\n\t\t\"tooltip\": {\n\t\t\t\"partial_support\": \"Questo formato può essere convertito solo come {direction}.\",\n\t\t\t\"direction_input\": \"input (da)\",\n\t\t\t\"direction_output\": \"output (a)\",\n\t\t\t\"video_server_processing\": \"Per impostazione predefinita, i video vengono caricati su un server per l'elaborazione. Scopri come configurarlo in locale qui.\"\n\t\t}\n\t},\n\t\"convert\": {\n\t\t\"external_warning\": {\n\t\t\t\"title\": \"Avviso server esterno\",\n\t\t\t\"text\": \"Se scegli di convertire in un formato video, quei file verranno caricati su un server esterno per essere convertiti. Vuoi continuare?\",\n\t\t\t\"yes\": \"Sì\",\n\t\t\t\"no\": \"No\"\n\t\t},\n\t\t\"panel\": {\n\t\t\t\"convert_all\": \"Converti tutti\",\n\t\t\t\"download_all\": \"Scarica tutti come .zip\",\n\t\t\t\"remove_all\": \"Rimuovi tutti i file\",\n\t\t\t\"set_all_to\": \"Imposta tutti a\",\n\t\t\t\"na\": \"N/D\"\n\t\t},\n\t\t\"dropdown\": {\n\t\t\t\"audio\": \"Audio\",\n\t\t\t\"video\": \"Video\",\n\t\t\t\"doc\": \"Documento\",\n\t\t\t\"image\": \"Immagine\",\n\t\t\t\"placeholder\": \"Cerca formato\"\n\t\t},\n\t\t\"tooltips\": {\n\t\t\t\"unknown_file\": \"Tipo di file sconosciuto\",\n\t\t\t\"audio_file\": \"File audio\",\n\t\t\t\"video_file\": \"File video\",\n\t\t\t\"document_file\": \"File documento\",\n\t\t\t\"image_file\": \"File immagine\",\n\t\t\t\"convert_file\": \"Converti questo file\",\n\t\t\t\"download_file\": \"Scarica questo file\"\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"cant_convert\": \"Non possiamo convertire questo file.\",\n\t\t\t\"vertd_server\": \"cosa stai facendo...? dovresti eseguire il server vertd!\",\n\t\t\t\"vertd_generic_body\": \"Si è verificato un errore durante il tentativo di conversione del tuo video. Vuoi inviare questo video agli sviluppatori per aiutare a risolvere questo bug? Verrà inviato solo il tuo file video. Nessun identificatore sarà caricato.\",\n\t\t\t\"vertd_generic_title\": \"Errore di conversione video\",\n\t\t\t\"vertd_generic_yes\": \"Invia video\",\n\t\t\t\"vertd_generic_no\": \"Non inviare\",\n\t\t\t\"vertd_failed_to_keep\": \"Impossibile mantenere il video sul server: {error}\",\n\t\t\t\"unsupported_format\": \"Sono supportati solo file immagine, video, audio e documento\",\n\t\t\t\"vertd_not_found\": \"Impossibile trovare l'istanza vertd per avviare la conversione video. Sei sicuro che l'URL dell'istanza sia impostato correttamente?\",\n\t\t\t\"worker_downloading\": \"Il convertitore {type} è attualmente in fase di inizializzazione, attendi qualche istante.\",\n\t\t\t\"worker_error\": \"Il convertitore {type} ha avuto un errore durante l'inizializzazione, riprova più tardi.\",\n\t\t\t\"worker_timeout\": \"Il convertitore {type} sta impiegando più del previsto per inizializzare, attendi ancora qualche istante o aggiorna la pagina.\",\n\t\t\t\"audio\": \"audio\",\n\t\t\t\"doc\": \"documento\",\n\t\t\t\"image\": \"immagine\"\n\t\t}\n\t},\n\t\"settings\": {\n\t\t\"title\": \"Impostazioni\",\n\t\t\"errors\": {\n\t\t\t\"save_failed\": \"Impossibile salvare le impostazioni!\"\n\t\t},\n\t\t\"appearance\": {\n\t\t\t\"title\": \"Aspetto\",\n\t\t\t\"brightness_theme\": \"Tema luminosità\",\n\t\t\t\"brightness_description\": \"Vuoi un lampo di sole, o una tranquilla notte solitaria?\",\n\t\t\t\"light\": \"Chiaro\",\n\t\t\t\"dark\": \"Scuro\",\n\t\t\t\"effect_settings\": \"Impostazioni effetti\",\n\t\t\t\"effect_description\": \"Desideri effetti *fancy*, o un'esperienza più statica?\",\n\t\t\t\"enable\": \"Abilita\",\n\t\t\t\"disable\": \"Disabilita\"\n\t\t},\n\t\t\"conversion\": {\n\t\t\t\"title\": \"Conversione\",\n\t\t\t\"advanced_settings\": \"Impostazioni avanzate\",\n\t\t\t\"filename_format\": \"Formato nome file\",\n\t\t\t\"filename_description\": \"Questo determinerà il nome del file al momento del download, <b>esclusa l'estensione del file.</b> È possibile inserire i seguenti *template* nel formato, che verranno sostituiti con le informazioni pertinenti: <b>%name%</b> per il nome del file originale, <b>%extension%</b> per l'estensione del file originale e <b>%date%</b> per una *stringa* di data di quando il file è stato convertito.\",\n\t\t\t\"placeholder\": \"VERT_%name%\",\n\t\t\t\"default_format\": \"Formato di conversione predefinito\",\n\t\t\t\"default_format_description\": \"Questo cambierà il formato predefinito selezionato quando carichi un file di questo tipo.\",\n\t\t\t\"default_format_image\": \"Immagini\",\n\t\t\t\"default_format_video\": \"Video\",\n\t\t\t\"default_format_audio\": \"Audio\",\n\t\t\t\"default_format_document\": \"Documenti\",\n\t\t\t\"metadata\": \"Metadati del file\",\n\t\t\t\"metadata_description\": \"Questo cambia se eventuali metadati (EXIF, informazioni sul brano, ecc.) del file originale vengono conservati nei file convertiti.\",\n\t\t\t\"keep\": \"Mantieni\",\n\t\t\t\"remove\": \"Rimuovi\",\n\t\t\t\"quality\": \"Qualità di conversione\",\n\t\t\t\"quality_description\": \"Questo cambia la qualità di output predefinita dei file convertiti (nella sua categoria). Valori più alti possono comportare tempi di conversione più lunghi e dimensioni maggiori.\",\n\t\t\t\"quality_video\": \"Questo cambia la qualità di output predefinita dei file video convertiti. Valori più alti possono comportare tempi di conversione più lunghi e dimensioni maggiori.\",\n\t\t\t\"quality_audio\": \"Audio (kbps)\",\n\t\t\t\"quality_images\": \"Immagine (%)\",\n\t\t\t\"rate\": \"Frequenza di campionamento (Hz)\"\n\t\t},\n\t\t\"vertd\": {\n\t\t\t\"title\": \"Conversione video\",\n\t\t\t\"status\": \"stato:\",\n\t\t\t\"loading\": \"caricamento...\",\n\t\t\t\"available\": \"disponibile, ID commit {commitId}\",\n\t\t\t\"unavailable\": \"non disponibile (l'URL è corretto?)\",\n\t\t\t\"description\": \"Il progetto <code>vertd</code> è un *server wrapper* per FFmpeg. Questo ti permette di convertire video attraverso la comodità dell'interfaccia web di VERT, pur essendo in grado di sfruttare la potenza della tua GPU per farlo il più rapidamente possibile.\",\n\t\t\t\"hosting_info\": \"Ospitiamo un'istanza pubblica per la tua comodità, ma è abbastanza facile ospitarne una tua sul tuo PC o server se sai cosa stai facendo. Puoi scaricare i binari del server [vertd_link]qui[/vertd_link] - il processo di configurazione diventerà più semplice in futuro, quindi resta sintonizzato!\",\n\t\t\t\"instance\": \"Istanza\",\n\t\t\t\"url_placeholder\": \"Esempio: http://localhost:24153\",\n\t\t\t\"conversion_speed\": \"Velocità di conversione\",\n\t\t\t\"speed_description\": \"Questo descrive il compromesso tra velocità e qualità. Velocità maggiori si tradurranno in una qualità inferiore, ma completeranno il lavoro più velocemente.\",\n\t\t\t\"speeds\": {\n\t\t\t\t\"very_slow\": \"Molto Lento\",\n\t\t\t\t\"slower\": \"Più Lento\",\n\t\t\t\t\"slow\": \"Lento\",\n\t\t\t\t\"medium\": \"Medio\",\n\t\t\t\t\"fast\": \"Veloce\",\n\t\t\t\t\"ultra_fast\": \"Ultra Veloce\"\n\t\t\t},\n\t\t\t\"auto_instance\": \"Automatico (consigliato)\",\n\t\t\t\"eu_instance\": \"Falkenstein, Germania\",\n\t\t\t\"us_instance\": \"Washington, USA\",\n\t\t\t\"custom_instance\": \"Personalizzato\"\n\t\t},\n\t\t\"privacy\": {\n\t\t\t\"title\": \"Privacy e dati\",\n\t\t\t\"plausible_title\": \"Statistiche Plausible\",\n\t\t\t\"plausible_description\": \"Utilizziamo [plausible_link]Plausible[/plausible_link], uno strumento di analisi focalizzato sulla privacy, per raccogliere statistiche completamente anonime. Tutti i dati sono anonimizzati e aggregati e nessuna informazione identificabile viene mai inviata o archiviata. Puoi visualizzare le statistiche [analytics_link]qui[/analytics_link] e scegliere di disattivare il tracciamento qui sotto.\",\n\t\t\t\"opt_in\": \"Attiva tracciamento\",\n\t\t\t\"opt_out\": \"Disattiva tracciamento\",\n\t\t\t\"cache_title\": \"Gestione della cache\",\n\t\t\t\"cache_description\": \"Memorizziamo i file del convertitore nella cache del tuo *browser* in modo che tu non debba riscaricarli ogni volta, migliorando le prestazioni e riducendo l'utilizzo dei dati.\",\n\t\t\t\"refresh_cache\": \"Aggiorna cache\",\n\t\t\t\"clear_cache\": \"Cancella cache\",\n\t\t\t\"files_cached\": \"{size} ({count} file)\",\n\t\t\t\"loading_cache\": \"Caricamento...\",\n\t\t\t\"total_size\": \"Dimensione Totale\",\n\t\t\t\"files_cached_label\": \"File in Cache\",\n\t\t\t\"cache_cleared\": \"Cache cancellata con successo!\",\n\t\t\t\"cache_clear_error\": \"Impossibile cancellare la cache.\"\n\t\t},\n\t\t\"language\": {\n\t\t\t\"title\": \"Lingua\",\n\t\t\t\"description\": \"Seleziona la tua lingua preferita per l'interfaccia di VERT.\"\n\t\t}\n\t},\n\t\"about\": {\n\t\t\"title\": \"Informazioni\",\n\t\t\"why\": {\n\t\t\t\"title\": \"Perché VERT?\",\n\t\t\t\"description\": \"<b>I convertitori di file ci hanno sempre deluso.</b> Sono brutti, pieni di pubblicità e, soprattutto, lenti. Abbiamo deciso di risolvere questo problema una volta per tutte creando un'alternativa che risolve tutti questi problemi e non solo.<br/><br/>Tutti i file non video vengono convertiti completamente sul dispositivo; questo significa che non ci sono ritardi tra l'invio e la ricezione dei file da un server e non possiamo mai spiare i file che converti.<br/><br/>I file video vengono caricati sul nostro velocissimo server RTX 4000 Ada. I tuoi video rimangono lì per un'ora se non li converti. Se converti il file, il video rimarrà sul server per un'ora o fino a quando non viene scaricato. Il file verrà quindi eliminato dal nostro server.\"\n\t\t},\n\t\t\"sponsors\": {\n\t\t\t\"title\": \"Sponsor\",\n\t\t\t\"description\": \"Vuoi sostenerci? Contatta uno sviluppatore nel server [discord_link]Discord[/discord_link] o invia un'e-mail a\",\n\t\t\t\"email_copied\": \"E-mail copiata negli appunti!\"\n\t\t},\n\t\t\"resources\": {\n\t\t\t\"title\": \"Risorse\",\n\t\t\t\"discord\": \"Discord\",\n\t\t\t\"source\": \"Sorgente\",\n\t\t\t\"email\": \"E-mail\"\n\t\t},\n\t\t\"donate\": {\n\t\t\t\"title\": \"Fai una donazione a VERT\",\n\t\t\t\"description\": \"Con il tuo supporto, possiamo continuare a mantenere e migliorare VERT.\",\n\t\t\t\"one_time\": \"Una tantum\",\n\t\t\t\"monthly\": \"Mensile\",\n\t\t\t\"custom\": \"Personalizzato\",\n\t\t\t\"pay_now\": \"Paga ora\",\n\t\t\t\"donate_amount\": \"Dona ${amount} USD\",\n\t\t\t\"thank_you\": \"Grazie per la tua donazione!\",\n\t\t\t\"payment_failed\": \"Pagamento fallito: {message}{period} Non ti è stato addebitato nulla.\",\n\t\t\t\"donation_error\": \"Si è verificato un errore durante l'elaborazione della tua donazione. Riprova più tardi.\",\n\t\t\t\"payment_error\": \"Errore nel recupero dei dettagli di pagamento. Riprova più tardi.\"\n\t\t},\n\t\t\"credits\": {\n\t\t\t\"title\": \"Crediti\",\n\t\t\t\"contact_team\": \"Se desideri contattare il team di sviluppo, utilizza l'e-mail che trovi sulla scheda \\\"Risorse\\\".\",\n\t\t\t\"notable_contributors\": \"Contributori di rilievo\",\n\t\t\t\"notable_description\": \"Vorremmo ringraziare queste persone per i loro importanti contributi a VERT.\",\n\t\t\t\"github_contributors\": \"Contributori GitHub\",\n\t\t\t\"github_description\": \"Un grande grazie a tutte queste persone per aver dato una mano! [github_link]Vuoi aiutare anche tu?[/github_link]\",\n\t\t\t\"no_contributors\": \"Sembra che nessuno abbia ancora contribuito... [contribute_link]sii il primo a contribuire![/contribute_link]\",\n\t\t\t\"libraries\": \"Librerie\",\n\t\t\t\"libraries_description\": \"Un grande ringraziamento a FFmpeg (audio, video), ImageMagick (immagini) e Pandoc (documenti) per aver mantenuto librerie così eccellenti per così tanti anni. VERT si affida a loro per fornirti le tue conversioni.\",\n\t\t\t\"roles\": {\n\t\t\t\t\"lead_developer\": \"Sviluppatore principale; backend di conversione, implementazione UI\",\n\t\t\t\t\"developer\": \"Sviluppatore; implementazione UI\",\n\t\t\t\t\"designer\": \"Designer; UX, branding, marketing\",\n\t\t\t\t\"docker_ci\": \"Manutenzione del supporto Docker e CI\",\n\t\t\t\t\"former_cofounder\": \"Ex co-fondatore e designer\"\n\t\t\t}\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"github_contributors\": \"Errore nel recupero dei contributori GitHub\"\n\t\t}\n\t},\n\t\"workers\": {\n\t\t\"errors\": {\n\t\t\t\"general\": \"Errore durante la conversione di {file}: {message}\",\n\t\t\t\"cancel\": \"Errore durante l'annullamento della conversione per {file}: {message}\",\n\t\t\t\"magick\": \"Errore nel *worker* Magick, la conversione delle immagini potrebbe non funzionare come previsto.\",\n\t\t\t\"ffmpeg\": \"Errore durante il caricamento di ffmpeg, alcune funzionalità potrebbero non funzionare.\",\n\t\t\t\"no_audio\": \"Nessuno *stream* audio trovato.\",\n\t\t\t\"invalid_rate\": \"Frequenza di campionamento specificata non valida: {rate}Hz\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "messages/ja.json",
    "content": "{\n\t\"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n\t\"navbar\": {\n\t\t\"upload\": \"アップロード\",\n\t\t\"convert\": \"変換\",\n\t\t\"settings\": \"設定\",\n\t\t\"about\": \"について\",\n\t\t\"toggle_theme\": \"テーマを切り替える\"\n\t},\n\t\"footer\": {\n\t\t\"copyright\": \"© {year} VERT.\",\n\t\t\"source_code\": \"ソースコード\",\n\t\t\"discord_server\": \"Discordサーバー\"\n\t},\n\t\"upload\": {\n\t\t\"title\": \"きっと気に入るファイル変換ツール。\",\n\t\t\"subtitle\": \"すべての画像・音声・ドキュメント処理はデバイス上で行われます。動画は超高速サーバーで変換されます。ファイルサイズ制限なし、広告なし、完全オープンソース。\",\n\t\t\"uploader\": {\n\t\t\t\"text\": \"ドロップまたはクリックして{action}\",\n\t\t\t\"convert\": \"変換\"\n\t\t},\n\t\t\"cards\": {\n\t\t\t\"title\": \"VERTがサポートしている形式\",\n\t\t\t\"images\": \"画像\",\n\t\t\t\"audio\": \"音声\",\n\t\t\t\"documents\": \"ドキュメント\",\n\t\t\t\"video\": \"動画\",\n\t\t\t\"video_server_processing\": \"サーバー対応\",\n\t\t\t\"local_supported\": \"ローカル対応\",\n\t\t\t\"status\": {\n\t\t\t\t\"text\": \"<b>ステータス:</b> {status}\",\n\t\t\t\t\"ready\": \"準備完了\",\n\t\t\t\t\"not_ready\": \"未準備\",\n\t\t\t\t\"not_initialized\": \"未初期化\",\n\t\t\t\t\"downloading\": \"ダウンロード中...\",\n\t\t\t\t\"initializing\": \"初期化中...\",\n\t\t\t\t\"unknown\": \"不明なステータス\"\n\t\t\t},\n\t\t\t\"supported_formats\": \"対応フォーマット:\"\n\t\t},\n\t\t\"tooltip\": {\n\t\t\t\"partial_support\": \"このフォーマットは{direction}としてのみ変換可能です。\",\n\t\t\t\"direction_input\": \"入力（変換元）\",\n\t\t\t\"direction_output\": \"出力（変換先）\",\n\t\t\t\"video_server_processing\": \"動画はデフォルトでサーバーにアップロードされて処理されます。ローカルで設定する方法はこちら。\"\n\t\t}\n\t},\n\t\"convert\": {\n\t\t\"external_warning\": {\n\t\t\t\"title\": \"外部サーバーの警告\",\n\t\t\t\"text\": \"動画フォーマットへの変換を選択すると、ファイルは外部サーバーにアップロードされて変換されます。続行しますか？\",\n\t\t\t\"yes\": \"はい\",\n\t\t\t\"no\": \"いいえ\"\n\t\t},\n\t\t\"panel\": {\n\t\t\t\"convert_all\": \"すべて変換\",\n\t\t\t\"download_all\": \"すべてを.zipでダウンロード\",\n\t\t\t\"remove_all\": \"すべてのファイルを削除\",\n\t\t\t\"set_all_to\": \"すべてを設定\",\n\t\t\t\"na\": \"該当なし\"\n\t\t},\n\t\t\"dropdown\": {\n\t\t\t\"audio\": \"音声\",\n\t\t\t\"video\": \"動画\",\n\t\t\t\"doc\": \"ドキュメント\",\n\t\t\t\"image\": \"画像\",\n\t\t\t\"placeholder\": \"フォーマットを検索\"\n\t\t},\n\t\t\"tooltips\": {\n\t\t\t\"unknown_file\": \"不明なファイルタイプ\",\n\t\t\t\"audio_file\": \"音声ファイル\",\n\t\t\t\"video_file\": \"動画ファイル\",\n\t\t\t\"document_file\": \"ドキュメントファイル\",\n\t\t\t\"image_file\": \"画像ファイル\",\n\t\t\t\"convert_file\": \"このファイルを変換\",\n\t\t\t\"download_file\": \"このファイルをダウンロード\"\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"cant_convert\": \"このファイルを変換できません。\",\n\t\t\t\"vertd_server\": \"何してるの..? vertdサーバーを起動する必要があります！\",\n\t\t\t\"unsupported_format\": \"画像、動画、音声、ドキュメントのみ対応しています\",\n\t\t\t\"vertd_not_found\": \"動画変換を開始するためのvertdインスタンスが見つかりません。URLが正しいか確認してください。\",\n\t\t\t\"worker_downloading\": \"{type}コンバーターを初期化中です。少々お待ちください。\",\n\t\t\t\"worker_error\": \"{type}コンバーターの初期化中にエラーが発生しました。後でもう一度お試しください。\",\n\t\t\t\"worker_timeout\": \"{type}コンバーターの初期化に予想以上の時間がかかっています。もう少しお待ちいただくか、ページを更新してください。\",\n\t\t\t\"audio\": \"音声\",\n\t\t\t\"doc\": \"ドキュメント\",\n\t\t\t\"image\": \"画像\"\n\t\t}\n\t},\n\t\"settings\": {\n\t\t\"title\": \"設定\",\n\t\t\"errors\": {\n\t\t\t\"save_failed\": \"設定の保存に失敗しました！\"\n\t\t},\n\t\t\"appearance\": {\n\t\t\t\"title\": \"外観\",\n\t\t\t\"brightness_theme\": \"明るさテーマ\",\n\t\t\t\"brightness_description\": \"まぶしい昼間か、静かな夜か？\",\n\t\t\t\"light\": \"ライト\",\n\t\t\t\"dark\": \"ダーク\",\n\t\t\t\"effect_settings\": \"エフェクト設定\",\n\t\t\t\"effect_description\": \"派手な効果にしますか？それとも静的な体験にしますか？\",\n\t\t\t\"enable\": \"有効\",\n\t\t\t\"disable\": \"無効\"\n\t\t},\n\t\t\"conversion\": {\n\t\t\t\"title\": \"変換\",\n\t\t\t\"advanced_settings\": \"詳細設定\",\n\t\t\t\"filename_format\": \"ファイル名フォーマット\",\n\t\t\t\"filename_description\": \"これはダウンロード時のファイル名を決定します（拡張子を除く）。以下のテンプレートを使用できます：<b>%name%</b>（元のファイル名）、<b>%extension%</b>（元の拡張子）、<b>%date%</b>（変換日時）。\",\n\t\t\t\"placeholder\": \"VERT_%name%\",\n\t\t\t\"default_format\": \"デフォルト変換フォーマット\",\n\t\t\t\"default_format_description\": \"このファイルタイプをアップロードしたときに自動で選択される形式を変更します。\",\n\t\t\t\"default_format_image\": \"画像\",\n\t\t\t\"default_format_video\": \"動画\",\n\t\t\t\"default_format_audio\": \"音声\",\n\t\t\t\"default_format_document\": \"ドキュメント\",\n\t\t\t\"metadata\": \"ファイルメタデータ\",\n\t\t\t\"metadata_description\": \"変換後のファイルに元のメタデータ（EXIF、曲情報など）を保持するかどうかを変更します。\",\n\t\t\t\"keep\": \"保持\",\n\t\t\t\"remove\": \"削除\",\n\t\t\t\"quality\": \"変換品質\",\n\t\t\t\"quality_description\": \"出力ファイルの品質を変更します。値が高いほど処理時間とファイルサイズが増加します。\",\n\t\t\t\"quality_video\": \"動画変換の品質を変更します。高品質ほど変換時間とサイズが増加します。\",\n\t\t\t\"quality_audio\": \"音声（kbps）\",\n\t\t\t\"quality_images\": \"画像（％）\",\n\t\t\t\"rate\": \"サンプリングレート（Hz）\"\n\t\t},\n\t\t\"vertd\": {\n\t\t\t\"title\": \"動画変換\",\n\t\t\t\"status\": \"ステータス：\",\n\t\t\t\"loading\": \"読み込み中...\",\n\t\t\t\"available\": \"利用可能（コミットID {commitId}）\",\n\t\t\t\"unavailable\": \"利用不可（URLが正しいですか？）\",\n\t\t\t\"description\": \"<code>vertd</code>プロジェクトはFFmpegのサーバーラッパーです。これにより、GPUの性能を活かして高速に変換しつつ、VERTのウェブインターフェイスから簡単に動画を変換できます。\",\n\t\t\t\"hosting_info\": \"私たちは利便性のために公開インスタンスをホストしていますが、自分のPCやサーバーでも簡単にホストできます。バイナリは[vertd_link]こちら[/vertd_link]からダウンロードできます。今後さらにセットアップが簡単になる予定です！\",\n\t\t\t\"instance\": \"インスタンス\",\n\t\t\t\"url_placeholder\": \"例: http://localhost:24153\",\n\t\t\t\"conversion_speed\": \"変換速度\",\n\t\t\t\"speed_description\": \"速度と品質のバランスを設定します。高速化すると品質が低下しますが、処理は速くなります。\",\n\t\t\t\"speeds\": {\n\t\t\t\t\"very_slow\": \"非常に遅い\",\n\t\t\t\t\"slower\": \"かなり遅い\",\n\t\t\t\t\"slow\": \"遅い\",\n\t\t\t\t\"medium\": \"普通\",\n\t\t\t\t\"fast\": \"速い\",\n\t\t\t\t\"ultra_fast\": \"超高速\"\n\t\t\t},\n\t\t\t\"auto_instance\": \"自動（推奨）\",\n\t\t\t\"eu_instance\": \"ドイツ・ファルケンシュタイン\",\n\t\t\t\"us_instance\": \"アメリカ・ワシントン\",\n\t\t\t\"custom_instance\": \"カスタム\"\n\t\t},\n\t\t\"privacy\": {\n\t\t\t\"title\": \"プライバシーとデータ\",\n\t\t\t\"plausible_title\": \"Plausible解析\",\n\t\t\t\"plausible_description\": \"私たちはプライバシー重視の解析ツール[plausible_link]Plausible[/plausible_link]を使用しています。すべてのデータは匿名化・集計され、個人情報は一切収集・保存されません。統計情報は[analytics_link]こちら[/analytics_link]で確認でき、以下でオプトアウト可能です。\",\n\t\t\t\"opt_in\": \"参加する\",\n\t\t\t\"opt_out\": \"参加しない\",\n\t\t\t\"cache_title\": \"キャッシュ管理\",\n\t\t\t\"cache_description\": \"コンバーターファイルをブラウザにキャッシュして再ダウンロードを防ぎ、パフォーマンスを向上させます。\",\n\t\t\t\"refresh_cache\": \"キャッシュを更新\",\n\t\t\t\"clear_cache\": \"キャッシュをクリア\",\n\t\t\t\"files_cached\": \"{size}（{count}ファイル）\",\n\t\t\t\"loading_cache\": \"読み込み中...\",\n\t\t\t\"total_size\": \"合計サイズ\",\n\t\t\t\"files_cached_label\": \"キャッシュ済みファイル\",\n\t\t\t\"cache_cleared\": \"キャッシュが正常にクリアされました！\"\n\t\t},\n\t\t\"language\": {\n\t\t\t\"title\": \"言語\",\n\t\t\t\"description\": \"VERTインターフェイスの表示言語を選択してください。\"\n\t\t}\n\t},\n\t\"about\": {\n\t\t\"title\": \"について\",\n\t\t\"why\": {\n\t\t\t\"title\": \"なぜVERT？\",\n\t\t\t\"description\": \"<b>従来のファイルコンバーターにはいつもがっかりしてきました。</b>見た目が悪く、広告だらけで、そして何より遅い。私たちはそれらの問題をすべて解決するためにVERTを作りました。<br/><br/>動画以外のファイルは完全にデバイス上で変換されるため、サーバーとのやり取りによる遅延もなく、あなたのファイルを覗き見ることもありません。<br/><br/>動画は超高速RTX 4000 Adaサーバーで処理され、変換しなかった場合は1時間以内に削除されます。変換された動画も1時間またはダウンロード完了後に削除されます。\"\n\t\t},\n\t\t\"sponsors\": {\n\t\t\t\"title\": \"スポンサー\",\n\t\t\t\"description\": \"私たちを支援したい場合は、[discord_link]Discord[/discord_link]サーバーで開発者に連絡するか、以下のメールアドレスまでご連絡ください。\",\n\t\t\t\"email_copied\": \"メールアドレスをコピーしました！\"\n\t\t},\n\t\t\"resources\": {\n\t\t\t\"title\": \"リソース\",\n\t\t\t\"discord\": \"Discord\",\n\t\t\t\"source\": \"ソース\",\n\t\t\t\"email\": \"メール\"\n\t\t},\n\t\t\"donate\": {\n\t\t\t\"title\": \"VERTを支援する\",\n\t\t\t\"description\": \"あなたの支援でVERTの維持と改善を続けられます。\",\n\t\t\t\"one_time\": \"一度きり\",\n\t\t\t\"monthly\": \"毎月\",\n\t\t\t\"custom\": \"カスタム\",\n\t\t\t\"pay_now\": \"今すぐ支払う\",\n\t\t\t\"donate_amount\": \"${amount} USDを寄付\",\n\t\t\t\"thank_you\": \"ご支援ありがとうございます！\",\n\t\t\t\"payment_failed\": \"支払いに失敗しました: {message}{period} 請求は行われていません。\",\n\t\t\t\"donation_error\": \"寄付の処理中にエラーが発生しました。後でもう一度お試しください。\",\n\t\t\t\"payment_error\": \"支払い情報の取得中にエラーが発生しました。後でもう一度お試しください。\"\n\t\t},\n\t\t\"credits\": {\n\t\t\t\"title\": \"クレジット\",\n\t\t\t\"contact_team\": \"開発チームに連絡したい場合は、「リソース」カードに記載されたメールをご利用ください。\",\n\t\t\t\"notable_contributors\": \"特筆すべき貢献者\",\n\t\t\t\"notable_description\": \"VERTに大きく貢献してくださった方々に感謝します。\",\n\t\t\t\"github_contributors\": \"GitHubの貢献者\",\n\t\t\t\"github_description\": \"多くの方々に感謝します！[github_link]あなたも参加してみませんか？[/github_link]\",\n\t\t\t\"no_contributors\": \"まだ誰も貢献していないようです… [contribute_link]最初の貢献者になりましょう！[/contribute_link]\",\n\t\t\t\"libraries\": \"ライブラリ\",\n\t\t\t\"libraries_description\": \"長年にわたり優れたライブラリを提供してくれているFFmpeg（音声・動画）、ImageMagick（画像）、Pandoc（ドキュメント）に感謝します。VERTはこれらに依存して動作しています。\",\n\t\t\t\"roles\": {\n\t\t\t\t\"lead_developer\": \"リード開発者；変換バックエンド、UI実装\",\n\t\t\t\t\"developer\": \"開発者；UI実装\",\n\t\t\t\t\"designer\": \"デザイナー；UX、ブランディング、マーケティング\",\n\t\t\t\t\"docker_ci\": \"DockerとCIの保守担当\",\n\t\t\t\t\"former_cofounder\": \"元共同創設者・デザイナー\"\n\t\t\t}\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"github_contributors\": \"GitHub貢献者の取得エラー\"\n\t\t}\n\t},\n\t\"workers\": {\n\t\t\"errors\": {\n\t\t\t\"general\": \"{file}の変換エラー：{message}\",\n\t\t\t\"cancel\": \"{file}の変換キャンセルエラー：{message}\",\n\t\t\t\"magick\": \"Magickワーカーでエラーが発生しました。画像変換が正常に動作しない可能性があります。\",\n\t\t\t\"ffmpeg\": \"ffmpegの読み込みエラー。一部の機能が動作しない可能性があります。\",\n\t\t\t\"no_audio\": \"音声ストリームが見つかりません。\",\n\t\t\t\"invalid_rate\": \"無効なサンプリングレートが指定されました: {rate}Hz\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "messages/ko.json",
    "content": "{\n\t\"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n\t\"navbar\": {\n\t\t\"upload\": \"업로드\",\n\t\t\"convert\": \"변환\",\n\t\t\"settings\": \"설정\",\n\t\t\"about\": \"정보\",\n\t\t\"toggle_theme\": \"테마 전환\"\n\t},\n\t\"footer\": {\n\t\t\"copyright\": \"© {year} VERT.\",\n\t\t\"source_code\": \"소스 코드\",\n\t\t\"discord_server\": \"Discord 서버\",\n\t\t\"privacy_policy\": \"개인정보 처리방침\"\n\t},\n\t\"upload\": {\n\t\t\"title\": \"이 파일 변환기,\\n 마음에 드실 거예요.\",\n\t\t\"subtitle\": \"모든 이미지, 오디오, 문서 처리는 사용자의 기기에서 이루어집니다. 동영상은 매우 빠른 VERT 전용 서버에서 변환됩니다. 광고나 파일 크기 제한이 전혀 없는 완전한 오픈 소스입니다.\",\n\t\t\"uploader\": {\n\t\t\t\"text\": \"드래그하거나 클릭해서 {action}\",\n\t\t\t\"convert\": \"변환하기\"\n\t\t},\n\t\t\"cards\": {\n\t\t\t\"title\": \"VERT가 지원하는 포맷들\",\n\t\t\t\"images\": \"이미지\",\n\t\t\t\"audio\": \"오디오\",\n\t\t\t\"documents\": \"문서\",\n\t\t\t\"video\": \"동영상\",\n\t\t\t\"video_server_processing\": \"서버 지원\",\n\t\t\t\"local_supported\": \"로컬 지원\",\n\t\t\t\"status\": {\n\t\t\t\t\"text\": \"<b>상태:</b> {status}\",\n\t\t\t\t\"ready\": \"준비됨\",\n\t\t\t\t\"not_ready\": \"준비되지 않음\",\n\t\t\t\t\"not_initialized\": \"준비 안됨\",\n\t\t\t\t\"downloading\": \"다운로드중...\",\n\t\t\t\t\"initializing\": \"준비중...\",\n\t\t\t\t\"unknown\": \"알 수 없음\"\n\t\t\t},\n\t\t\t\"supported_formats\": \"지원 포맷:\"\n\t\t},\n\t\t\"tooltip\": {\n\t\t\t\"partial_support\": \"이 형식은 {direction}으로만 변환할 수 있습니다.\",\n\t\t\t\"direction_input\": \"입력 (from)\",\n\t\t\t\"direction_output\": \"출력 (to)\",\n\t\t\t\"video_server_processing\": \"동영상은 기본적으로 처리를 위해 서버로 업로드됩니다. 로컬로 처리하도록 설정하는 방법은 여기에서 확인하세요.\"\n\t\t}\n\t},\n\t\"convert\": {\n\t\t\"archive_file\": {\n\t\t\t\"extracting\": \"ZIP파일 감지됨: {filename}\",\n\t\t\t\"extracted\": \"{filename}압축 파일에서 {extract_count}개의 파일을 풀었습니다. {ignore_count}개 항목은 무시되었습니다.\",\n\t\t\t\"extract_error\": \"{filename}압축 파일 풀던 중 오류 발생: {error}\"\n\t\t},\n\t\t\"external_warning\": {\n\t\t\t\"title\": \"외부 서버 경고\",\n\t\t\t\"text\": \"동영상 형식으로 변환을 선택하면 해당 파일은 변환을 위해 지정한 외부 서버로 업로드됩니다. 계속하시겠습니까?\",\n\t\t\t\"yes\": \"계속\",\n\t\t\t\"no\": \"아니오\"\n\t\t},\n\t\t\"panel\": {\n\t\t\t\"convert_all\": \"모두 변환\",\n\t\t\t\"download_all\": \".zip으로 다운로드\",\n\t\t\t\"remove_all\": \"모든 파일 삭제\",\n\t\t\t\"set_all_to\": \"모두 다음으로 설정\",\n\t\t\t\"na\": \"N/A\"\n\t\t},\n\t\t\"dropdown\": {\n\t\t\t\"audio\": \"오디오\",\n\t\t\t\"video\": \"비디오\",\n\t\t\t\"doc\": \"문서\",\n\t\t\t\"image\": \"이미지\",\n\t\t\t\"placeholder\": \"포맷 검색\"\n\t\t},\n\t\t\"tooltips\": {\n\t\t\t\"unknown_file\": \"알 수 없는 파일 포맷\",\n\t\t\t\"audio_file\": \"오디오 파일\",\n\t\t\t\"video_file\": \"비디오 파일\",\n\t\t\t\"document_file\": \"문서 파일\",\n\t\t\t\"image_file\": \"이미지 파일\",\n\t\t\t\"convert_file\": \"파일 변환하기\",\n\t\t\t\"download_file\": \"파일 다운로드\"\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"cant_convert\": \"이 파일을 변환할 수 없습니다.\",\n\t\t\t\"vertd_server\": \"뭐 하는거임? vertd 서버부터 실행하셈\",\n\t\t\t\"vertd_generic_view\": \"오류 세부정보 보기\",\n\t\t\t\"vertd_generic_body\": \"비디오 변환 중 오류가 발생했습니다. 이 비디오를 개발자에게 전송해서 이 버그를 수정하는 데 도움을 주시겠습니까? 오직 비디오 파일만 전송됩니다. 익명으로 처리되며, 다른 개인 정보는 포함되지 않습니다.\",\n\t\t\t\"vertd_generic_title\": \"비디오 변환 오류\",\n\t\t\t\"vertd_generic_yes\": \"비디오 전송\",\n\t\t\t\"vertd_generic_no\": \"전송 안 함\",\n\t\t\t\"vertd_failed_to_keep\": \"영상을 서버에 저장하는데 실패했습니다: {error}\",\n\t\t\t\"vertd_details\": \"오류 세부정보 보기\",\n\t\t\t\"vertd_details_body\": \"제출을 누르면, 검토를 위해 항상 보고되는 오류 로그와 함께 <b>동영상도 첨부</b>됩니다. 아래 정보는 우리가 자동으로 받는 로그입니다:\",\n\t\t\t\"vertd_details_footer\": \"이 정보는 문제 해결 목적으로만 사용되며 절대 공유되지 않습니다. 자세한 내용은 [privacy_link]개인정보 처리방침[/privacy_link]을 확인하세요.\",\n\t\t\t\"vertd_details_job_id\": \"<b>작업 ID:</b> {jobId}\",\n\t\t\t\"vertd_details_from\": \"<b>원본 포맷:</b> {from}\",\n\t\t\t\"vertd_details_to\": \"<b>변환 포맷:</b> {to}\",\n\t\t\t\"vertd_details_error_message\": \"<b>오류 메시지:</b> [view_link]오류 로그 보기[/view_link]\",\n\t\t\t\"vertd_details_close\": \"닫기\",\n\t\t\t\"unsupported_format\": \"이미지, 비디오, 오디오 및 문서 파일만 지원됩니다.\",\n\t\t\t\"format_output_only\": \"이 포맷은 현재 입력으로 사용할 수 없으며 (변환된)출력으로만 사용할 수 있습니다.\",\n\t\t\t\"vertd_not_found\": \"비디오 변환을 시작할 vertd 인스턴스를 찾을 수 없습니다. 인스턴스 URL이 올바르게 설정되었는지 확인해주세요.\",\n\t\t\t\"worker_downloading\": \"현재 {type} 변환기를 준비하고 있습니다. 잠시 기다려 주십시오.\",\n\t\t\t\"worker_error\": \"현재 {type} 변환기 준비 중 오류가 발생했습니다. 나중에 다시 시도해 주십시오.\",\n\t\t\t\"worker_timeout\": \"{type} 변환기를 준비하는데 예상보다 오래 걸리고 있습니다. 잠시 더 기다리거나 페이지를 새로고침해 주세요.\",\n\t\t\t\"audio\": \"오디오\",\n\t\t\t\"doc\": \"문서\",\n\t\t\t\"image\": \"이미지\"\n\t\t}\n\t},\n\t\"settings\": {\n\t\t\"title\": \"설정\",\n\t\t\"errors\": {\n\t\t\t\"save_failed\": \"현재 설정을 저장하는데 실패했습니다\"\n\t\t},\n\t\t\"appearance\": {\n\t\t\t\"title\": \"테마\",\n\t\t\t\"brightness_theme\": \"테마 변경\",\n\t\t\t\"brightness_description\": \"걍 알아서\",\n\t\t\t\"light\": \"라이트 모드\",\n\t\t\t\"dark\": \"다크 모드\",\n\t\t\t\"effect_settings\": \"이펙트(효과) 설정\",\n\t\t\t\"effect_description\": \"동적인 애니메이션이나 이펙트, 아님 정적인거?\",\n\t\t\t\"enable\": \"켜기\",\n\t\t\t\"disable\": \"끄기\"\n\t\t},\n\t\t\"conversion\": {\n\t\t\t\"title\": \"변환\",\n\t\t\t\"advanced_settings\": \"고급 설정\",\n\t\t\t\"filename_format\": \"파일 이름 형식\",\n\t\t\t\"filename_description\": \"다운로드할 파일의 이름을 설정합니다. <b>파일 확장자(포맷)는 포함되지 않습니다.</b> 다음 템플릿을 형식에 넣을 수 있으며, 관련 정보로 대체됩니다: <b>%name%</b> 원본 파일 이름, <b>%extension%</b> 원본 파일 확장자, <b>%date%</b> 파일이 변환된 날짜 문자열.\",\n\t\t\t\"placeholder\": \"VERT_%name%\",\n\t\t\t\"default_format\": \"기본 변환 형식\",\n\t\t\t\"default_format_description\": \"파일 유형의 파일을 업로드할 때 선택되는 기본 형식을 변경합니다.\",\n\t\t\t\"default_format_image\": \"이미지\",\n\t\t\t\"default_format_video\": \"비디오\",\n\t\t\t\"default_format_audio\": \"오디오\",\n\t\t\t\"default_format_document\": \"문서\",\n\t\t\t\"metadata\": \"파일 메타데이터\",\n\t\t\t\"metadata_description\": \"원본 파일의 메타데이터(EXIF, 노래 정보 등)가 변환된 파일에 유지되는지 선택할 수 있습니다.\",\n\t\t\t\"keep\": \"유지\",\n\t\t\t\"remove\": \"제거\",\n\t\t\t\"quality\": \"변환 품질\",\n\t\t\t\"quality_description\": \"변환된 파일의 기본 출력 품질을 변경합니다(카테고리 내에서). 더 높은 값은 더 긴 변환 시간과 파일 크기를 초래할 수 있습니다.\",\n\t\t\t\"quality_video\": \"변환된 비디오 파일의 기본 출력 품질을 변경합니다. 높은 값은 더 긴시간과 파일 크기를 초래할 수 있습니다.\",\n\t\t\t\"quality_audio\": \"오디오 (kbps)\",\n\t\t\t\"quality_images\": \"이미지 (%)\",\n\t\t\t\"rate\": \"샘플링 주파수 (Hz)\"\n\t\t},\n\t\t\"vertd\": {\n\t\t\t\"title\": \"비디오 변환 서버\",\n\t\t\t\"status\": \"상태:\",\n\t\t\t\"loading\": \"로딩중...\",\n\t\t\t\"available\": \"사용 가능, 커밋 ID {commitId}\",\n\t\t\t\"unavailable\": \"사용 불가 (URL를 다시 확인해주세요.)\",\n\t\t\t\"description\": \"<code>vertd</code> 프로젝트는 FFmpeg를 위한 서버 래퍼입니다. 이를 통해 VERT의 웹 인터페이스를 통해 비디오를 변환할 수 있으며, GPU를 활용하여 가능한 한 빠르게 작업을 수행할 수 있습니다.\",\n\t\t\t\"hosting_info\": \"편의를 위해 공개 인스턴스를 호스팅하지만, PC나 서버에서 직접 호스팅하는 것도 매우 쉽습니다. 서버 바이너리를 [vertd_link]여기[/vertd_link]에서 다운로드할 수 있습니다. 이 설정 프로세스는 앞으로 더 쉬워질 것이므로 기대해 주세요!\",\n\t\t\t\"instance\": \"인스턴스\",\n\t\t\t\"url_placeholder\": \"예시: http://localhost:24153\",\n\t\t\t\"conversion_speed\": \"변환 속도\",\n\t\t\t\"speed_description\": \"이는 속도와 품질 사이의 균형을 설명합니다. 속도를 높일수록 품질은 낮아지지만 작업 속도는 더 빨라집니다.\",\n\t\t\t\"speeds\": {\n\t\t\t\t\"very_slow\": \"매우 느림\",\n\t\t\t\t\"slower\": \"느림\",\n\t\t\t\t\"slow\": \"조금 느림\",\n\t\t\t\t\"medium\": \"보통\",\n\t\t\t\t\"fast\": \"빠름\",\n\t\t\t\t\"ultra_fast\": \"매우 빠름\"\n\t\t\t},\n\t\t\t\"auto_instance\": \"자동 (권장됨)\",\n\t\t\t\"eu_instance\": \"Falkenstein, Germany\",\n\t\t\t\"us_instance\": \"Washington, USA\",\n\t\t\t\"custom_instance\": \"사용자 지정\"\n\t\t},\n\t\t\"privacy\": {\n\t\t\t\"title\": \"개인정보 및 데이터\",\n\t\t\t\"plausible_title\": \"Plausible analytics\",\n\t\t\t\"plausible_description\": \"우리는 개인정보 보호에 초점을 둔 분석 도구인 [plausible_link]Plausible[/plausible_link]를 사용해 완전히 익명화된 통계를 수집합니다. 모든 데이터는 익명화되어 집계되며, 식별 가능한 정보는 전송되거나 보관되지 않습니다. 분석 결과는 [analytics_link]여기[/analytics_link]에서 확인할 수 있고, 아래에서 수집을 거부(opt-out)할 수 있습니다\",\n\t\t\t\"opt_in\": \"수락\",\n\t\t\t\"opt_out\": \"거부\",\n\t\t\t\"cache_title\": \"캐시 정리\",\n\t\t\t\"cache_description\": \"브라우저에 변환기 파일을 캐시하여 매번 다시 다운로드할 필요가 없도록 하여 최적화와 데이터 사용량을 줄입니다.\",\n\t\t\t\"refresh_cache\": \"캐시 새로고침\",\n\t\t\t\"clear_cache\": \"캐시 지우기\",\n\t\t\t\"files_cached\": \"{size} ({count} files)\",\n\t\t\t\"loading_cache\": \"로딩중...\",\n\t\t\t\"total_size\": \"총 크기\",\n\t\t\t\"files_cached_label\": \"캐시된 파일\",\n\t\t\t\"cache_cleared\": \"캐시를 성공적으로 지웠습니다!\",\n\t\t\t\"cache_clear_error\": \"캐시를 지우는 중 오류가 발생했습니다\"\n\t\t},\n\t\t\"language\": {\n\t\t\t\"title\": \"언어\",\n\t\t\t\"description\": \"선호하시는 언어를 선택하세요.\"\n\t\t}\n\t},\n\t\"about\": {\n\t\t\"title\": \"정보\",\n\t\t\"why\": {\n\t\t\t\"title\": \"왜 VERT인가?\",\n\t\t\t\"description\": \"<b>파일 변환기들은 항상 저희 기대치에 충족하지 못했습니다.</b> 못생긴 UI에, 광고로 떡칠하고, 그리고 가장 중요한 것은 느리다는겁니다. 그래서 저희가 이 모든 문제를 한 번에 해결할 대안을 직접 만들기로 했습니다. 기존 변환기들의 단점을 해결한 것은 물론이고, 그 이상의 기능도 제공하죠<br/><br/>동영상을 제외한 모든 파일은 사용자의 기기에서 바로 변환됩니다. 즉, 서버로 파일을 보냈다가 다시 받는 시간이 전혀 필요 없고, 저희가 여러분의 파일을 엿볼 일도 전혀 없다는 뜻입니다.<br/><br/>예외적으로 동영상 파일은 초고속 RTX 4000 Ada 서버로 업로드됩니다. 변환하지 않으면 영상은 서버에 1시간 동안 유지됩니다. 변환한 경우에도 영상은 서버에 최대 1시간 또는 다운로드될 때까지 보관되며, 그 후 서버에서 삭제됩니다.\"\n\t\t},\n\t\t\"sponsors\": {\n\t\t\t\"title\": \"후원자\",\n\t\t\t\"description\": \"지원하고 싶으신가요? [discord_link]Discord[/discord_link] 서버의 개발자에게 문의하시거나, 다음 이메일로 보내주세요:\",\n\t\t\t\"email_copied\": \"클립보드에 이메일 주소가 복사되었습니다!\"\n\t\t},\n\t\t\"resources\": {\n\t\t\t\"title\": \"Resources\",\n\t\t\t\"discord\": \"Discord\",\n\t\t\t\"source\": \"소스 코드\",\n\t\t\t\"email\": \"이메일\"\n\t\t},\n\t\t\"donate\": {\n\t\t\t\"title\": \"VERT에 기부하기\",\n\t\t\t\"description\": \"여러분의 후원으로 VERT를 지속적으로 유지하고 개발할 수 있습니다.\",\n\t\t\t\"one_time\": \"일회성\",\n\t\t\t\"monthly\": \"매월\",\n\t\t\t\"custom\": \"사용자 지정\",\n\t\t\t\"pay_now\": \"지금 결제하기\",\n\t\t\t\"donate_amount\": \"${amount} USD 후원하기\",\n\t\t\t\"thank_you\": \"후원해주셔서 감사합니다!\",\n\t\t\t\"payment_failed\": \"결제 실패: {message}{period} 요금이 청구되지 않았습니다.\",\n\t\t\t\"donation_error\": \"결제 처리 중 오류가 발생했습니다. 나중에 다시 시도해 주세요.\",\n\t\t\t\"payment_error\": \"결제 세부정보를 가져오는 중 오류가 발생했습니다. 나중에 다시 시도해 주세요.\"\n\t\t},\n\t\t\"credits\": {\n\t\t\t\"title\": \"Credits\",\n\t\t\t\"contact_team\": \"개발팀에 연락하시려면 \\\"Resources\\\" 카드에 있는 이메일로 연락해 주세요.\",\n\t\t\t\"notable_contributors\": \"주요 기여자\",\n\t\t\t\"notable_description\": \"VERT에 크게 기여해 주신 분들께 정말 감사드립니다.\",\n\t\t\t\"github_contributors\": \"GitHub 기여자\",\n\t\t\t\"github_description\": \"도와주신 모든 분들께 진심으로 감사드립니다! [github_link]기여하기[/github_link]\",\n\t\t\t\"no_contributors\": \"아직 기여한 사람이 없는 것 같습니다... [contribute_link]첫 번째 기여자가 되어보세요![/contribute_link]\",\n\t\t\t\"libraries\": \"라이브러리들\",\n\t\t\t\"libraries_description\": \"수년 동안 훌륭한 라이브러리를 유지해 주신 FFmpeg (오디오, 비디오), ImageMagick (이미지) 및 Pandoc (문서)에 진심으로 감사드립니다. VERT는 위 라이브러리들을 사용하여 변환을 제공합니다.\",\n\t\t\t\"roles\": {\n\t\t\t\t\"lead_developer\": \"총괄 개발자; 변환 백엔드, UI 구현\",\n\t\t\t\t\"developer\": \"개발자; UI 구현\",\n\t\t\t\t\"designer\": \"디자이너; UX, 브랜딩, 마케팅\",\n\t\t\t\t\"docker_ci\": \"Docker 및 CI 지원 유지\",\n\t\t\t\t\"former_cofounder\": \"전 공동 창립자 및 디자이너\"\n\t\t\t}\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"github_contributors\": \"Error, Github 기여자 불러오기 실패\"\n\t\t}\n\t},\n\t\"workers\": {\n\t\t\"errors\": {\n\t\t\t\"general\": \"{file}파일을 변환하는데 오류 발생: {message}\",\n\t\t\t\"cancel\": \"{file}파일 변환 취소 중 오류 발생: {message}\",\n\t\t\t\"magick\": \"Magick 작업에서 오류 발생, 이미지 변환이 예상대로 작동하지 않을 수 있습니다.\",\n\t\t\t\"ffmpeg\": \"FFmpeg 로드 중 오류 발생, 일부 기능이 예상대로 작동하지 않을 수 있습니다.\",\n\t\t\t\"pandoc\": \"Pandoc 작업 로드 중 오류 발생, 문서 변환이 예상대로 작동하지 않을 수 있습니다.\",\n\t\t\t\"no_audio\": \"오디오 스트림을 찾을 수 없습니다.\",\n\t\t\t\"invalid_rate\": \"지정된 샘플 레이트가 유효하지 않습니다: {rate}Hz\"\n\t\t}\n\t},\n\t\"privacy\": {\n\t\t\"title\": \"개인정보 처리방침\",\n\t\t\"summary\": {\n\t\t\t\"title\": \"요약\",\n\t\t\t\"description\": \"VERT의 개인정보 처리방침은 매우 간단합니다: 우리는 귀하에 대한 데이터를 수집하거나 보관하지 않습니다. 우리는 쿠키나 유저를 추적하지 않으며,, 모든 변환(비디오 제외)은 귀하의 브라우저에서 로컬로 수행됩니다. 비디오는 다운로드 후 또는 1시간 후에 삭제되며, 귀하가 명시적으로 보관을 허용한 경우에만 문제 해결을 위해 사용됩니다. VERT는 웹사이트 호스팅을 위한 Coolify 인스턴스와 비디오 변환을 위한 vertd, 완전히 익명화되고 집계된 분석을 위한 Plausible 인스턴스를 자체 호스팅합니다.<br/><br/>이는 [vert_link]vert.sh[/vert_link]의 공식 VERT 인스턴스에만 적용될 수 있습니다. 타사 인스턴스는 귀하의 데이터를 다르게 처리할 수 있습니다.\"\n\t\t},\n\t\t\"conversions\": {\n\t\t\t\"title\": \"변환\",\n\t\t\t\"description\": \"대부분의 변환(이미지, 문서, 오디오)은 관련 도구의 WebAssembly 버전(예: ImageMagick, Pandoc, FFmpeg)을 사용하여 여러분의 기기에서 로컬로 수행됩니다. 즉, 파일이 기기를 떠나지 않으며 우리가 파일에 접근할 일은 없습니다.<br/><br/>동영상 변환은 더 높은 연산 성능이 필요하고 아직 브라우저에서 충분히 빠르게 처리하기 어려워 서버에서 수행됩니다. VERT로 변환한 동영상은 다운로드 후 또는 1시간이 지나면 삭제되며, 문제 해결만을 위해 더 오래 보관하도록 명시적으로 허용한 경우에만 예외적으로 보관됩니다.\"\n\t\t},\n\t\t\"conversion_errors\": {\n\t\t\t\"title\": \"변환 오류\",\n\t\t\t\"description\": \"비디오 변환이 실패할 경우, 문제 진단을 위해 일부 익명 데이터를 수집할 수 있습니다. 이 데이터에는 다음이 포함될 수 있습니다:\",\n\t\t\t\"list_job_id\": \"작업 ID (익명화된 파일 이름)\",\n\t\t\t\"list_format_from\": \"변환 전 포맷\",\n\t\t\t\"list_format_to\": \"변환 후 포맷\",\n\t\t\t\"list_stderr\": \"작업의 FFmpeg stderr 출력 (오류 메시지)\",\n\t\t\t\"list_video\": \"실제 비디오 파일 (명시적 권한이 부여된 경우)\",\n\t\t\t\"footer\": \"이 정보는 오직 변환 문제를 진단하기 위해서만 사용됩니다. 실제 비디오 파일은 귀하가 수락한 경우에만 수집되며, 그 경우에도 오직 문제 해결을 위해서만 사용됩니다.\"\n\t\t},\n\t\t\"analytics\": {\n\t\t\t\"title\": \"분석\",\n\t\t\t\"description\": \"저희는 완전히 익명화되고 집계된 분석을 위해 Plausible을 자체 호스팅합니다. Plausible는 쿠키를 사용하지 않으며 모든 주요 개인정보 보호 규정(GDPR/CCPA/PECR)을 준수합니다. \\\"개인정보 및 데이터\\\" 섹션에서 [settings_link]설정[/settings_link]을 통해 분석을 선택 해제할 수 있으며, Plausible의 개인정보 보호 관행에 대한 자세한 내용은 [plausible_link]여기[/plausible_link]에서 확인할 수 있습니다.\"\n\t\t},\n\t\t\"local_storage\": {\n\t\t\t\"title\": \"Local Storage\",\n\t\t\t\"description\": \"브라우저의 로컬 스토리지를 사용해 설정을 저장하고, 반복적인 GitHub API 요청을 줄이기 위해 \\\"정보\\\" 섹션의 GitHub 기여자 목록을 브라우저의 세션 스토리지에 임시로 저장합니다. 어떤 개인 데이터도 저장되거나 전송되지 않습니다.<br/><br/>사용되는 변환 도구(FFmpeg, ImageMagick, Pandoc)의 WebAssembly 버전도 사용자가 처음 웹사이트를 방문할 때 브라우저에 로컬로 저장되므로, 매번 다시 다운로드할 필요가 없습니다. 어떤 개인 정보나 데이터도 저장되거나 전송되지 않습니다. 이 데이터는 언제든지 [settings_link]설정[/settings_link]의 \\\"개인정보 및 데이터\\\" 섹션에서 확인하거나 삭제할 수 있습니다.\"\n\t\t},\n\t\t\"contact\": {\n\t\t\t\"title\": \"문의하기\",\n\t\t\t\"description\": \"질문이 있으시면 다음 이메일로 문의해 주세요: [email_link]hello@vert.sh[/email_link]. 서드파티 VERT 인스턴스를 사용 중인 경우 해당 인스턴스의 호스트에게 문의해 주세요.\"\n\t\t},\n\t\t\"last_updated\": \"Last updated: 2025-10-19\"\n\t}\n}\n"
  },
  {
    "path": "messages/pt-BR.json",
    "content": "{\n\t\"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n\t\"navbar\": {\n\t\t\"upload\": \"Upload\",\n\t\t\"convert\": \"Converter\",\n\t\t\"settings\": \"Ajustes\",\n\t\t\"about\": \"Sobre\",\n\t\t\"toggle_theme\": \"Alternar tema\"\n\t},\n\t\"footer\": {\n\t\t\"copyright\": \"© {year} VERT.\",\n\t\t\"source_code\": \"Código fonte\",\n\t\t\"discord_server\": \"Servidor Discord\",\n\t\t\"privacy_policy\": \"Política de privacidade\"\n\t},\n\t\"upload\": {\n\t\t\"title\": \"O conversor de arquivos que você vai adorar.\",\n\t\t\"subtitle\": \"Todo processamento de imagens, audio e documentos é feito no seu dispositivo. Vídeos são convertidos em nossos servidores ultrarrápidos. Sem limite de tamanho de arquivo, sem anúncios e totalmente de código aberto.\",\n\t\t\"uploader\": {\n\t\t\t\"text\": \"Arraste ou clique para {action}\",\n\t\t\t\"convert\": \"converter\"\n\t\t},\n\t\t\"cards\": {\n\t\t\t\"title\": \"VERT suporta...\",\n\t\t\t\"images\": \"Imagens\",\n\t\t\t\"audio\": \"Áudio\",\n\t\t\t\"documents\": \"Documentos\",\n\t\t\t\"video\": \"Vídeo\",\n\t\t\t\"video_server_processing\": \"Suportado pelo servidor\",\n\t\t\t\"local_supported\": \"Suportado localmente\",\n\t\t\t\"status\": {\n\t\t\t\t\"text\": \"<b>Status:</b> {status}\",\n\t\t\t\t\"ready\": \"pronto\",\n\t\t\t\t\"not_ready\": \"não pronto\",\n\t\t\t\t\"not_initialized\": \"não inicializado\",\n\t\t\t\t\"downloading\": \"baixando...\",\n\t\t\t\t\"initializing\": \"inicializando...\",\n\t\t\t\t\"unknown\": \"status desconhecido\"\n\t\t\t},\n\t\t\t\"supported_formats\": \"Formatos suportados:\"\n\t\t},\n\t\t\"tooltip\": {\n\t\t\t\"partial_support\": \"Este formato só pode ser convertido como {direction}.\",\n\t\t\t\"direction_input\": \"entrada (de)\",\n\t\t\t\"direction_output\": \"saída (para)\",\n\t\t\t\"video_server_processing\": \"Uploads de vídeo para um servidor para processamento por padrão, saiba como configurá-lo localmente aqui.\"\n\t\t}\n\t},\n\t\"convert\": {\n\t\t\"archive_file\": {\n\t\t\t\"extract\": \"Extrair arquivo\",\n\t\t\t\"extracting\": \"Arquivo detectado: {filename}\",\n\t\t\t\"extracted\": \"Extraídos {extract_count} arquivos de {filename}. {ignore_count} itens foram ignorados.\",\n\t\t\t\"detected\": \"Arquivos {type} detectados em {filename}.\",\n\t\t\t\"audio\": \"áudio\",\n\t\t\t\"video\": \"vídeo\",\n\t\t\t\"doc\": \"documento\",\n\t\t\t\"image\": \"imagem\",\n\t\t\t\"extract_error\": \"Erro ao extrair {filename}: {error}\"\n\t\t},\n\t\t\"large_file_warning\": \"Devido a limitações do navegador/dispositivo, a conversão de vídeo para áudio está desativada para este arquivo, pois ele é maior que {limit}GB. Recomendamos usar Firefox ou Safari para arquivos deste tamanho, pois eles têm menos limitações.\",\n\t\t\"external_warning\": {\n\t\t\t\"title\": \"Aviso de servidor externo\",\n\t\t\t\"text\": \"Se você escolher converter para um formato de vídeo, esses arquivos serão enviados para um servidor externo para conversão. Deseja continuar?\",\n\t\t\t\"yes\": \"Sim\",\n\t\t\t\"no\": \"Não\"\n\t\t},\n\t\t\"panel\": {\n\t\t\t\"convert_all\": \"Converter tudo\",\n\t\t\t\"download_all\": \"Baixar tudo como .zip\",\n\t\t\t\"remove_all\": \"Remover todos os arquivos\",\n\t\t\t\"set_all_to\": \"Definir todos para\",\n\t\t\t\"na\": \"N/D\"\n\t\t},\n\t\t\"dropdown\": {\n\t\t\t\"audio\": \"Áudio\",\n\t\t\t\"video\": \"ídeo\",\n\t\t\t\"doc\": \"Documento\",\n\t\t\t\"image\": \"Imagem\",\n\t\t\t\"placeholder\": \"Pesquisar formato\",\n\t\t\t\"no_formats\": \"Nenhum formato disponível\",\n\t\t\t\"no_results\": \"Nenhum formato corresponde à sua pesquisa\"\n\t\t},\n\t\t\"tooltips\": {\n\t\t\t\"unknown_file\": \"Tipo de arquivo desconhecido\",\n\t\t\t\"audio_file\": \"Arquivo de áudio\",\n\t\t\t\"video_file\": \"Arquivo de vídeo\",\n\t\t\t\"document_file\": \"Arquivo de documento\",\n\t\t\t\"image_file\": \"Arquivo de imagem\",\n\t\t\t\"convert_file\": \"Converter este arquivo\",\n\t\t\t\"download_file\": \"Baixar este arquivo\"\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"cant_convert\": \"Não podemos converter este arquivo.\",\n\t\t\t\"vertd_server\": \"O quê você está fazendo..? você deveria executar o servidor vertd!\",\n\t\t\t\"vertd_generic_view\": \"Ver detalhes do erro\",\n\t\t\t\"vertd_generic_body\": \"Ocorreu um erro ao tentar converter seu vídeo. Gostaria de enviar este vídeo para os desenvolvedores para ajudar a corrigir este bug? Apenas seu arquivo de vídeo será enviado. Nenhum identificador será carregado.\",\n\t\t\t\"vertd_generic_title\": \"Erro de conversão de vídeo\",\n\t\t\t\"vertd_generic_yes\": \"Enviar vídeo\",\n\t\t\t\"vertd_generic_no\": \"Não enviar\",\n\t\t\t\"vertd_failed_to_keep\": \"Falha ao manter o vídeo no servidor: {error}\",\n\t\t\t\"vertd_details\": \"Ver detalhes do erro\",\n\t\t\t\"vertd_details_body\": \"Se você pressionar enviar, <b>seu vídeo também será anexado</b> junto com o registro de erros que sempre é relatado para nós para revisão. As seguintes informações são o registro que recebemos automaticamente:\",\n\t\t\t\"vertd_details_footer\": \"Estas informações serão usadas apenas para fins de solução de problemas e nunca serão compartilhadas. Veja nossa [privacy_link]política de privacidade[/privacy_link] para mais detalhes.\",\n\t\t\t\"vertd_details_job_id\": \"<b>ID do trabalho:</b> {jobId}\",\n\t\t\t\"vertd_details_from\": \"<b>Formato de origem:</b> {from}\",\n\t\t\t\"vertd_details_to\": \"<b>Formato de destino:</b> {to}\",\n\t\t\t\"vertd_details_error_message\": \"<b>Mensagem de erro:</b> [view_link]Ver registros de erro[/view_link]\",\n\t\t\t\"vertd_details_close\": \"Fechar\",\n\t\t\t\"vertd_ratelimit\": \"Seu vídeo, '{filename}', falhou na conversão algumas vezes. Para evitar sobrecarga do servidor, novas tentativas de conversão para este arquivo foram temporariamente bloqueadas. Por favor, tente novamente mais tarde.\",\n\t\t\t\"unsupported_format\": \"Apenas arquivos de imagem, vídeo, áudio e documento são suportados\",\n\t\t\t\"format_output_only\": \"Este formato atualmente só pode ser usado como saída (convertido para), não como entrada.\",\n\t\t\t\"vertd_not_found\": \"Não foi possível encontrar a instância vertd para iniciar a conversão de vídeo. Você tem certeza de que a URL da instância está configurada corretamente?\",\n\t\t\t\"worker_downloading\": \"O conversor {type} está sendo inicializado, por favor, aguarde alguns momentos.\",\n\t\t\t\"worker_error\": \"O conversor {type} teve um erro durante a inicialização, por favor, tente novamente mais tarde.\",\n\t\t\t\"worker_timeout\": \"O conversor {type} está demorando mais do que o esperado para inicializar, por favor, aguarde mais alguns momentos ou atualize a página.\",\n\t\t\t\"audio\": \"áudio\",\n\t\t\t\"doc\": \"documento\",\n\t\t\t\"image\": \"imagem\"\n\t\t}\n\t},\n\t\"settings\": {\n\t\t\"title\": \"Configurações\",\n\t\t\"errors\": {\n\t\t\t\"save_failed\": \"Falha ao salvar as configurações!\"\n\t\t},\n\t\t\"appearance\": {\n\t\t\t\"title\": \"Aparência\",\n\t\t\t\"brightness_theme\": \"Tema de exibição\",\n\t\t\t\"brightness_description\": \"Quer um visual brilhante e ensolarado, ou uma noite tranquila e solitária?\",\n\t\t\t\"light\": \"Claro\",\n\t\t\t\"dark\": \"Escuro\",\n\t\t\t\"effect_settings\": \"Configurações de efeitos\",\n\t\t\t\"effect_description\": \"Você gostaria de efeitos sofisticados ou uma experiência mais estática?\",\n\t\t\t\"enable\": \"Ativar\",\n\t\t\t\"disable\": \"Desativar\"\n\t\t},\n\t\t\"conversion\": {\n\t\t\t\"title\": \"Conversão\",\n\t\t\t\"advanced_settings\": \"Configurações avançadas\",\n\t\t\t\"filename_format\": \"Formato do nome do arquivo\",\n\t\t\t\"filename_description\": \"Isso determinará o nome do arquivo no download, <b>não incluindo a extensão do arquivo.</b> Você pode colocar os seguintes modelos no formato, que serão substituídos pelas informações relevantes: <b>%name%</b> para o nome original do arquivo, <b>%extension%</b> para a extensão original do arquivo e <b>%date%</b> para uma string de data de quando o arquivo foi convertido.\",\n\t\t\t\"placeholder\": \"VERT_%name%\",\n\t\t\t\"default_format\": \"Formato de conversão padrão\",\n\t\t\t\"default_format_enable\": \"Habilitar\",\n\t\t\t\"default_format_disable\": \"Desabilitar\",\n\t\t\t\"default_format_description\": \"Isso mudará o formato padrão selecionado quando você enviar um arquivo deste tipo.\",\n\t\t\t\"default_format_image\": \"Imagens\",\n\t\t\t\"default_format_video\": \"ídeos\",\n\t\t\t\"default_format_audio\": \"Áudio\",\n\t\t\t\"default_format_document\": \"Documentos\",\n\t\t\t\"metadata\": \"Metadados do arquivo\",\n\t\t\t\"metadata_description\": \"Isso altera se algum metadado (EXIF, informações da música, etc.) no arquivo original é preservado nos arquivos convertidos.\",\n\t\t\t\"keep\": \"Manter\",\n\t\t\t\"remove\": \"Remover\",\n\t\t\t\"quality\": \"Qualidade da conversão\",\n\t\t\t\"quality_description\": \"Isso altera a qualidade de saída padrão dos arquivos convertidos (em sua categoria). Valores mais altos podem resultar em tempos de conversão mais longos e tamanho de arquivo maior.\",\n\t\t\t\"quality_video\": \"Isso altera a qualidade de saída padrão dos arquivos de vídeo convertidos. Valores mais altos podem resultar em tempos de conversão mais longos e tamanho de arquivo maior.\",\n\t\t\t\"quality_audio\": \"Áudio (kbps)\",\n\t\t\t\"quality_images\": \"Imagem (%)\",\n\t\t\t\"rate\": \"Taxa de amostragem (Hz)\"\n\t\t},\n\t\t\"vertd\": {\n\t\t\t\"title\": \"Conversão de vídeo\",\n\t\t\t\"status\": \"status:\",\n\t\t\t\"loading\": \"carregando...\",\n\t\t\t\"available\": \"disponível, id do commit {commitId}\",\n\t\t\t\"unavailable\": \"indisponível (a url está correta?)\",\n\t\t\t\"description\": \"O projeto <code>vertd</code> é um wrapper de servidor para FFmpeg. Isso permite que você converta vídeos através da conveniência da interface web do VERT, enquanto ainda pode aproveitar o poder da sua GPU para fazer isso o mais rápido possível.\",\n\t\t\t\"hosting_info\": \"Hospedamos uma instância pública para sua conveniência, mas é bastante fácil hospedar a sua própria em seu PC ou servidor se você souber o que está fazendo. Você pode baixar os binários do servidor [vertd_link]aqui[/vertd_link] - o processo de configuração disso ficará mais fácil no futuro, então fique atento!\",\n\t\t\t\"instance\": \"Instância\",\n\t\t\t\"url_placeholder\": \"Exemplo: http://localhost:24153\",\n\t\t\t\"conversion_speed\": \"Velocidade de conversão\",\n\t\t\t\"speed_description\": \"Isso descreve a troca entre velocidade e qualidade. Velocidades mais rápidas resultarão em qualidade inferior, mas farão o trabalho mais rapidamente.\",\n\t\t\t\"speeds\": {\n\t\t\t\t\"very_slow\": \"Muito lento\",\n\t\t\t\t\"slower\": \"Mais lento\",\n\t\t\t\t\"slow\": \"Lento\",\n\t\t\t\t\"medium\": \"Médio\",\n\t\t\t\t\"fast\": \"Rápido\",\n\t\t\t\t\"ultra_fast\": \"Ultra rápido\"\n\t\t\t},\n\t\t\t\"auto_instance\": \"Automático (recomendado)\",\n\t\t\t\"eu_instance\": \"Falkenstein, Alemanha\",\n\t\t\t\"us_instance\": \"Washington, EUA\",\n\t\t\t\"custom_instance\": \"Personalizado\"\n\t\t},\n\t\t\"privacy\": {\n\t\t\t\"title\": \"Privacidade e dados\",\n\t\t\t\"plausible_title\": \"Analytics Plausible\",\n\t\t\t\"plausible_description\": \"Nós usamos [plausible_link]Plausible[/plausible_link], uma ferramenta de análise focada em privacidade, para coletar estatísticas completamente anônimas. Todos os dados são anonimizados e agregados, e nenhuma informação identificável é enviada ou armazenada. Você pode visualizar as análises [analytics_link]aqui[/analytics_link] e escolher optar por não participar abaixo.\",\n\t\t\t\"opt_in\": \"Aceitar\",\n\t\t\t\"opt_out\": \"Recusar\",\n\t\t\t\"cache_title\": \"Gerenciamento de cache\",\n\t\t\t\"cache_description\": \"Armazenamos em cache os arquivos do conversor no seu navegador para que você não precise baixá-los novamente toda vez, melhorando o desempenho e reduzindo o uso de dados.\",\n\t\t\t\"refresh_cache\": \"Atualizar cache\",\n\t\t\t\"clear_cache\": \"Limpar cache\",\n\t\t\t\"files_cached\": \"{size} ({count} arquivos)\",\n\t\t\t\"loading_cache\": \"Carregando...\",\n\t\t\t\"total_size\": \"Tamanho total\",\n\t\t\t\"files_cached_label\": \"Arquivos em cache\",\n\t\t\t\"cache_cleared\": \"Cache limpo com sucesso!\",\n\t\t\t\"cache_clear_error\": \"Falha ao limpar o cache.\",\n\t\t\t\"site_data_title\": \"Gerenciamento de dados do site\",\n\t\t\t\"site_data_description\": \"Limpe todos os dados do site, incluindo configurações e arquivos em cache, redefinindo o VERT para seu estado padrão e recarregando a página.\",\n\t\t\t\"clear_all_data\": \"Limpar todos os dados do site\",\n\t\t\t\"clear_all_data_confirm_title\": \"Limpar todos os dados do site?\",\n\t\t\t\"clear_all_data_confirm\": \"Isso irá redefinir todas as configurações e cache, e então recarregar a página. Esta ação não pode ser desfeita.\",\n\t\t\t\"clear_all_data_cancel\": \"Cancelar\",\n\t\t\t\"all_data_cleared\": \"Todos os dados do site foram limpos! Recarregando a página...\",\n\t\t\t\"all_data_clear_error\": \"Falha ao limpar todos os dados do site.\"\n\t\t},\n\t\t\"language\": {\n\t\t\t\"title\": \"Idioma\",\n\t\t\t\"description\": \"Selecione seu idioma preferido para a interface do VERT.\"\n\t\t}\n\t},\n\t\"about\": {\n\t\t\"title\": \"Sobre\",\n\t\t\"why\": {\n\t\t\t\"title\": \"Por que usar o VERT?\",\n\t\t\t\"description\": \"<b>Os conversores de arquivos sempre nos decepcionaram.</b> Eles são feios, cheios de anúncios e, o mais importante, lentos. Decidimos resolver esse problema de uma vez por todas, criando uma alternativa que resolve todos esses problemas e mais.<br/><br/>Todos os arquivos que não são de vídeo são convertidos completamente no dispositivo; isso significa que não há atraso entre o envio e o recebimento dos arquivos de um servidor, e nunca bisbilhotamos os arquivos que você converte.<br/><br/>Os arquivos de vídeo são enviados para o nosso servidor RTX 4000 Ada super rápido. Seus vídeos permanecem lá por uma hora se você não os converter. Se você converter o arquivo, o vídeo permanecerá no servidor por uma hora ou até ser baixado. O arquivo será então excluído do nosso servidor.\"\n\t\t},\n\t\t\"sponsors\": {\n\t\t\t\"title\": \"Patrocinadores\",\n\t\t\t\"description\": \"Quer nos apoiar? Entre em contato com um desenvolvedor no servidor [discord_link]Discord[/discord_link], ou envie um email para\",\n\t\t\t\"email_copied\": \"Email copiado para a área de transferência!\"\n\t\t},\n\t\t\"resources\": {\n\t\t\t\"title\": \"Recursos\",\n\t\t\t\"discord\": \"Discord\",\n\t\t\t\"source\": \"Código fonte\",\n\t\t\t\"email\": \"Email\"\n\t\t},\n\t\t\"donate\": {\n\t\t\t\"title\": \"Doar para o VERT\",\n\t\t\t\"description\": \"Com seu apoio, podemos continuar mantendo e melhorando o VERT.\",\n\t\t\t\"one_time\": \"Única vez\",\n\t\t\t\"monthly\": \"Mensal\",\n\t\t\t\"custom\": \"Personalizado\",\n\t\t\t\"pay_now\": \"Pagar agora\",\n\t\t\t\"donate_amount\": \"Doar ${amount} USD\",\n\t\t\t\"thank_you\": \"Obrigado pela sua doação!\",\n\t\t\t\"payment_failed\": \"Falha no pagamento: {message}{period} Você não foi cobrado.\",\n\t\t\t\"donation_error\": \"Ocorreu um erro ao processar sua doação. Por favor, tente novamente mais tarde.\",\n\t\t\t\"payment_error\": \"Erro ao buscar detalhes do pagamento. Por favor, tente novamente mais tarde.\",\n\t\t\t\"donation_notice_official\": \"Suas doações aqui vão para a instância oficial do VERT (vert.sh) e ajudam a apoiar o desenvolvimento do projeto.\",\n\t\t\t\"donation_notice_unofficial\": \"Suas doações aqui vão para o operador desta instância do VERT. Se você deseja apoiar os desenvolvedores oficiais do VERT, por favor visite [official_link]vert.sh[/official_link] em vez disso.\"\n\t\t},\n\t\t\"credits\": {\n\t\t\t\"title\": \"Créditos\",\n\t\t\t\"contact_team\": \"Se você gostaria de contatar a equipe de desenvolvimento, por favor use o email encontrado no cartão \\\"Recursos\\\".\",\n\t\t\t\"notable_contributors\": \"Contribuidores notáveis\",\n\t\t\t\"notable_description\": \"Gostaríamos de agradecer a essas pessoas por suas grandes contribuições ao VERT.\",\n\t\t\t\"github_contributors\": \"Contribuidores do GitHub\",\n\t\t\t\"github_description\": \"Um grande obrigado a todas essas pessoas por ajudarem! [github_link]Quer ajudar também?[/github_link]\",\n\t\t\t\"no_contributors\": \"Parece que ninguém contribuiu ainda... [contribute_link]seja o primeiro a contribuir![/contribute_link]\",\n\t\t\t\"libraries\": \"Bibliotecas\",\n\t\t\t\"libraries_description\": \"Um grande obrigado ao FFmpeg (áudio, vídeo), ImageMagick (imagens) e Pandoc (documentos) por manterem bibliotecas tão excelentes por tantos anos. O VERT depende deles para fornecer suas conversões.\",\n\t\t\t\"roles\": {\n\t\t\t\t\"lead_developer\": \"Desenvolvedor principal; backend de conversão, implementação da interface do usuário\",\n\t\t\t\t\"developer\": \"Desenvolvedor; implementação da interface do usuário\",\n\t\t\t\t\"designer\": \"Designer; UX, branding, marketing\",\n\t\t\t\t\"docker_ci\": \"Manutenção do Docker e suporte CI\",\n\t\t\t\t\"former_cofounder\": \"Ex-cofundador e designer\"\n\t\t\t}\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"github_contributors\": \"Erro ao buscar contribuintes do GitHub\"\n\t\t}\n\t},\n\t\"workers\": {\n\t\t\"errors\": {\n\t\t\t\"general\": \"Erro ao converter {file}: {message}\",\n\t\t\t\"cancel\": \"Erro ao cancelar a conversão de {file}: {message}\",\n\t\t\t\"magick\": \"Erro no worker Magick, a conversão de imagens pode não funcionar como esperado.\",\n\t\t\t\"ffmpeg\": \"Erro ao carregar FFmpeg, alguns recursos podem não funcionar como esperado.\",\n\t\t\t\"pandoc\": \"Erro ao carregar o worker Pandoc, a conversão de documentos pode não funcionar como esperado.\",\n\t\t\t\"no_audio\": \"Nenhum fluxo de áudio encontrado.\",\n\t\t\t\"invalid_rate\": \"Taxa de amostragem especificada inválida: {rate}Hz\",\n\t\t\t\"file_too_large\": \"Este arquivo excede o limite de {limit}GB do navegador / dispositivo. Tente Firefox ou Safari para converter este arquivo grande, que normalmente têm limites mais altos.\"\n\t\t}\n\t},\n\t\"privacy\": {\n\t\t\"title\": \"Política de Privacidade\",\n\t\t\"summary\": {\n\t\t\t\"title\": \"Resumo\",\n\t\t\t\"description\": \"A política de privacidade do VERT é muito simples: não coletamos nem armazenamos nenhum dado sobre você. Não usamos cookies ou rastreadores, a análise é completamente privada e todas as conversões (exceto vídeos) acontecem localmente no seu navegador. Os vídeos são excluídos após serem baixados ou após uma hora, a menos que você dê permissão explícita para armazená-los; eles serão usados apenas para fins de solução de problemas. O VERT hospeda uma instância Coolify para hospedar o site e o vertd (para conversão de vídeo), e uma instância Plausible para análises completamente anônimas e agregadas. Usamos Stripe para processar doações, que pode coletar alguns dados usados para prevenção de fraudes.<br/><br/>Observe que isso pode se aplicar apenas à instância oficial do VERT em [vert_link]vert.sh[/vert_link]; instâncias de terceiros podem lidar com seus dados de maneira diferente.\"\n\t\t},\n\t\t\"conversions\": {\n\t\t\t\"title\": \"Conversões\",\n\t\t\t\"description\": \"A maioria das conversões (imagens, documentos, áudio) acontece inteiramente localmente no seu dispositivo usando versões WebAssembly das ferramentas relevantes (por exemplo, ImageMagick, Pandoc, FFmpeg). Isso significa que seus arquivos nunca saem do seu dispositivo e nunca teremos acesso a eles.<br/><br/>As conversões de vídeo são realizadas em nossos servidores porque exigem mais poder de processamento e ainda não podem ser feitas muito rapidamente no navegador. Os vídeos que você converte com o VERT são excluídos após serem baixados ou após uma hora, a menos que você dê permissão explícita para armazená-los por mais tempo, apenas para fins de solução de problemas.\"\n\t\t},\n\t\t\"donations\": {\n\t\t\t\"title\": \"Doações\",\n\t\t\t\"description\": \"Usamos Stripe na página [about_link]sobre[/about_link] para coletar doações. O Stripe pode coletar certas informações sobre o pagamento e o dispositivo para prevenção de fraudes, conforme descrito na [stripe_link]documentação deles sobre detecção avançada de fraudes[/stripe_link]. As solicitações de rede externas para o Stripe são adiadas e só são feitas depois que você clica no botão para pagar.\"\n\t\t},\n\t\t\"conversion_errors\": {\n\t\t\t\"title\": \"Erros de Conversão\",\n\t\t\t\"description\": \"Quando uma conversão de vídeo falha, podemos coletar alguns dados anônimos para nos ajudar a diagnosticar o problema. Esses dados podem incluir:\",\n\t\t\t\"list_job_id\": \"O ID do trabalho, que é o nome do arquivo anonimizado\",\n\t\t\t\"list_format_from\": \"O formato do qual você converteu\",\n\t\t\t\"list_format_to\": \"O formato para o qual você converteu\",\n\t\t\t\"list_stderr\": \"A saída stderr do FFmpeg do seu trabalho (mensagem de erro)\",\n\t\t\t\"list_video\": \"O arquivo de vídeo real (se for dada permissão explícita)\",\n\t\t\t\"footer\": \"Essas informações são usadas exclusivamente para o propósito de diagnosticar problemas de conversão. O arquivo de vídeo real só será coletado se você nos der permissão para isso, onde será usado apenas para solução de problemas.\"\n\t\t},\n\t\t\"analytics\": {\n\t\t\t\"title\": \"Analytics\",\n\t\t\t\"description\": \"Hospedamos uma instância Plausible para análises completamente anônimas e agregadas. O Plausible não usa cookies e está em conformidade com todas as principais regulamentações de privacidade (GDPR/CCPA/PECR). Você pode optar por não participar das análises na seção \\\"Privacidade e dados\\\" em [settings_link]configurações[/settings_link] e ler mais sobre as práticas de privacidade do Plausible [plausible_link]aqui[/plausible_link].\"\n\t\t},\n\t\t\"local_storage\": {\n\t\t\t\"title\": \"Armazenamento local\",\n\t\t\t\"description\": \"Usamos o armazenamento local do seu navegador para salvar suas configurações, e o armazenamento de sessão do seu navegador para armazenar temporariamente a lista de colaboradores do GitHub para a seção \\\"Sobre\\\" para reduzir solicitações repetidas à API do GitHub. Nenhum dado pessoal é armazenado ou transmitido.<br/><br/>As versões WebAssembly das ferramentas de conversão que usamos (FFmpeg, ImageMagick, Pandoc) também são armazenadas localmente no seu navegador quando você visita o site pela primeira vez, para que você não precise baixá-las novamente a cada visita. Nenhum dado pessoal é armazenado ou transmitido. Você pode visualizar ou excluir esses dados a qualquer momento na seção \\\"Privacidade e dados\\\" em [settings_link]configurações[/settings_link].\"\n\t\t},\n\t\t\"contact\": {\n\t\t\t\"title\": \"Contato\",\n\t\t\t\"description\": \"Para perguntas, envie um e-mail para: [email_link]hello@vert.sh[/email_link]. Se você estiver usando uma instância de terceiros do VERT, entre em contato com o host dessa instância.\"\n\t\t},\n\t\t\"last_updated\": \"Última atualização: 2025-11-27\"\n\t}\n}\n"
  },
  {
    "path": "messages/tr.json",
    "content": "{\n\t\"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n\t\"navbar\": {\n\t\t\"upload\": \"Yükle\",\n\t\t\"convert\": \"Dönüştür\",\n\t\t\"settings\": \"Ayarlar\",\n\t\t\"about\": \"Hakkımızda\",\n\t\t\"toggle_theme\": \"Temayı değiştir\"\n\t},\n\t\"footer\": {\n\t\t\"copyright\": \"© {year} VERT.\",\n\t\t\"source_code\": \"Kaynak kodu\",\n\t\t\"discord_server\": \"Discord sunucusu\"\n\t},\n\t\"upload\": {\n\t\t\"title\": \"Sevdiğiniz dosya dönüştürücü.\",\n\t\t\"subtitle\": \"Tüm görüntü, ses ve belge işlemleri cihazınızda gerçekleştirilir. Videolar, ışık hızındaki sunucularımızda dönüştürülür. Dosya boyutu sınırı ve reklam yoktur. Tamamen açık kaynaklıdır.\",\n\t\t\"uploader\": {\n\t\t\t\"text\": \"{action} için sürükleyip bırakın veya dosya seçin\",\n\t\t\t\"convert\": \"dönüştürmek\"\n\t\t},\n\t\t\"cards\": {\n\t\t\t\"title\": \"VERT'in desteklediği formatlar...\",\n\t\t\t\"images\": \"Görsel\",\n\t\t\t\"audio\": \"Ses\",\n\t\t\t\"documents\": \"Belge\",\n\t\t\t\"video\": \"Video\",\n\t\t\t\"video_server_processing\": \"Sunucuda gerçekleşir\",\n\t\t\t\"local_supported\": \"Cihazınızda gerçekleşir\",\n\t\t\t\"status\": {\n\t\t\t\t\"text\": \"<b>Durum:</b> {status}\",\n\t\t\t\t\"ready\": \"hazır\",\n\t\t\t\t\"not_ready\": \"hazır değil\",\n\t\t\t\t\"not_initialized\": \"başlatılmamış\",\n\t\t\t\t\"downloading\": \"indiriliyor...\",\n\t\t\t\t\"initializing\": \"başlatılıyor...\",\n\t\t\t\t\"unknown\": \"bilinmeyen durum\"\n\t\t\t},\n\t\t\t\"supported_formats\": \"Desteklenen formatlar:\"\n\t\t},\n\t\t\"tooltip\": {\n\t\t\t\"partial_support\": \"Bu format yalnızca şu şekilde dönüştürülebilir: {direction}.\",\n\t\t\t\"direction_input\": \"kaynak\",\n\t\t\t\"direction_output\": \"çıktı\",\n\t\t\t\"video_server_processing\": \"Videolar varsayılan olarak işlenmek üzere sunucuya yüklenir. Yerel olarak nasıl ayarlayacağınızı buradan öğrenebilirsiniz.\"\n\t\t}\n\t},\n\t\"convert\": {\n\t\t\"external_warning\": {\n\t\t\t\"title\": \"Harici sunucu uyarısı\",\n\t\t\t\"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?\",\n\t\t\t\"yes\": \"Evet\",\n\t\t\t\"no\": \"Hayır\"\n\t\t},\n\t\t\"panel\": {\n\t\t\t\"convert_all\": \"Tümünü dönüştür\",\n\t\t\t\"download_all\": \"Tümünü .zip olarak indir\",\n\t\t\t\"remove_all\": \"Tüm dosyaları kaldır\",\n\t\t\t\"set_all_to\": \"Tümünü ayarla\",\n\t\t\t\"na\": \"N/A\"\n\t\t},\n\t\t\"dropdown\": {\n\t\t\t\"audio\": \"Ses\",\n\t\t\t\"video\": \"Video\",\n\t\t\t\"doc\": \"Belge\",\n\t\t\t\"image\": \"Görsel\",\n\t\t\t\"placeholder\": \"Format ara\"\n\t\t},\n\t\t\"tooltips\": {\n\t\t\t\"unknown_file\": \"Bilinmeyen dosya türü\",\n\t\t\t\"audio_file\": \"Ses dosyası\",\n\t\t\t\"video_file\": \"Video dosyası\",\n\t\t\t\"document_file\": \"Belge dosyası\",\n\t\t\t\"image_file\": \"Görsel dosyası\",\n\t\t\t\"convert_file\": \"Bu dosyayı dönüştür\",\n\t\t\t\"download_file\": \"Bu dosyayı indir\"\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"cant_convert\": \"Bu dosyayı dönüştüremiyoruz.\",\n\t\t\t\"vertd_server\": \"Ne yapıyorsun..? vertd sunucusunu çalıştırman gerekiyordu!\",\n\t\t\t\"unsupported_format\": \"Yalnızca görüntü, video, ses ve belge dosyaları desteklenir.\",\n\t\t\t\"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?\",\n\t\t\t\"worker_downloading\": \"{type} dönüştürme işlemi şu anda başlatılıyor, lütfen birkaç saniye bekleyin.\",\n\t\t\t\"worker_error\": \"{type} dönüştürme işlemi başlatılırken bir hata oluştu, lütfen daha sonra tekrar deneyin.\",\n\t\t\t\"worker_timeout\": \"{type} dönüştürme işlemi beklenenden daha uzun sürüyor, lütfen biraz daha bekleyin veya sayfayı yenileyin.\",\n\t\t\t\"audio\": \"ses\",\n\t\t\t\"doc\": \"belge\",\n\t\t\t\"image\": \"görsel\"\n\t\t}\n\t},\n\t\"settings\": {\n\t\t\"title\": \"Ayarlar\",\n\t\t\"errors\": {\n\t\t\t\"save_failed\": \"Ayarlar kaydedilirken hata oluştu!\"\n\t\t},\n\t\t\"appearance\": {\n\t\t\t\"title\": \"Görünüm\",\n\t\t\t\"brightness_theme\": \"Tema seçimi\",\n\t\t\t\"brightness_description\": \"Güneşli bir gün mü istersiniz, yoksa sessiz ve yalnız bir gece mi?\",\n\t\t\t\"light\": \"Açık\",\n\t\t\t\"dark\": \"Koyu\",\n\t\t\t\"effect_settings\": \"Efekt ayarları\",\n\t\t\t\"effect_description\": \"Süslü efektler mi istersiniz, yoksa daha sade bir deneyim mi?\",\n\t\t\t\"enable\": \"Etkinleştir\",\n\t\t\t\"disable\": \"Devre dışı bırak\"\n\t\t},\n\t\t\"conversion\": {\n\t\t\t\"title\": \"Dönüştürme\",\n\t\t\t\"advanced_settings\": \"Gelişmiş ayarlar\",\n\t\t\t\"filename_format\": \"Dosya adı formatı\",\n\t\t\t\"filename_description\": \"Bu ayar, <b>dosya uzantısını etkilemeden</b> indirilen dosyanın adını belirleyecektir. Aşağıdaki şablonları formata ekleyebilirsiniz, bunlar ilgili bilgilerle değiştirilecektir: orijinal dosya adı için <b>%name%</b>, orijinal dosya uzantısı için <b>%extension%</b> ve dosyanın dönüştürüldüğü tarihin tarih için <b>%date%</b>.\",\n\t\t\t\"placeholder\": \"VERT_%name%\",\n\t\t\t\"default_format\": \"Varsayılan dönüştürme formatı\",\n\t\t\t\"default_format_description\": \"Bu ayar, bu dosya türünde bir dosya yüklediğinizde seçili olan varsayılan formatı değiştirecektir.\",\n\t\t\t\"default_format_image\": \"Görsel\",\n\t\t\t\"default_format_video\": \"Video\",\n\t\t\t\"default_format_audio\": \"Ses\",\n\t\t\t\"default_format_document\": \"Belge\",\n\t\t\t\"metadata\": \"Dosya metadata\",\n\t\t\t\"metadata_description\": \"Bu ayar, orijinal dosyadaki meta verilerin (EXIF, şarkı bilgileri vb.) dönüştürülen dosyalarda korunup korunmayacağını değiştirir.\",\n\t\t\t\"keep\": \"Sakla\",\n\t\t\t\"remove\": \"Kaldır\",\n\t\t\t\"quality\": \"Dönüştürme kalitesi\",\n\t\t\t\"quality_description\": \"Bu, dönüştürülen dosyaların (kendi kategorisinde) varsayılan çıktı kalitesini değiştirir. Yüksek değerler, uzun dönüştürme sürelerine ve büyük dosya boyutuna neden olabilir.\",\n\t\t\t\"quality_video\": \"Bu, dönüştürülen videoların varsayılan çıktı kalitesini değiştirir. Yüksek değerler, uzun dönüştürme sürelerine ve büyük dosya boyutuna neden olabilir.\",\n\t\t\t\"quality_audio\": \"Ses (kbps)\",\n\t\t\t\"quality_images\": \"Görsel (%)\",\n\t\t\t\"rate\": \"Örnekleme oranı (Hz)\"\n\t\t},\n\t\t\"vertd\": {\n\t\t\t\"title\": \"Video dönüştürme\",\n\t\t\t\"status\": \"durum:\",\n\t\t\t\"loading\": \"yükleniyor...\",\n\t\t\t\"available\": \"uygun, işlem no: {commitId}\",\n\t\t\t\"unavailable\": \"uygun değil (url doğru mu?)\",\n\t\t\t\"description\": \"<code>vertd</code> projesi, FFmpeg için bir sunucu sarmalayıcısıdır (server wrapper). Bu ayar, VERT'in web arayüzünün kullanım kolaylığı ile videoları dönüştürmenize olanak sağlarken, ekran kartınızın gücünden yararlanarak işlemi mümkün olan en hızlı şekilde yapmanızı sağlar.\",\n\t\t\t\"hosting_info\": \"Kolaylık sağlaması açısından herkese açık bir dönüştürücü sunuyoruz, ancak kendi bilgisayarınızda veya sunucunuzda kendi dönüştürücünüzü kurmak da oldukça kolaydır. Sunucu binary dosyalarını [vertd_link]buradan[/vertd_link] indirebilirsiniz. Kurulum işlemini gelecekte daha kolay hale getirmeye çalışıyoruz, bu nedenle bizi takip etmeyi unutmayın!\",\n\t\t\t\"instance\": \"Sunucu\",\n\t\t\t\"url_placeholder\": \"Örneğin: http://localhost:24153\",\n\t\t\t\"conversion_speed\": \"Dönüştürme hızı\",\n\t\t\t\"speed_description\": \"Bu ayar, hız ve kalite arasındaki dengeyi belirlemenizi sağlar. Yüksek hızlar, düşük kaliteye neden olur ancak işlem daha hızlı tamamlanır.\",\n\t\t\t\"speeds\": {\n\t\t\t\t\"very_slow\": \"En Yavaş\",\n\t\t\t\t\"slower\": \"Daha Yavaş\",\n\t\t\t\t\"slow\": \"Yavaş\",\n\t\t\t\t\"medium\": \"Orta\",\n\t\t\t\t\"fast\": \"Hızlı\",\n\t\t\t\t\"ultra_fast\": \"En Hızlı\"\n\t\t\t},\n\t\t\t\"auto_instance\": \"Otomatik (önerilen)\",\n\t\t\t\"eu_instance\": \"Falkenstein, Germany\",\n\t\t\t\"us_instance\": \"Washington, USA\",\n\t\t\t\"custom_instance\": \"Özel\"\n\t\t},\n\t\t\"privacy\": {\n\t\t\t\"title\": \"Gizlilik & kişisel veriler\",\n\t\t\t\"plausible_title\": \"Plausible analytics\",\n\t\t\t\"plausible_description\": \"Tamamen anonim istatistikler toplamak için gizliliğe odaklı bir analiz aracı olan [plausible_link]Plausible[/plausible_link]’ı kullanıyoruz. Tüm veriler anonimleştirilmiş ve birleştirilmiş şekilde işlenir; hiçbir kişisel veya tanımlanabilir bilgi gönderilmez ya da saklanmaz. Analitik verilerini [analytics_link]buradan[/analytics_link] görüntüleyebilir ve aşağıdan devre dışı bırakmayı seçebilirsiniz.\",\n\t\t\t\"opt_in\": \"Etkinleştir\",\n\t\t\t\"opt_out\": \"Devre dışı bırak\",\n\t\t\t\"cache_title\": \"Önbellek yönetimi\",\n\t\t\t\"cache_description\": \"Dönüştürücü dosyalarını tarayıcınızda önbelleğe alırız, böylece her seferinde yeniden indirmenize gerek kalmaz, performans artar ve veri kullanımı azalır.\",\n\t\t\t\"refresh_cache\": \"Önbelleği Yenile\",\n\t\t\t\"clear_cache\": \"Önbelleği Temizle\",\n\t\t\t\"files_cached\": \"{size} ({count} dosya)\",\n\t\t\t\"loading_cache\": \"Yükleniyor...\",\n\t\t\t\"total_size\": \"Toplam Boyut\",\n\t\t\t\"files_cached_label\": \"Önbelleğe Alınan Dosyalar\",\n\t\t\t\"cache_cleared\": \"Önbellek başarıyla temizlendi.\"\n\t\t},\n\t\t\"language\": {\n\t\t\t\"title\": \"Dil\",\n\t\t\t\"description\": \"VERT arayüzü için tercih ettiğiniz dili seçin.\"\n\t\t}\n\t},\n\t\"about\": {\n\t\t\"title\": \"Hakkımızda\",\n\t\t\"why\": {\n\t\t\t\"title\": \"Neden VERT?\",\n\t\t\t\"description\": \"<b>Dosya dönüştürücüler bizi her zaman hayal kırıklığına uğratmıştır. </b> Çoğu dönüştürücü site, kötü ve reklamlarla dolu arayüze sahiptir ve en önemlisi yavaştır. Tüm bu sorunları ve daha fazlasını çözen bir alternatif oluşturarak bu sorunu sonsuza kadar çözmeye karar verdik. <br/><br/>Video dışındaki tüm dosyalar tamamen cihazınızda dönüştürülür; bu, sunucuya dosya yükleme ve sunucudan dosya indirme sırasında gecikme olmaması ve dönüştürdüğünüz dosyaların asla başka biri tarafından görüntülenememesi anlamına gelir. <br/><br/>Video dosyaları, ışık hızındaki RTX 4000 Ada sunucumuza yüklenir. Videolarınızı dönüştürseniz de dönüştürmeseniz de bir saat sonra sunucularımızdan silinir. Video dönüştürme işlemi gerçekleştirirseniz, bir saat içinde dönüştürülmüş dosyayı indirebilirsiniz. Dosya daha sonra sunucumuzdan silinir.\"\n\t\t},\n\t\t\"sponsors\": {\n\t\t\t\"title\": \"Sponsorlar\",\n\t\t\t\"description\": \"Bizi desteklemek ister misiniz? [discord_link]Discord[/discord_link] sunucumuzda bir geliştiriciyle iletişime geçin veya şu adrese e-posta gönderin:\",\n\t\t\t\"email_copied\": \"E-posta kopyalandı!\"\n\t\t},\n\t\t\"resources\": {\n\t\t\t\"title\": \"Bağlantılar\",\n\t\t\t\"discord\": \"Discord\",\n\t\t\t\"source\": \"GitHub\",\n\t\t\t\"email\": \"E-posta\"\n\t\t},\n\t\t\"donate\": {\n\t\t\t\"title\": \"VERT'e bağış yapın\",\n\t\t\t\"description\": \"Desteğinizle VERT'i çalıştırmaya ve geliştirmeye devam edebiliriz.\",\n\t\t\t\"one_time\": \"Tek seferlik\",\n\t\t\t\"monthly\": \"Aylık\",\n\t\t\t\"custom\": \"Özel\",\n\t\t\t\"pay_now\": \"Ödeme yap\",\n\t\t\t\"donate_amount\": \"${amount} USD Bağış Yap\",\n\t\t\t\"thank_you\": \"Bağışınız için teşekkür ederiz!\",\n\t\t\t\"payment_failed\": \"Ödeme başarısız: {message}{period} Kartınızdan para çekilmedi.\",\n\t\t\t\"donation_error\": \"Bağışınız işlenirken bir hata oluştu. Lütfen daha sonra tekrar deneyin.\",\n\t\t\t\"payment_error\": \"Ödeme bilgileri alınırken hata oluştu. Lütfen daha sonra tekrar deneyin.\"\n\t\t},\n\t\t\"credits\": {\n\t\t\t\"title\": \"Katkıda bulunanlar\",\n\t\t\t\"contact_team\": \"Geliştirme ekibiyle iletişime geçmek isterseniz, \\\"Bağlantılar\\\" kısmında bulunan e-posta adresini kullanabilirsiniz.\",\n\t\t\t\"notable_contributors\": \"Önemli katılımcılar\",\n\t\t\t\"notable_description\": \"VERT'e sağladıkları büyük katkılardan dolayı bu kişilere teşekkür ederiz.\",\n\t\t\t\"github_contributors\": \"GitHub katılımcıları\",\n\t\t\t\"github_description\": \"Yardımcı olan herkese çok teşekkürler! [github_link]Sen de yardım etmek ister misin?[/github_link]\",\n\t\t\t\"no_contributors\": \"Henüz kimse katkıda bulunmamış gibi görünüyor... [contribute_link]ilk katkıda bulunan sen ol![/contribute_link]\",\n\t\t\t\"libraries\": \"Kütüphaneler\",\n\t\t\t\"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.\",\n\t\t\t\"roles\": {\n\t\t\t\t\"lead_developer\": \"Lead developer; conversion backend, UI implementation\",\n\t\t\t\t\"developer\": \"Developer; UI implementation\",\n\t\t\t\t\"designer\": \"Designer; UX, branding, marketing\",\n\t\t\t\t\"docker_ci\": \"Maintaining Docker & CI support\",\n\t\t\t\t\"former_cofounder\": \"Former co-founder & designer\"\n\t\t\t}\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"github_contributors\": \"GitHub katılımcılarını yüklerken hata oluştu\"\n\t\t}\n\t},\n\t\"workers\": {\n\t\t\"errors\": {\n\t\t\t\"general\": \"{file} dönüştürülürken hata oluştu: {message}\",\n\t\t\t\"cancel\": \"{file} için dönüştürme işlemi iptal edilirken hata oluştu: {message}\",\n\t\t\t\"magick\": \"Magick işlemi sırasında hata oluştu, görsel dönüştürme işlemi beklendiği gibi çalışmayabilir.\",\n\t\t\t\"ffmpeg\": \"ffmpeg yüklenirken hata oluştu, bazı özellikler çalışmayabilir.\",\n\t\t\t\"no_audio\": \"Ses akışı bulunamadı.\",\n\t\t\t\"invalid_rate\": \"Geçersiz örnekleme hızı: {rate}Hz\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "messages/zh-Hans.json",
    "content": "{\n\t\"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n\t\"navbar\": {\n\t\t\"upload\": \"上传\",\n\t\t\"convert\": \"转换\",\n\t\t\"settings\": \"设置\",\n\t\t\"about\": \"关于\",\n\t\t\"toggle_theme\": \"切换主题\"\n\t},\n\t\"footer\": {\n\t\t\"copyright\": \"© {year} VERT.\",\n\t\t\"source_code\": \"源代码\",\n\t\t\"discord_server\": \"Discord 服务器\",\n\t\t\"privacy_policy\": \"隐私政策\"\n\t},\n\t\"upload\": {\n\t\t\"title\": \"你一定会喜欢的文件转换工具。\",\n\t\t\"subtitle\": \"所有图片、音频和文档处理都在你的设备上进行。视频通过超快速服务器转换。无文件大小限制，无广告，完全开源。\",\n\t\t\"uploader\": {\n\t\t\t\"text\": \"拖放或点击以{action}\",\n\t\t\t\"convert\": \"转换\"\n\t\t},\n\t\t\"cards\": {\n\t\t\t\"title\": \"VERT 支持...\",\n\t\t\t\"images\": \"图片\",\n\t\t\t\"audio\": \"音频\",\n\t\t\t\"documents\": \"文档\",\n\t\t\t\"video\": \"视频\",\n\t\t\t\"video_server_processing\": \"服务器支持\",\n\t\t\t\"local_supported\": \"本地支持\",\n\t\t\t\"status\": {\n\t\t\t\t\"text\": \"<b>状态：</b>{status}\",\n\t\t\t\t\"ready\": \"就绪\",\n\t\t\t\t\"not_ready\": \"未就绪\",\n\t\t\t\t\"not_initialized\": \"未初始化\",\n\t\t\t\t\"downloading\": \"下载中...\",\n\t\t\t\t\"initializing\": \"初始化中...\",\n\t\t\t\t\"unknown\": \"未知状态\"\n\t\t\t},\n\t\t\t\"supported_formats\": \"支持的格式：\"\n\t\t},\n\t\t\"tooltip\": {\n\t\t\t\"partial_support\": \"此格式仅可作为{direction}进行转换。\",\n\t\t\t\"direction_input\": \"输入（来源）\",\n\t\t\t\"direction_output\": \"输出（目标）\",\n\t\t\t\"video_server_processing\": \"视频默认上传到服务器进行处理，点击这里了解如何在本地设置。\"\n\t\t}\n\t},\n\t\"convert\": {\n\t\t\"archive_file\": {\n\t\t\t\"extracting\": \"检测到 ZIP 压缩包：{filename}\",\n\t\t\t\"extracted\": \"从 {filename} 中提取了 {extract_count} 个文件。{ignore_count} 个项目被忽略。\",\n\t\t\t\"extract_error\": \"提取 {filename} 时出错：{error}\"\n\t\t},\n\t\t\"large_file_warning\": \"由于浏览器/设备限制，此文件大于 {limit}GB，视频转音频功能已禁用。我们建议使用 Firefox 或 Safari 处理此大小的文件，因为它们的限制较少。\",\n\t\t\"external_warning\": {\n\t\t\t\"title\": \"外部服务器警告\",\n\t\t\t\"text\": \"如果你选择转换为视频格式，这些文件将被上传到外部服务器进行转换。是否继续？\",\n\t\t\t\"yes\": \"是\",\n\t\t\t\"no\": \"否\"\n\t\t},\n\t\t\"panel\": {\n\t\t\t\"convert_all\": \"全部转换\",\n\t\t\t\"download_all\": \"下载全部为 .zip\",\n\t\t\t\"remove_all\": \"删除所有文件\",\n\t\t\t\"set_all_to\": \"全部设置为\",\n\t\t\t\"na\": \"不适用\"\n\t\t},\n\t\t\"dropdown\": {\n\t\t\t\"audio\": \"音频\",\n\t\t\t\"video\": \"视频\",\n\t\t\t\"doc\": \"文档\",\n\t\t\t\"image\": \"图片\",\n\t\t\t\"placeholder\": \"搜索格式\"\n\t\t},\n\t\t\"tooltips\": {\n\t\t\t\"unknown_file\": \"未知文件类型\",\n\t\t\t\"audio_file\": \"音频文件\",\n\t\t\t\"video_file\": \"视频文件\",\n\t\t\t\"document_file\": \"文档文件\",\n\t\t\t\"image_file\": \"图片文件\",\n\t\t\t\"convert_file\": \"转换此文件\",\n\t\t\t\"download_file\": \"下载此文件\"\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"cant_convert\": \"无法转换此文件。\",\n\t\t\t\"vertd_server\": \"你在做什么...？你应该运行 vertd 服务器！\",\n\t\t\t\"vertd_generic_view\": \"查看错误详情\",\n\t\t\t\"vertd_generic_body\": \"尝试转换视频时发生错误。你想将此视频提交给开发者以帮助修复此错误吗？只会发送你的视频文件，不会上传任何标识符。\",\n\t\t\t\"vertd_generic_title\": \"视频转换错误\",\n\t\t\t\"vertd_generic_yes\": \"提交视频\",\n\t\t\t\"vertd_generic_no\": \"不提交\",\n\t\t\t\"vertd_failed_to_keep\": \"无法在服务器上保留视频：{error}\",\n\t\t\t\"vertd_details\": \"查看错误详情\",\n\t\t\t\"vertd_details_body\": \"如果你按下提交，<b>你的视频也会被附加</b>在错误日志旁边，日志会自动报告给我们审核。以下信息是我们自动接收的日志：\",\n\t\t\t\"vertd_details_footer\": \"此信息仅用于故障排查，绝不会被共享。查看我们的[privacy_link]隐私政策[/privacy_link]了解更多详情。\",\n\t\t\t\"vertd_details_job_id\": \"<b>任务 ID：</b>{jobId}\",\n\t\t\t\"vertd_details_from\": \"<b>来源格式：</b>{from}\",\n\t\t\t\"vertd_details_to\": \"<b>目标格式：</b>{to}\",\n\t\t\t\"vertd_details_error_message\": \"<b>错误消息：</b>[view_link]查看错误日志[/view_link]\",\n\t\t\t\"vertd_details_close\": \"关闭\",\n\t\t\t\"unsupported_format\": \"仅支持图片、视频、音频和文档文件\",\n\t\t\t\"format_output_only\": \"此格式目前只能用作输出（转换目标），不能用作输入。\",\n\t\t\t\"vertd_not_found\": \"未找到 vertd 实例来开始视频转换。请确保实例 URL 设置正确。\",\n\t\t\t\"worker_downloading\": \"{type}转换器正在初始化，请稍候。\",\n\t\t\t\"worker_error\": \"{type}转换器初始化时出错，请稍后重试。\",\n\t\t\t\"worker_timeout\": \"{type}转换器初始化时间超出预期，请再等待一会儿或刷新页面。\",\n\t\t\t\"audio\": \"音频\",\n\t\t\t\"doc\": \"文档\",\n\t\t\t\"image\": \"图片\"\n\t\t}\n\t},\n\t\"settings\": {\n\t\t\"title\": \"设置\",\n\t\t\"errors\": {\n\t\t\t\"save_failed\": \"保存设置失败！\"\n\t\t},\n\t\t\"appearance\": {\n\t\t\t\"title\": \"外观\",\n\t\t\t\"brightness_theme\": \"亮度主题\",\n\t\t\t\"brightness_description\": \"想要阳光明媚的闪光弹，还是宁静孤独的夜晚？\",\n\t\t\t\"light\": \"浅色\",\n\t\t\t\"dark\": \"深色\",\n\t\t\t\"effect_settings\": \"效果设置\",\n\t\t\t\"effect_description\": \"你想要华丽的效果，还是更静态的体验？\",\n\t\t\t\"enable\": \"启用\",\n\t\t\t\"disable\": \"禁用\"\n\t\t},\n\t\t\"conversion\": {\n\t\t\t\"title\": \"转换\",\n\t\t\t\"advanced_settings\": \"高级设置\",\n\t\t\t\"filename_format\": \"文件名格式\",\n\t\t\t\"filename_description\": \"这将决定下载时的文件名，<b>不包括文件扩展名。</b>你可以在格式中使用以下模板，它们将被替换为相关信息：<b>%name%</b>表示原始文件名，<b>%extension%</b>表示原始文件扩展名，<b>%date%</b>表示文件转换时的日期字符串。\",\n\t\t\t\"placeholder\": \"VERT_%name%\",\n\t\t\t\"default_format\": \"默认转换格式\",\n\t\t\t\"default_format_description\": \"这将更改上传此文件类型时自动选择的默认格式。\",\n\t\t\t\"default_format_image\": \"图片\",\n\t\t\t\"default_format_video\": \"视频\",\n\t\t\t\"default_format_audio\": \"音频\",\n\t\t\t\"default_format_document\": \"文档\",\n\t\t\t\"metadata\": \"文件元数据\",\n\t\t\t\"metadata_description\": \"这将更改转换后的文件是否保留原始文件的元数据（EXIF、歌曲信息等）。\",\n\t\t\t\"keep\": \"保留\",\n\t\t\t\"remove\": \"删除\",\n\t\t\t\"quality\": \"转换质量\",\n\t\t\t\"quality_description\": \"更改输出文件的质量。值越高，处理时间和文件大小越大。\",\n\t\t\t\"quality_video\": \"更改视频转换的质量。质量越高，转换时间和文件大小越大。\",\n\t\t\t\"quality_audio\": \"音频（kbps）\",\n\t\t\t\"quality_images\": \"图片（%）\",\n\t\t\t\"rate\": \"采样率（Hz）\"\n\t\t},\n\t\t\"vertd\": {\n\t\t\t\"title\": \"视频转换\",\n\t\t\t\"status\": \"状态：\",\n\t\t\t\"loading\": \"加载中...\",\n\t\t\t\"available\": \"可用（提交 ID {commitId}）\",\n\t\t\t\"unavailable\": \"不可用（URL 正确吗？）\",\n\t\t\t\"description\": \"<code>vertd</code>项目是 FFmpeg 的服务器包装器。这允许你通过 VERT 网页界面方便地转换视频，同时仍能利用 GPU 的强大性能以尽可能快的速度完成转换。\",\n\t\t\t\"hosting_info\": \"我们为你提供了一个公共实例以方便使用，但如果你知道如何操作，在自己的电脑或服务器上托管也很容易。你可以在[vertd_link]这里[/vertd_link]下载服务器二进制文件 - 设置过程将来会变得更简单，敬请期待！\",\n\t\t\t\"instance\": \"实例\",\n\t\t\t\"url_placeholder\": \"例如：http://localhost:24153\",\n\t\t\t\"conversion_speed\": \"转换速度\",\n\t\t\t\"speed_description\": \"这描述了速度和质量之间的权衡。速度越快质量越低，但完成工作的速度更快。\",\n\t\t\t\"speeds\": {\n\t\t\t\t\"very_slow\": \"非常慢\",\n\t\t\t\t\"slower\": \"较慢\",\n\t\t\t\t\"slow\": \"慢\",\n\t\t\t\t\"medium\": \"中等\",\n\t\t\t\t\"fast\": \"快\",\n\t\t\t\t\"ultra_fast\": \"超快\"\n\t\t\t},\n\t\t\t\"auto_instance\": \"自动（推荐）\",\n\t\t\t\"eu_instance\": \"德国法尔肯施泰因\",\n\t\t\t\"us_instance\": \"美国华盛顿\",\n\t\t\t\"custom_instance\": \"自定义\"\n\t\t},\n\t\t\"privacy\": {\n\t\t\t\"title\": \"隐私与数据\",\n\t\t\t\"plausible_title\": \"Plausible 分析\",\n\t\t\t\"plausible_description\": \"我们使用[plausible_link]Plausible[/plausible_link]，一个注重隐私的分析工具，来收集完全匿名的统计数据。所有数据都是匿名和聚合的，不会发送或存储任何可识别信息。你可以在[analytics_link]这里[/analytics_link]查看分析数据，并在下方选择退出。\",\n\t\t\t\"opt_in\": \"选择加入\",\n\t\t\t\"opt_out\": \"选择退出\",\n\t\t\t\"cache_title\": \"缓存管理\",\n\t\t\t\"cache_description\": \"我们在浏览器中缓存转换器文件，这样你就不必每次都重新下载，从而提高性能并减少数据使用。\",\n\t\t\t\"refresh_cache\": \"刷新缓存\",\n\t\t\t\"clear_cache\": \"清除缓存\",\n\t\t\t\"files_cached\": \"{size}（{count}个文件）\",\n\t\t\t\"loading_cache\": \"加载中...\",\n\t\t\t\"total_size\": \"总大小\",\n\t\t\t\"files_cached_label\": \"已缓存文件\",\n\t\t\t\"cache_cleared\": \"缓存已成功清除！\",\n\t\t\t\"cache_clear_error\": \"清除缓存失败。\",\n\t\t\t\"site_data_title\": \"网站数据管理\",\n\t\t\t\"site_data_description\": \"清除所有网站数据，包括设置和缓存文件，将 VERT 重置为默认状态并重新加载页面。\",\n\t\t\t\"clear_all_data\": \"清除所有网站数据\",\n\t\t\t\"clear_all_data_confirm_title\": \"清除所有网站数据？\",\n\t\t\t\"clear_all_data_confirm\": \"这将重置所有设置和缓存，然后重新加载页面。此操作无法撤消。\",\n\t\t\t\"clear_all_data_cancel\": \"取消\",\n\t\t\t\"all_data_cleared\": \"所有网站数据已清除！正在重新加载页面...\",\n\t\t\t\"all_data_clear_error\": \"清除所有网站数据失败。\"\n\t\t},\n\t\t\"language\": {\n\t\t\t\"title\": \"语言\",\n\t\t\t\"description\": \"选择 VERT 界面的首选语言。\"\n\t\t}\n\t},\n\t\"about\": {\n\t\t\"title\": \"关于\",\n\t\t\"why\": {\n\t\t\t\"title\": \"为什么选择 VERT？\",\n\t\t\t\"description\": \"<b>文件转换器一直让我们失望。</b>它们很丑陋，充满广告，最重要的是；很慢。我们决定通过制作一个解决所有这些问题的替代方案，一劳永逸地解决这个问题。<br/><br/>所有非视频文件都完全在设备上转换；这意味着不需要在服务器之间发送和接收文件的延迟，而且我们永远不会窥探你转换的文件。<br/><br/>视频文件会上传到我们超快速的 RTX 4000 Ada 服务器。如果你不转换视频，它们会在服务器上保留一小时。如果你转换文件，视频将在服务器上保留一小时，或直到下载完成。然后文件将从我们的服务器中删除。\"\n\t\t},\n\t\t\"sponsors\": {\n\t\t\t\"title\": \"赞助商\",\n\t\t\t\"description\": \"想支持我们吗？请在[discord_link]Discord[/discord_link]服务器上联系开发者，或发送电子邮件至\",\n\t\t\t\"email_copied\": \"电子邮件已复制到剪贴板！\"\n\t\t},\n\t\t\"resources\": {\n\t\t\t\"title\": \"资源\",\n\t\t\t\"discord\": \"Discord\",\n\t\t\t\"source\": \"源代码\",\n\t\t\t\"email\": \"电子邮件\"\n\t\t},\n\t\t\"donate\": {\n\t\t\t\"title\": \"捐赠给 VERT\",\n\t\t\t\"description\": \"有了你的支持，我们可以继续维护和改进 VERT。\",\n\t\t\t\"one_time\": \"一次性\",\n\t\t\t\"monthly\": \"每月\",\n\t\t\t\"custom\": \"自定义\",\n\t\t\t\"pay_now\": \"立即支付\",\n\t\t\t\"donate_amount\": \"捐赠 ${amount} 美元\",\n\t\t\t\"thank_you\": \"感谢你的捐赠！\",\n\t\t\t\"payment_failed\": \"支付失败：{message}{period}你未被收费。\",\n\t\t\t\"donation_error\": \"处理捐赠时出错。请稍后重试。\",\n\t\t\t\"payment_error\": \"获取支付详情时出错。请稍后重试。\"\n\t\t},\n\t\t\"credits\": {\n\t\t\t\"title\": \"致谢\",\n\t\t\t\"contact_team\": \"如果你想联系开发团队，请使用“资源”卡片上的电子邮件。\",\n\t\t\t\"notable_contributors\": \"杰出贡献者\",\n\t\t\t\"notable_description\": \"我们要感谢这些人对 VERT 的重大贡献。\",\n\t\t\t\"github_contributors\": \"GitHub 贡献者\",\n\t\t\t\"github_description\": \"非常感谢所有这些人的帮助！[github_link]也想帮忙吗？[/github_link]\",\n\t\t\t\"no_contributors\": \"似乎还没有人贡献……[contribute_link]成为第一个贡献者！[/contribute_link]\",\n\t\t\t\"libraries\": \"库\",\n\t\t\t\"libraries_description\": \"非常感谢 FFmpeg（音频、视频）、ImageMagick（图片）和 Pandoc（文档）多年来维护如此出色的库。VERT 依赖它们为你提供转换服务。\",\n\t\t\t\"roles\": {\n\t\t\t\t\"lead_developer\": \"首席开发者；转换后端、UI 实现\",\n\t\t\t\t\"developer\": \"开发者；UI 实现\",\n\t\t\t\t\"designer\": \"设计师；用户体验、品牌、营销\",\n\t\t\t\t\"docker_ci\": \"维护 Docker 和 CI 支持\",\n\t\t\t\t\"former_cofounder\": \"前联合创始人和设计师\"\n\t\t\t}\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"github_contributors\": \"获取 GitHub 贡献者时出错\"\n\t\t}\n\t},\n\t\"workers\": {\n\t\t\"errors\": {\n\t\t\t\"general\": \"转换 {file} 时出错：{message}\",\n\t\t\t\"cancel\": \"取消转换 {file} 时出错：{message}\",\n\t\t\t\"magick\": \"Magick worker 出错，图片转换可能无法正常工作。\",\n\t\t\t\"ffmpeg\": \"加载 ffmpeg 时出错，某些功能可能无法工作。\",\n\t\t\t\"pandoc\": \"加载 Pandoc worker 时出错，文档转换可能无法正常工作。\",\n\t\t\t\"no_audio\": \"未找到音频流。\",\n\t\t\t\"invalid_rate\": \"指定的采样率无效：{rate}Hz\",\n\t\t\t\"file_too_large\": \"此文件超过 {limit}GB 浏览器/设备限制。请尝试使用 Firefox 或 Safari 转换此大文件，它们通常具有更高的限制。\"\n\t\t}\n\t},\n\t\"privacy\": {\n\t\t\"title\": \"隐私政策\",\n\t\t\"summary\": {\n\t\t\t\"title\": \"摘要\",\n\t\t\t\"description\": \"VERT 的隐私政策非常简单：我们根本不收集或存储你的任何数据。我们不使用 cookie 或跟踪器，分析是完全私密的，所有转换（视频除外）都在你的浏览器本地进行。视频在下载后或一小时后删除，除非你明确授权存储；它只会用于故障排查。VERT 自托管 Coolify 实例用于托管网站和 vertd（用于视频转换），以及用于完全匿名和聚合分析的 Plausible 实例。<br/><br/>请注意，这可能仅适用于[vert_link]vert.sh[/vert_link]的官方 VERT 实例；第三方实例可能以不同方式处理你的数据。\"\n\t\t},\n\t\t\"conversions\": {\n\t\t\t\"title\": \"转换\",\n\t\t\t\"description\": \"大多数转换（图片、文档、音频）完全在你的设备上本地使用相关工具的 WebAssembly 版本（例如 ImageMagick、Pandoc、FFmpeg）进行。这意味着你的文件永远不会离开你的设备，我们也永远无法访问它们。<br/><br/>视频转换在我们的服务器上进行，因为它们需要更多的处理能力，并且目前无法在浏览器上非常快速地完成。你使用 VERT 转换的视频在下载后或一小时后删除，除非你明确授权我们将它们存储更长时间，纯粹用于故障排查。\"\n\t\t},\n\t\t\"conversion_errors\": {\n\t\t\t\"title\": \"转换错误\",\n\t\t\t\"description\": \"当视频转换失败时，我们可能会收集一些匿名数据以帮助我们诊断问题。这些数据可能包括：\",\n\t\t\t\"list_job_id\": \"任务 ID，即匿名化的文件名\",\n\t\t\t\"list_format_from\": \"你转换的来源格式\",\n\t\t\t\"list_format_to\": \"你转换的目标格式\",\n\t\t\t\"list_stderr\": \"你任务的 FFmpeg stderr 输出（错误消息）\",\n\t\t\t\"list_video\": \"实际视频文件（如果明确授权）\",\n\t\t\t\"footer\": \"此信息仅用于诊断转换问题。只有在你明确授权的情况下，才会收集实际视频文件，并且仅用于故障排查。\"\n\t\t},\n\t\t\"analytics\": {\n\t\t\t\"title\": \"分析\",\n\t\t\t\"description\": \"我们自托管 Plausible 实例用于完全匿名和聚合的分析。Plausible 不使用 cookie，并符合所有主要隐私法规（GDPR/CCPA/PECR）。你可以在[settings_link]设置[/settings_link]的“隐私与数据”部分选择退出分析，并在[plausible_link]这里[/plausible_link]阅读更多关于 Plausible 隐私实践的信息。\"\n\t\t},\n\t\t\"local_storage\": {\n\t\t\t\"title\": \"本地存储\",\n\t\t\t\"description\": \"我们使用浏览器的本地存储来保存你的设置，使用浏览器的会话存储来临时存储“关于”部分的 GitHub 贡献者列表，以减少重复的 GitHub API 请求。不会存储或传输任何个人数据。<br/><br/>我们使用的转换工具的 WebAssembly 版本（FFmpeg、ImageMagick、Pandoc）也会在你首次访问网站时本地存储在浏览器中，这样你就不需要每次访问时都重新下载它们。不会存储或传输任何个人数据。你可以随时在[settings_link]设置[/settings_link]的“隐私与数据”部分查看或删除这些数据。\"\n\t\t},\n\t\t\"contact\": {\n\t\t\t\"title\": \"联系\",\n\t\t\t\"description\": \"如有问题，请发送电子邮件至：[email_link]hello@vert.sh[/email_link]。如果你使用的是第三方 VERT 实例，请联系该实例的托管者。\"\n\t\t},\n\t\t\"last_updated\": \"最后更新：2025-10-19\"\n\t}\n}\n"
  },
  {
    "path": "messages/zh-Hant.json",
    "content": "{\n\t\"$schema\": \"https://inlang.com/schema/inlang-message-format\",\n\t\"navbar\": {\n\t\t\"upload\": \"上傳\",\n\t\t\"convert\": \"轉換\",\n\t\t\"settings\": \"設定\",\n\t\t\"about\": \"關於\",\n\t\t\"toggle_theme\": \"切換主題\"\n\t},\n\t\"footer\": {\n\t\t\"copyright\": \"© {year} VERT.\",\n\t\t\"source_code\": \"原始碼\",\n\t\t\"discord_server\": \"Discord 伺服器\",\n\t\t\"privacy_policy\": \"隱私權政策\"\n\t},\n\t\"upload\": {\n\t\t\"title\": \"你一定會喜歡的檔案轉換工具。\",\n\t\t\"subtitle\": \"所有圖片、音訊和文件處理都在你的裝置上進行。影片透過超快速伺服器轉換。無檔案大小限制，無廣告，完全開源。\",\n\t\t\"uploader\": {\n\t\t\t\"text\": \"拖放或點擊以{action}\",\n\t\t\t\"convert\": \"轉換\"\n\t\t},\n\t\t\"cards\": {\n\t\t\t\"title\": \"VERT 支援...\",\n\t\t\t\"images\": \"圖片\",\n\t\t\t\"audio\": \"音訊\",\n\t\t\t\"documents\": \"文件\",\n\t\t\t\"video\": \"影片\",\n\t\t\t\"video_server_processing\": \"伺服器支援\",\n\t\t\t\"local_supported\": \"本機支援\",\n\t\t\t\"status\": {\n\t\t\t\t\"text\": \"<b>狀態：</b>{status}\",\n\t\t\t\t\"ready\": \"就緒\",\n\t\t\t\t\"not_ready\": \"未就緒\",\n\t\t\t\t\"not_initialized\": \"未初始化\",\n\t\t\t\t\"downloading\": \"下載中...\",\n\t\t\t\t\"initializing\": \"初始化中...\",\n\t\t\t\t\"unknown\": \"未知狀態\"\n\t\t\t},\n\t\t\t\"supported_formats\": \"支援的格式：\"\n\t\t},\n\t\t\"tooltip\": {\n\t\t\t\"partial_support\": \"此格式僅可作為{direction}進行轉換。\",\n\t\t\t\"direction_input\": \"輸入（來源）\",\n\t\t\t\"direction_output\": \"輸出（目標）\",\n\t\t\t\"video_server_processing\": \"影片預設上傳到伺服器進行處理，點擊這裡了解如何在本機設定。\"\n\t\t}\n\t},\n\t\"convert\": {\n\t\t\"archive_file\": {\n\t\t\t\"extracting\": \"偵測到 ZIP 壓縮檔：{filename}\",\n\t\t\t\"extracted\": \"從 {filename} 中提取了 {extract_count} 個檔案。{ignore_count} 個項目被忽略。\",\n\t\t\t\"extract_error\": \"提取 {filename} 時出錯：{error}\"\n\t\t},\n\t\t\"large_file_warning\": \"由於瀏覽器/裝置限制，此檔案大於 {limit}GB，影片轉音訊功能已停用。我們建議使用 Firefox 或 Safari 處理此大小的檔案，因為它們的限制較少。\",\n\t\t\"external_warning\": {\n\t\t\t\"title\": \"外部伺服器警告\",\n\t\t\t\"text\": \"如果你選擇轉換為影片格式，這些檔案將被上傳到外部伺服器進行轉換。是否繼續？\",\n\t\t\t\"yes\": \"是\",\n\t\t\t\"no\": \"否\"\n\t\t},\n\t\t\"panel\": {\n\t\t\t\"convert_all\": \"全部轉換\",\n\t\t\t\"download_all\": \"下載全部為 .zip\",\n\t\t\t\"remove_all\": \"刪除所有檔案\",\n\t\t\t\"set_all_to\": \"全部設定為\",\n\t\t\t\"na\": \"不適用\"\n\t\t},\n\t\t\"dropdown\": {\n\t\t\t\"audio\": \"音訊\",\n\t\t\t\"video\": \"影片\",\n\t\t\t\"doc\": \"文件\",\n\t\t\t\"image\": \"圖片\",\n\t\t\t\"placeholder\": \"搜尋格式\"\n\t\t},\n\t\t\"tooltips\": {\n\t\t\t\"unknown_file\": \"未知檔案類型\",\n\t\t\t\"audio_file\": \"音訊檔案\",\n\t\t\t\"video_file\": \"影片檔案\",\n\t\t\t\"document_file\": \"文件檔案\",\n\t\t\t\"image_file\": \"圖片檔案\",\n\t\t\t\"convert_file\": \"轉換此檔案\",\n\t\t\t\"download_file\": \"下載此檔案\"\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"cant_convert\": \"無法轉換此檔案。\",\n\t\t\t\"vertd_server\": \"你在做什麼...？你應該執行 vertd 伺服器！\",\n\t\t\t\"vertd_generic_view\": \"檢視錯誤詳情\",\n\t\t\t\"vertd_generic_body\": \"嘗試轉換影片時發生錯誤。你想將此影片提交給開發者以協助修復此錯誤嗎？只會傳送你的影片檔案，不會上傳任何識別碼。\",\n\t\t\t\"vertd_generic_title\": \"影片轉換錯誤\",\n\t\t\t\"vertd_generic_yes\": \"提交影片\",\n\t\t\t\"vertd_generic_no\": \"不提交\",\n\t\t\t\"vertd_failed_to_keep\": \"無法在伺服器上保留影片：{error}\",\n\t\t\t\"vertd_details\": \"檢視錯誤詳情\",\n\t\t\t\"vertd_details_body\": \"如果你按下提交，<b>你的影片也會被附加</b>在錯誤日誌旁邊，日誌會自動報告給我們審核。以下資訊是我們自動接收的日誌：\",\n\t\t\t\"vertd_details_footer\": \"此資訊僅用於故障排除，絕不會被分享。檢視我們的[privacy_link]隱私權政策[/privacy_link]以了解更多詳情。\",\n\t\t\t\"vertd_details_job_id\": \"<b>任務 ID：</b>{jobId}\",\n\t\t\t\"vertd_details_from\": \"<b>來源格式：</b>{from}\",\n\t\t\t\"vertd_details_to\": \"<b>目標格式：</b>{to}\",\n\t\t\t\"vertd_details_error_message\": \"<b>錯誤訊息：</b>[view_link]檢視錯誤日誌[/view_link]\",\n\t\t\t\"vertd_details_close\": \"關閉\",\n\t\t\t\"unsupported_format\": \"僅支援圖片、影片、音訊和文件檔案\",\n\t\t\t\"format_output_only\": \"此格式目前只能用作輸出（轉換目標），不能用作輸入。\",\n\t\t\t\"vertd_not_found\": \"未找到 vertd 執行個體來開始影片轉換。請確保執行個體 URL 設定正確。\",\n\t\t\t\"worker_downloading\": \"{type}轉換器正在初始化，請稍候。\",\n\t\t\t\"worker_error\": \"{type}轉換器初始化時出錯，請稍後重試。\",\n\t\t\t\"worker_timeout\": \"{type}轉換器初始化時間超出預期，請再等待一會兒或重新整理頁面。\",\n\t\t\t\"audio\": \"音訊\",\n\t\t\t\"doc\": \"文件\",\n\t\t\t\"image\": \"圖片\"\n\t\t}\n\t},\n\t\"settings\": {\n\t\t\"title\": \"設定\",\n\t\t\"errors\": {\n\t\t\t\"save_failed\": \"儲存設定失敗！\"\n\t\t},\n\t\t\"appearance\": {\n\t\t\t\"title\": \"外觀\",\n\t\t\t\"brightness_theme\": \"亮度主題\",\n\t\t\t\"brightness_description\": \"想要陽光明媚的閃光彈，還是寧靜孤獨的夜晚？\",\n\t\t\t\"light\": \"淺色\",\n\t\t\t\"dark\": \"深色\",\n\t\t\t\"effect_settings\": \"效果設定\",\n\t\t\t\"effect_description\": \"你想要華麗的效果，還是更靜態的體驗？\",\n\t\t\t\"enable\": \"啟用\",\n\t\t\t\"disable\": \"停用\"\n\t\t},\n\t\t\"conversion\": {\n\t\t\t\"title\": \"轉換\",\n\t\t\t\"advanced_settings\": \"進階設定\",\n\t\t\t\"filename_format\": \"檔案名稱格式\",\n\t\t\t\"filename_description\": \"這將決定下載時的檔案名稱，<b>不包括副檔名。</b>你可以在格式中使用以下範本，它們將被替換為相關資訊：<b>%name%</b>表示原始檔案名稱，<b>%extension%</b>表示原始副檔名，<b>%date%</b>表示檔案轉換時的日期字串。\",\n\t\t\t\"placeholder\": \"VERT_%name%\",\n\t\t\t\"default_format\": \"預設轉換格式\",\n\t\t\t\"default_format_description\": \"這將更改上傳此檔案類型時自動選擇的預設格式。\",\n\t\t\t\"default_format_image\": \"圖片\",\n\t\t\t\"default_format_video\": \"影片\",\n\t\t\t\"default_format_audio\": \"音訊\",\n\t\t\t\"default_format_document\": \"文件\",\n\t\t\t\"metadata\": \"檔案中繼資料\",\n\t\t\t\"metadata_description\": \"這將更改轉換後的檔案是否保留原始檔案的中繼資料（EXIF、歌曲資訊等）。\",\n\t\t\t\"keep\": \"保留\",\n\t\t\t\"remove\": \"移除\",\n\t\t\t\"quality\": \"轉換品質\",\n\t\t\t\"quality_description\": \"更改輸出檔案的品質。值越高，處理時間和檔案大小越大。\",\n\t\t\t\"quality_video\": \"更改影片轉換的品質。品質越高，轉換時間和檔案大小越大。\",\n\t\t\t\"quality_audio\": \"音訊（kbps）\",\n\t\t\t\"quality_images\": \"圖片（%）\",\n\t\t\t\"rate\": \"取樣率（Hz）\"\n\t\t},\n\t\t\"vertd\": {\n\t\t\t\"title\": \"影片轉換\",\n\t\t\t\"status\": \"狀態：\",\n\t\t\t\"loading\": \"載入中...\",\n\t\t\t\"available\": \"可用（提交 ID {commitId}）\",\n\t\t\t\"unavailable\": \"不可用（URL 正確嗎？）\",\n\t\t\t\"description\": \"<code>vertd</code>專案是 FFmpeg 的伺服器包裝器。這允許你透過 VERT 網頁介面方便地轉換影片，同時仍能利用 GPU 的強大效能以儘可能快的速度完成轉換。\",\n\t\t\t\"hosting_info\": \"我們為你提供了一個公共執行個體以方便使用，但如果你知道如何操作，在自己的電腦或伺服器上託管也很容易。你可以在[vertd_link]這裡[/vertd_link]下載伺服器二進位檔案 - 設定程序將來會變得更簡單，敬請期待！\",\n\t\t\t\"instance\": \"執行個體\",\n\t\t\t\"url_placeholder\": \"例如：http://localhost:24153\",\n\t\t\t\"conversion_speed\": \"轉換速度\",\n\t\t\t\"speed_description\": \"這描述了速度和品質之間的權衡。速度越快品質越低，但完成工作的速度更快。\",\n\t\t\t\"speeds\": {\n\t\t\t\t\"very_slow\": \"非常慢\",\n\t\t\t\t\"slower\": \"較慢\",\n\t\t\t\t\"slow\": \"慢\",\n\t\t\t\t\"medium\": \"中等\",\n\t\t\t\t\"fast\": \"快\",\n\t\t\t\t\"ultra_fast\": \"超快\"\n\t\t\t},\n\t\t\t\"auto_instance\": \"自動（建議）\",\n\t\t\t\"eu_instance\": \"德國法爾肯施泰因\",\n\t\t\t\"us_instance\": \"美國華盛頓\",\n\t\t\t\"custom_instance\": \"自訂\"\n\t\t},\n\t\t\"privacy\": {\n\t\t\t\"title\": \"隱私權與資料\",\n\t\t\t\"plausible_title\": \"Plausible 分析\",\n\t\t\t\"plausible_description\": \"我們使用[plausible_link]Plausible[/plausible_link]，一個注重隱私權的分析工具，來收集完全匿名的統計資料。所有資料都是匿名和彙總的，不會傳送或儲存任何可識別資訊。你可以在[analytics_link]這裡[/analytics_link]檢視分析資料，並在下方選擇退出。\",\n\t\t\t\"opt_in\": \"選擇加入\",\n\t\t\t\"opt_out\": \"選擇退出\",\n\t\t\t\"cache_title\": \"快取管理\",\n\t\t\t\"cache_description\": \"我們在瀏覽器中快取轉換器檔案，這樣你就不必每次都重新下載，從而提高效能並減少資料使用。\",\n\t\t\t\"refresh_cache\": \"重新整理快取\",\n\t\t\t\"clear_cache\": \"清除快取\",\n\t\t\t\"files_cached\": \"{size}（{count}個檔案）\",\n\t\t\t\"loading_cache\": \"載入中...\",\n\t\t\t\"total_size\": \"總大小\",\n\t\t\t\"files_cached_label\": \"已快取檔案\",\n\t\t\t\"cache_cleared\": \"快取已成功清除！\",\n\t\t\t\"cache_clear_error\": \"清除快取失敗。\",\n\t\t\t\"site_data_title\": \"網站資料管理\",\n\t\t\t\"site_data_description\": \"清除所有網站資料，包括設定和快取檔案，將 VERT 重置為預設狀態並重新載入頁面。\",\n\t\t\t\"clear_all_data\": \"清除所有網站資料\",\n\t\t\t\"clear_all_data_confirm_title\": \"清除所有網站資料？\",\n\t\t\t\"clear_all_data_confirm\": \"這將重置所有設定和快取，然後重新載入頁面。此操作無法復原。\",\n\t\t\t\"clear_all_data_cancel\": \"取消\",\n\t\t\t\"all_data_cleared\": \"所有網站資料已清除！正在重新載入頁面...\",\n\t\t\t\"all_data_clear_error\": \"清除所有網站資料失敗。\"\n\t\t},\n\t\t\"language\": {\n\t\t\t\"title\": \"語言\",\n\t\t\t\"description\": \"選擇 VERT 介面的偏好語言。\"\n\t\t}\n\t},\n\t\"about\": {\n\t\t\"title\": \"關於\",\n\t\t\"why\": {\n\t\t\t\"title\": \"為什麼選擇 VERT？\",\n\t\t\t\"description\": \"<b>檔案轉換器一直讓我們失望。</b>它們很醜陋，充滿廣告，最重要的是；很慢。我們決定透過製作一個解決所有這些問題的替代方案，一勞永逸地解決這個問題。<br/><br/>所有非影片檔案都完全在裝置上轉換；這意味著不需要在伺服器之間傳送和接收檔案的延遲，而且我們永遠不會窺探你轉換的檔案。<br/><br/>影片檔案會上傳到我們超快速的 RTX 4000 Ada 伺服器。如果你不轉換影片，它們會在伺服器上保留一小時。如果你轉換檔案，影片將在伺服器上保留一小時，或直到下載完成。然後檔案將從我們的伺服器中刪除。\"\n\t\t},\n\t\t\"sponsors\": {\n\t\t\t\"title\": \"贊助商\",\n\t\t\t\"description\": \"想支援我們嗎？請在[discord_link]Discord[/discord_link]伺服器上聯絡開發者，或傳送電子郵件至\",\n\t\t\t\"email_copied\": \"電子郵件已複製到剪貼簿！\"\n\t\t},\n\t\t\"resources\": {\n\t\t\t\"title\": \"資源\",\n\t\t\t\"discord\": \"Discord\",\n\t\t\t\"source\": \"原始碼\",\n\t\t\t\"email\": \"電子郵件\"\n\t\t},\n\t\t\"donate\": {\n\t\t\t\"title\": \"捐贈給 VERT\",\n\t\t\t\"description\": \"有了你的支援，我們可以繼續維護和改進 VERT。\",\n\t\t\t\"one_time\": \"一次性\",\n\t\t\t\"monthly\": \"每月\",\n\t\t\t\"custom\": \"自訂\",\n\t\t\t\"pay_now\": \"立即付款\",\n\t\t\t\"donate_amount\": \"捐贈 ${amount} 美元\",\n\t\t\t\"thank_you\": \"感謝你的捐贈！\",\n\t\t\t\"payment_failed\": \"付款失敗：{message}{period}你未被收費。\",\n\t\t\t\"donation_error\": \"處理捐贈時出錯。請稍後重試。\",\n\t\t\t\"payment_error\": \"取得付款詳情時出錯。請稍後重試。\"\n\t\t},\n\t\t\"credits\": {\n\t\t\t\"title\": \"致謝\",\n\t\t\t\"contact_team\": \"如果你想聯絡開發團隊，請使用「資源」卡片上的電子郵件。\",\n\t\t\t\"notable_contributors\": \"傑出貢獻者\",\n\t\t\t\"notable_description\": \"我們要感謝這些人對 VERT 的重大貢獻。\",\n\t\t\t\"github_contributors\": \"GitHub 貢獻者\",\n\t\t\t\"github_description\": \"非常感謝所有這些人的協助！[github_link]也想幫忙嗎？[/github_link]\",\n\t\t\t\"no_contributors\": \"似乎還沒有人貢獻……[contribute_link]成為第一個貢獻者！[/contribute_link]\",\n\t\t\t\"libraries\": \"程式庫\",\n\t\t\t\"libraries_description\": \"非常感謝 FFmpeg（音訊、影片）、ImageMagick（圖片）和 Pandoc（文件）多年來維護如此出色的程式庫。VERT 依賴它們為你提供轉換服務。\",\n\t\t\t\"roles\": {\n\t\t\t\t\"lead_developer\": \"首席開發者；轉換後端、UI 實作\",\n\t\t\t\t\"developer\": \"開發者；UI 實作\",\n\t\t\t\t\"designer\": \"設計師；使用者體驗、品牌、行銷\",\n\t\t\t\t\"docker_ci\": \"維護 Docker 和 CI 支援\",\n\t\t\t\t\"former_cofounder\": \"前共同創辦人和設計師\"\n\t\t\t}\n\t\t},\n\t\t\"errors\": {\n\t\t\t\"github_contributors\": \"取得 GitHub 貢獻者時出錯\"\n\t\t}\n\t},\n\t\"workers\": {\n\t\t\"errors\": {\n\t\t\t\"general\": \"轉換 {file} 時出錯：{message}\",\n\t\t\t\"cancel\": \"取消轉換 {file} 時出錯：{message}\",\n\t\t\t\"magick\": \"Magick worker 出錯，圖片轉換可能無法正常運作。\",\n\t\t\t\"ffmpeg\": \"載入 ffmpeg 時出錯，某些功能可能無法運作。\",\n\t\t\t\"pandoc\": \"載入 Pandoc worker 時出錯，文件轉換可能無法正常運作。\",\n\t\t\t\"no_audio\": \"未找到音訊串流。\",\n\t\t\t\"invalid_rate\": \"指定的取樣率無效：{rate}Hz\",\n\t\t\t\"file_too_large\": \"此檔案超過 {limit}GB 瀏覽器/裝置限制。請嘗試使用 Firefox 或 Safari 轉換此大型檔案，它們通常具有較高的限制。\"\n\t\t}\n\t},\n\t\"privacy\": {\n\t\t\"title\": \"隱私權政策\",\n\t\t\"summary\": {\n\t\t\t\"title\": \"摘要\",\n\t\t\t\"description\": \"VERT 的隱私權政策非常簡單：我們根本不收集或儲存你的任何資料。我們不使用 cookie 或追蹤器，分析是完全私密的，所有轉換（影片除外）都在你的瀏覽器本機進行。影片在下載後或一小時後刪除，除非你明確授權儲存；它只會用於故障排除。VERT 自託管 Coolify 執行個體用於託管網站和 vertd（用於影片轉換），以及用於完全匿名和彙總分析的 Plausible 執行個體。<br/><br/>請注意，這可能僅適用於[vert_link]vert.sh[/vert_link]的官方 VERT 執行個體；第三方執行個體可能以不同方式處理你的資料。\"\n\t\t},\n\t\t\"conversions\": {\n\t\t\t\"title\": \"轉換\",\n\t\t\t\"description\": \"大多數轉換（圖片、文件、音訊）完全在你的裝置上本機使用相關工具的 WebAssembly 版本（例如 ImageMagick、Pandoc、FFmpeg）進行。這意味著你的檔案永遠不會離開你的裝置，我們也永遠無法存取它們。<br/><br/>影片轉換在我們的伺服器上進行，因為它們需要更多的處理能力，並且目前無法在瀏覽器上非常快速地完成。你使用 VERT 轉換的影片在下載後或一小時後刪除，除非你明確授權我們將它們儲存更長時間，純粹用於故障排除。\"\n\t\t},\n\t\t\"conversion_errors\": {\n\t\t\t\"title\": \"轉換錯誤\",\n\t\t\t\"description\": \"當影片轉換失敗時，我們可能會收集一些匿名資料以協助我們診斷問題。這些資料可能包括：\",\n\t\t\t\"list_job_id\": \"任務 ID，即匿名化的檔案名稱\",\n\t\t\t\"list_format_from\": \"你轉換的來源格式\",\n\t\t\t\"list_format_to\": \"你轉換的目標格式\",\n\t\t\t\"list_stderr\": \"你任務的 FFmpeg stderr 輸出（錯誤訊息）\",\n\t\t\t\"list_video\": \"實際影片檔案（如果明確授權）\",\n\t\t\t\"footer\": \"此資訊僅用於診斷轉換問題。只有在你明確授權的情況下，才會收集實際影片檔案，並且僅用於故障排除。\"\n\t\t},\n\t\t\"analytics\": {\n\t\t\t\"title\": \"分析\",\n\t\t\t\"description\": \"我們自託管 Plausible 執行個體用於完全匿名和彙總的分析。Plausible 不使用 cookie，並符合所有主要隱私權法規（GDPR/CCPA/PECR）。你可以在[settings_link]設定[/settings_link]的「隱私權與資料」部分選擇退出分析，並在[plausible_link]這裡[/plausible_link]閱讀更多關於 Plausible 隱私權實務的資訊。\"\n\t\t},\n\t\t\"local_storage\": {\n\t\t\t\"title\": \"本機儲存\",\n\t\t\t\"description\": \"我們使用瀏覽器的本機儲存來儲存你的設定，使用瀏覽器的工作階段儲存來暫時儲存「關於」部分的 GitHub 貢獻者清單，以減少重複的 GitHub API 請求。不會儲存或傳輸任何個人資料。<br/><br/>我們使用的轉換工具的 WebAssembly 版本（FFmpeg、ImageMagick、Pandoc）也會在你首次造訪網站時本機儲存在瀏覽器中，這樣你就不需要每次造訪時都重新下載它們。不會儲存或傳輸任何個人資料。你可以隨時在[settings_link]設定[/settings_link]的「隱私權與資料」部分檢視或刪除這些資料。\"\n\t\t},\n\t\t\"contact\": {\n\t\t\t\"title\": \"聯絡\",\n\t\t\t\"description\": \"如有問題，請傳送電子郵件至：[email_link]hello@vert.sh[/email_link]。如果你使用的是第三方 VERT 執行個體，請聯絡該執行個體的託管者。\"\n\t\t},\n\t\t\"last_updated\": \"最後更新：2025-10-19\"\n\t}\n}\n"
  },
  {
    "path": "nginx/default-ssl.conf",
    "content": "server {\n    listen 80;\n    server_name vert;\n\n    # Redirect all HTTP traffic to HTTPS\n    return 301 https://$host$request_uri;\n}\n\nserver {\n    listen 443 ssl;\n    server_name vert;\n\n    ssl_certificate     /etc/ssl/vert/vert.crt;\n    ssl_certificate_key /etc/ssl/vert/vert.key;\n\n    root /usr/share/nginx/html;\n    index index.html;\n\n    client_max_body_size 10M;\n\n    # Security headers\n    add_header X-Frame-Options \"SAMEORIGIN\" always;\n    add_header X-Content-Type-Options \"nosniff\" always;\n    add_header Referrer-Policy \"strict-origin-when-cross-origin\" always;\n    add_header Permissions-Policy \"geolocation=(), microphone=(), camera=()\" always;\n    add_header Strict-Transport-Security \"max-age=31536000; includeSubDomains\" always;\n\n    location / {\n        try_files $uri $uri/ /index.html;\n    }\n    \n    error_page 404 /index.html;\n}"
  },
  {
    "path": "nginx/default.conf",
    "content": "server {\n    listen 80;\n    listen [::]:80;\n    server_name vert;\n\n    root /usr/share/nginx/html;\n    index index.html;\n\n    client_max_body_size 10M;\n\n    # Security headers\n    add_header X-Frame-Options \"SAMEORIGIN\" always;\n    add_header X-Content-Type-Options \"nosniff\" always;\n    add_header Referrer-Policy \"strict-origin-when-cross-origin\" always;\n    add_header Permissions-Policy \"geolocation=(), microphone=(), camera=()\" always;\n\n    location / {\n        try_files $uri $uri/ /index.html;\n    }\n    \n    error_page 404 /index.html;\n}"
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"vert\",\n\t\"version\": \"0.0.1\",\n\t\"type\": \"module\",\n\t\"scripts\": {\n\t\t\"dev\": \"vite dev\",\n\t\t\"build\": \"paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide && vite build\",\n\t\t\"preview\": \"vite preview\",\n\t\t\"check\": \"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json\",\n\t\t\"check:watch\": \"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch\",\n\t\t\"format\": \"prettier --write .\",\n\t\t\"lint\": \"prettier --check . && eslint .\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@inlang/paraglide-js\": \"^2.5.0\",\n\t\t\"@poppanator/sveltekit-svg\": \"^5.0.1\",\n\t\t\"@sveltejs/adapter-static\": \"^3.0.10\",\n\t\t\"@sveltejs/kit\": \"^2.49.0\",\n\t\t\"@sveltejs/vite-plugin-svelte\": \"^4.0.4\",\n\t\t\"@types/eslint\": \"^9.6.1\",\n\t\t\"@types/sanitize-html\": \"^2.16.0\",\n\t\t\"autoprefixer\": \"^10.4.22\",\n\t\t\"css-select\": \"5.1.0\",\n\t\t\"eslint\": \"^9.39.1\",\n\t\t\"eslint-config-prettier\": \"^10.1.8\",\n\t\t\"eslint-plugin-svelte\": \"^2.46.1\",\n\t\t\"globals\": \"^15.15.0\",\n\t\t\"prettier\": \"^3.6.2\",\n\t\t\"prettier-plugin-svelte\": \"^3.4.0\",\n\t\t\"prettier-plugin-tailwindcss\": \"^0.6.14\",\n\t\t\"sass\": \"^1.94.2\",\n\t\t\"svelte\": \"^5.43.14\",\n\t\t\"svelte-check\": \"^4.3.4\",\n\t\t\"tailwindcss\": \"^3.4.18\",\n\t\t\"typescript\": \"^5.9.3\",\n\t\t\"typescript-eslint\": \"^8.47.0\",\n\t\t\"vite\": \"^5.4.21\",\n\t\t\"vite-plugin-top-level-await\": \"^1.6.0\"\n\t},\n\t\"dependencies\": {\n\t\t\"@bjorn3/browser_wasi_shim\": \"^0.4.2\",\n\t\t\"@ffmpeg/ffmpeg\": \"^0.12.15\",\n\t\t\"@ffmpeg/util\": \"^0.12.2\",\n\t\t\"@fontsource/azeret-mono\": \"^5.2.11\",\n\t\t\"@fontsource/lexend\": \"^5.2.11\",\n\t\t\"@fontsource/radio-canada-big\": \"^5.2.7\",\n\t\t\"@imagemagick/magick-wasm\": \"^0.0.37\",\n\t\t\"@stripe/stripe-js\": \"^8.5.2\",\n\t\t\"byte-data\": \"^19.0.1\",\n\t\t\"client-zip\": \"^2.5.0\",\n\t\t\"clsx\": \"^2.1.1\",\n\t\t\"fflate\": \"^0.8.2\",\n\t\t\"lucide-svelte\": \"^0.554.0\",\n\t\t\"music-metadata\": \"^11.10.3\",\n\t\t\"overlayscrollbars\": \"^2.12.0\",\n\t\t\"overlayscrollbars-svelte\": \"^0.5.5\",\n\t\t\"p-queue\": \"^9.0.1\",\n\t\t\"riff-file\": \"^1.0.3\",\n\t\t\"sanitize-html\": \"^2.17.0\",\n\t\t\"svelte-stripe\": \"^1.4.0\",\n\t\t\"vert-wasm\": \"^0.0.2\",\n\t\t\"vite-plugin-wasm\": \"^3.5.0\"\n\t},\n\t\"trustedDependencies\": [\n\t\t\"@parcel/watcher\",\n\t\t\"@swc/core\"\n\t]\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "export default {\n\tplugins: {\n\t\ttailwindcss: {},\n\t\tautoprefixer: {}\n\t}\n};\n"
  },
  {
    "path": "project.inlang/.gitignore",
    "content": "cache"
  },
  {
    "path": "project.inlang/project_id",
    "content": "ff77Td2rnvEqQyzBYT"
  },
  {
    "path": "project.inlang/settings.json",
    "content": "{\n\t\"$schema\": \"https://inlang.com/schema/project-settings\",\n\t\"baseLocale\": \"en\",\n\t\"locales\": [\n\t\t\"en\",\n\t\t\"es\",\n\t\t\"fr\",\n\t\t\"de\",\n\t\t\"it\",\n\t\t\"ba\",\n\t\t\"hr\",\n\t\t\"tr\",\n\t\t\"ja\",\n\t\t\"ko\",\n\t\t\"el\",\n\t\t\"id\",\n\t\t\"zh-Hans\",\n\t\t\"zh-Hant\",\n\t\t\"pt-BR\"\n\t],\n\t\"modules\": [\n\t\t\"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js\",\n\t\t\"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js\"\n\t],\n\t\"plugin.inlang.messageFormat\": {\n\t\t\"pathPattern\": \"./messages/{locale}.json\"\n\t}\n}\n"
  },
  {
    "path": "src/app.d.ts",
    "content": "import \"@poppanator/sveltekit-svg/dist/svg\";\n\ntype EventPayload = {\n\treadonly n: string;\n\treadonly u: Location[\"href\"];\n\treadonly d: Location[\"hostname\"];\n\treadonly r: Document[\"referrer\"] | null;\n\treadonly w: Window[\"innerWidth\"];\n\treadonly h: 1 | 0;\n\treadonly p?: string;\n};\n\ntype CallbackArgs = {\n\treadonly status: number;\n};\n\ntype EventOptions = {\n\t/**\n\t * Callback called when the event is successfully sent.\n\t */\n\treadonly callback?: (args: CallbackArgs) => void;\n\t/**\n\t * Properties to be bound to the event.\n\t */\n\treadonly props?: { readonly [propName: string]: string | number | boolean };\n};\n\ndeclare global {\n\tinterface Window {\n\t\tplausible: TrackEvent;\n\t}\n\n\tconst __COMMIT_HASH__: string;\n}\n\n/**\n * Options used when initializing the tracker.\n */\nexport type PlausibleInitOptions = {\n\t/**\n\t * If true, pageviews will be tracked when the URL hash changes.\n\t * Enable this if you are using a frontend that uses hash-based routing.\n\t */\n\treadonly hashMode?: boolean;\n\t/**\n\t * Set to true if you want events to be tracked when running the site locally.\n\t */\n\treadonly trackLocalhost?: boolean;\n\t/**\n\t * The domain to bind the event to.\n\t * Defaults to `location.hostname`\n\t */\n\treadonly domain?: Location[\"hostname\"];\n\t/**\n\t * The API host where the events will be sent.\n\t * Defaults to `'https://plausible.io'`\n\t */\n\treadonly apiHost?: string;\n};\n\n/**\n * Data passed to Plausible as events.\n */\nexport type PlausibleEventData = {\n\t/**\n\t * The URL to bind the event to.\n\t * Defaults to `location.href`.\n\t */\n\treadonly url?: Location[\"href\"];\n\t/**\n\t * The referrer to bind the event to.\n\t * Defaults to `document.referrer`\n\t */\n\treadonly referrer?: Document[\"referrer\"] | null;\n\t/**\n\t * The current device's width.\n\t * Defaults to `window.innerWidth`\n\t */\n\treadonly deviceWidth?: Window[\"innerWidth\"];\n};\n\n/**\n * Options used when tracking Plausible events.\n */\nexport type PlausibleOptions = PlausibleInitOptions & PlausibleEventData;\n\n/**\n * Tracks a custom event.\n *\n * Use it to track your defined goals by providing the goal's name as `eventName`.\n *\n * ### Example\n * ```js\n * import Plausible from 'plausible-tracker'\n *\n * const { trackEvent } = Plausible()\n *\n * // Tracks the 'signup' goal\n * trackEvent('signup')\n *\n * // Tracks the 'Download' goal passing a 'method' property.\n * trackEvent('Download', { props: { method: 'HTTP' } })\n * ```\n *\n * @param eventName - Name of the event to track\n * @param options - Event options.\n * @param eventData - Optional event data to send. Defaults to the current page's data merged with the default options provided earlier.\n */\ntype TrackEvent = (\n\teventName: string,\n\toptions?: EventOptions,\n\teventData?: PlausibleOptions,\n) => void;\n\n/**\n * Manually tracks a page view.\n *\n * ### Example\n * ```js\n * import Plausible from 'plausible-tracker'\n *\n * const { trackPageview } = Plausible()\n *\n * // Track a page view\n * trackPageview()\n * ```\n *\n * @param eventData - Optional event data to send. Defaults to the current page's data merged with the default options provided earlier.\n * @param options - Event options.\n */\ntype TrackPageview = (\n\teventData?: PlausibleOptions,\n\toptions?: EventOptions,\n) => void;\n\n/**\n * Cleans up all event listeners attached.\n */\ntype Cleanup = () => void;\n\n/**\n * Tracks the current page and all further pages automatically.\n *\n * Call this if you don't want to manually manage pageview tracking.\n *\n * ### Example\n * ```js\n * import Plausible from 'plausible-tracker'\n *\n * const { enableAutoPageviews } = Plausible()\n *\n * // This tracks the current page view and all future ones as well\n * enableAutoPageviews()\n * ```\n *\n * The returned value is a callback that removes the added event listeners and restores `history.pushState`\n * ```js\n * import Plausible from 'plausible-tracker'\n *\n * const { enableAutoPageviews } = Plausible()\n *\n * const cleanup = enableAutoPageviews()\n *\n * // Remove event listeners and restore `history.pushState`\n * cleanup()\n * ```\n */\ntype EnableAutoPageviews = () => Cleanup;\n\n/**\n * Tracks all outbound link clicks automatically\n *\n * Call this if you don't want to manually manage these links.\n *\n * It works using a **[MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)** to automagically detect link nodes throughout your application and bind `click` events to them.\n *\n * Optionally takes the same parameters as [`MutationObserver.observe`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe).\n *\n * ### Example\n * ```js\n * import Plausible from 'plausible-tracker'\n *\n * const { enableAutoOutboundTracking } = Plausible()\n *\n * // This tracks all the existing and future outbound links on your page.\n * enableAutoOutboundTracking()\n * ```\n *\n * The returned value is a callback that removes the added event listeners and disconnects the observer\n * ```js\n * import Plausible from 'plausible-tracker'\n *\n * const { enableAutoOutboundTracking } = Plausible()\n *\n * const cleanup = enableAutoOutboundTracking()\n *\n * // Remove event listeners and disconnect the observer\n * cleanup()\n * ```\n */\ntype EnableAutoOutboundTracking = (\n\ttargetNode?: Node & ParentNode,\n\tobserverInit?: MutationObserverInit,\n) => Cleanup;\n\n/**\n * Initializes the tracker with your default values.\n *\n * ### Example (es module)\n * ```js\n * import Plausible from 'plausible-tracker'\n *\n * const { enableAutoPageviews, trackEvent } = Plausible({\n *   domain: 'my-app-domain.com',\n *   hashMode: true\n * })\n *\n * enableAutoPageviews()\n *\n * function onUserRegister() {\n *   trackEvent('register')\n * }\n * ```\n *\n * ### Example (commonjs)\n * ```js\n * var Plausible = require('plausible-tracker');\n *\n * var { enableAutoPageviews, trackEvent } = Plausible({\n *   domain: 'my-app-domain.com',\n *   hashMode: true\n * })\n *\n * enableAutoPageviews()\n *\n * function onUserRegister() {\n *   trackEvent('register')\n * }\n * ```\n *\n * @param defaults - Default event parameters that will be applied to all requests.\n */\n\ndeclare global {\n\tnamespace App {\n\t\t// interface Error {}\n\t\t// interface Locals {}\n\t\t// interface PageData {}\n\t\t// interface PageState {}\n\t\t// interface Platform {}\n\t}\n}\n\ndeclare module \"svelte/elements\" {\n\t// eslint-disable-next-line @typescript-eslint/no-unused-vars\n\tinterface HTMLAttributes<T> {\n\t\t[key: `event-${string}`]: string | undefined | null;\n\t}\n}\n\nexport {};\n"
  },
  {
    "path": "src/app.html",
    "content": "<!doctype html>\r\n<html lang=\"%lang%\">\r\n\t<head>\r\n\t\t<meta charset=\"utf-8\" />\r\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\r\n\t\t<link rel=\"icon\" href=\"%sveltekit.assets%/favicon.png\" />\r\n\t\t<link rel=\"apple-touch-icon\" href=\"%sveltekit.assets%/favicon.png\" />\r\n\r\n\t\t<link\r\n\t\t\trel=\"apple-touch-startup-image\"\r\n\t\t\thref=\"%sveltekit.assets%/lettermark.jpg\"\r\n\t\t/>\r\n\t\t<meta name=\"mobile-web-app-capable\" content=\"yes\" />\r\n\t\t<meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\r\n\t\t<meta\r\n\t\t\tname=\"apple-mobile-web-app-status-bar-style\"\r\n\t\t\tcontent=\"black-translucent\"\r\n\t\t/>\r\n\r\n\t\t%sveltekit.head%\r\n\t\t<script>\r\n\t\t\t(function () {\r\n\t\t\t\t// Apply theme before DOM is loaded\r\n\t\t\t\tlet theme = localStorage.getItem(\"theme\");\r\n\t\t\t\tconst prefersDark = window.matchMedia(\r\n\t\t\t\t\t\"(prefers-color-scheme: dark)\",\r\n\t\t\t\t).matches;\r\n\t\t\t\tconsole.log(\r\n\t\t\t\t\t`Theme: ${theme || \"N/A\"}, prefers dark: ${prefersDark}`,\r\n\t\t\t\t);\r\n\r\n\t\t\t\tif (theme !== \"light\" && theme !== \"dark\") {\r\n\t\t\t\t\tconsole.log(\"Invalid theme, setting to default\");\r\n\t\t\t\t\ttheme = prefersDark ? \"dark\" : \"light\";\r\n\t\t\t\t\tlocalStorage.setItem(\"theme\", theme);\r\n\t\t\t\t}\r\n\r\n\t\t\t\tconsole.log(`Applying theme: ${theme}`);\r\n\t\t\t\tdocument.documentElement.classList.add(theme);\r\n\r\n\t\t\t\t// Lock dark reader if it's set to dark mode\r\n\t\t\t\tif (theme === \"dark\") {\r\n\t\t\t\t\tconst lock = document.createElement(\"meta\");\r\n\t\t\t\t\tlock.name = \"darkreader-lock\";\r\n\t\t\t\t\tdocument.head.appendChild(lock);\r\n\t\t\t\t}\r\n\t\t\t})();\r\n\t\t</script>\r\n\t</head>\r\n\t<body data-sveltekit-preload-data=\"hover\">\r\n\t\t<div style=\"display: contents\">%sveltekit.body%</div>\r\n\t</body>\r\n</html>\r\n"
  },
  {
    "path": "src/hooks.server.ts",
    "content": "import type { Handle } from \"@sveltejs/kit\";\nimport { paraglideMiddleware } from \"$lib/paraglide/server\";\n\n// creating a handle to use the paraglide middleware\nconst paraglideHandle: Handle = ({ event, resolve }) =>\n\tparaglideMiddleware(\n\t\tevent.request,\n\t\t({ request: localizedRequest, locale }) => {\n\t\t\tevent.request = localizedRequest;\n\t\t\treturn resolve(event, {\n\t\t\t\ttransformPageChunk: ({ html }) => {\n\t\t\t\t\treturn html.replace(\"%lang%\", locale);\n\t\t\t\t},\n\t\t\t});\n\t\t},\n\t);\n\nexport const handle: Handle = paraglideHandle;\n"
  },
  {
    "path": "src/hooks.ts",
    "content": "import type { Reroute } from \"@sveltejs/kit\";\nimport { deLocalizeUrl } from \"$lib/paraglide/runtime\";\n\nexport const reroute: Reroute = (request) => {\n\treturn deLocalizeUrl(request.url).pathname;\n};\n"
  },
  {
    "path": "src/lib/assets/style/host-grotesk.css",
    "content": "@font-face {\n\tfont-family: \"Host Grotesk\";\n\tfont-style: normal;\n\tfont-weight: 400;\n\tsrc: url(\"$lib/assets/font/HostGrotesk-Regular.woff2\") format(\"woff2\");\n}\n\n@font-face {\n\tfont-family: \"Host Grotesk\";\n\tfont-style: italic;\n\tfont-weight: 400;\n\tsrc: url(\"$lib/assets/font/HostGrotesk-Italic.woff2\") format(\"woff2\");\n}\n\n@font-face {\n\tfont-family: \"Host Grotesk\";\n\tfont-style: normal;\n\tfont-weight: 500;\n\tsrc: url(\"$lib/assets/font/HostGrotesk-Medium.woff2\") format(\"woff2\");\n}\n\n@font-face {\n\tfont-family: \"Host Grotesk\";\n\tfont-style: italic;\n\tfont-weight: 500;\n\tsrc: url(\"$lib/assets/font/HostGrotesk-MediumItalic.woff2\") format(\"woff2\");\n}\n\n@font-face {\n\tfont-family: \"Host Grotesk\";\n\tfont-style: normal;\n\tfont-weight: 600;\n\tsrc: url(\"$lib/assets/font/HostGrotesk-SemiBold.woff2\") format(\"woff2\");\n}\n\n@font-face {\n\tfont-family: \"Host Grotesk\";\n\tfont-style: italic;\n\tfont-weight: 600;\n\tsrc: url(\"$lib/assets/font/HostGrotesk-SemiBoldItalic.woff2\")\n\t\tformat(\"woff2\");\n}\n"
  },
  {
    "path": "src/lib/components/functional/ConversionPanel.svelte",
    "content": "<script lang=\"ts\">\n\timport { effects, files, isMobile } from \"$lib/store/index.svelte\";\n\timport { FolderArchiveIcon, RefreshCw, Trash2Icon } from \"lucide-svelte\";\n\timport Panel from \"../visual/Panel.svelte\";\n\timport Dropdown from \"./Dropdown.svelte\";\n\timport Tooltip from \"../visual/Tooltip.svelte\";\n\timport ProgressBar from \"../visual/ProgressBar.svelte\";\n\timport FormatDropdown from \"./FormatDropdown.svelte\";\n\timport { categories } from \"$lib/converters\";\n\timport { m } from \"$lib/paraglide/messages\";\n\n\tconst length = $derived(files.files.length);\n\tconst progress = $derived(files.files.filter((f) => f.result).length);\n</script>\n\n<Panel class=\"flex flex-col gap-4\">\n\t<div\n\t\tclass=\"w-full h-auto flex items-center justify-between flex-col md:flex-row gap-4\"\n\t>\n\t\t<div\n\t\t\tclass=\"flex items-center flex-col md:flex-row gap-2.5 max-md:w-full\"\n\t\t>\n\t\t\t<button\n\t\t\t\tonclick={() => files.convertAll()}\n\t\t\t\tclass=\"btn {$effects\n\t\t\t\t\t? ''\n\t\t\t\t\t: '!scale-100'} highlight flex gap-3 max-md:w-full md:max-w-[15.5rem]\"\n\t\t\t\tdisabled={!files.ready}\n\t\t\t>\n\t\t\t\t<RefreshCw size=\"24\" />\n\t\t\t\t<p>{m[\"convert.panel.convert_all\"]()}</p>\n\t\t\t</button>\n\t\t\t<button\n\t\t\t\tclass=\"btn {$effects\n\t\t\t\t\t? ''\n\t\t\t\t\t: '!scale-100'} flex gap-3 max-md:w-full md:max-w-[15.5rem]\"\n\t\t\t\tdisabled={!files.ready || !files.results}\n\t\t\t\tonclick={() => files.downloadAll()}\n\t\t\t>\n\t\t\t\t<FolderArchiveIcon size=\"24\" />\n\t\t\t\t<p>{m[\"convert.panel.download_all\"]()}</p>\n\t\t\t</button>\n\t\t\t{#if $isMobile}\n\t\t\t\t<button\n\t\t\t\t\tclass=\"btn p-4 {$effects\n\t\t\t\t\t\t? ''\n\t\t\t\t\t\t: '!scale-100'} flex gap-3 max-md:w-full\"\n\t\t\t\t\tdisabled={files.files.length === 0}\n\t\t\t\t\tonclick={() => (files.files = [])}\n\t\t\t\t>\n\t\t\t\t\t<Trash2Icon size=\"24\" />\n\t\t\t\t\t<p>{m[\"convert.panel.remove_all\"]()}</p>\n\t\t\t\t</button>\n\t\t\t{:else}\n\t\t\t\t<Tooltip\n\t\t\t\t\ttext={m[\"convert.panel.remove_all\"]()}\n\t\t\t\t\tposition=\"right\"\n\t\t\t\t>\n\t\t\t\t\t<button\n\t\t\t\t\t\tclass=\"btn p-4 {$effects\n\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t: '!scale-100'} flex gap-3 max-md:w-full\"\n\t\t\t\t\t\tdisabled={files.files.length === 0}\n\t\t\t\t\t\tonclick={() => (files.files = [])}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Trash2Icon size=\"24\" />\n\t\t\t\t\t</button>\n\t\t\t\t</Tooltip>\n\t\t\t{/if}\n\t\t</div>\n\t\t<div class=\"w-full bg-separator h-0.5 flex md:hidden\"></div>\n\t\t<div class=\"flex items-center gap-2\">\n\t\t\t<p class=\"whitespace-normal text-xl text-right w-full\">\n\t\t\t\t{m[\"convert.panel.set_all_to\"]()}\n\t\t\t</p>\n\t\t\t<div class=\"w-48 md:max-w-[6.5rem]\">\n\t\t\t\t<!-- check if all files have the same converters -->\n\t\t\t\t<!-- video and audio together still have this dropdown disabled because audio has just ffmpeg (video has vertd & ffmpeg), even tho it can convert between video and audio  -->\n\t\t\t\t{#if files.files.length > 0 && files.files.every((f) => f.converters.length) && files.files.every((f) => JSON.stringify(f.converters) === JSON.stringify(files.files[0].converters))}\n\t\t\t\t\t<FormatDropdown\n\t\t\t\t\t\tonselect={(r) =>\n\t\t\t\t\t\t\tfiles.files.forEach((f) => {\n\t\t\t\t\t\t\t\tf.to = r;\n\t\t\t\t\t\t\t\tf.result = null;\n\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t{categories}\n\t\t\t\t\t\tdropdownSize={\"large\"}\n\t\t\t\t\t/>\n\t\t\t\t{:else}\n\t\t\t\t\t<Dropdown options={[m[\"convert.panel.na\"]()]} disabled />\n\t\t\t\t{/if}\n\t\t\t</div>\n\t\t</div>\n\t\t{#if files.files.length > 50}\n\t\t\t<div class=\"w-full px-2 flex gap-4 items-center\">\n\t\t\t\t<div\n\t\t\t\t\tclass=\"flex-shrink-0 -mt-0.5 font-normal text-sm text-muted\"\n\t\t\t\t>\n\t\t\t\t\t{progress}/{length}\n\t\t\t\t</div>\n\t\t\t\t<div class=\"flex-grow\">\n\t\t\t\t\t<ProgressBar min={0} max={length} {progress} />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t{/if}\n\t</div></Panel\n>\n"
  },
  {
    "path": "src/lib/components/functional/Dialog.svelte",
    "content": "<script lang=\"ts\">\n\timport { duration, fade, fly } from \"$lib/util/animation\";\n\timport { removeDialog } from \"$lib/store/DialogProvider\";\n\timport { BanIcon, CheckIcon, InfoIcon, TriangleAlert } from \"lucide-svelte\";\n\timport { quintOut } from \"svelte/easing\";\n\timport type { Dialog as DialogType } from \"$lib/store/DialogProvider\";\n\n\ttype Props = DialogType;\n\n\tlet props: Props = $props();\n\tconst { id, title, message, buttons, type } = props;\n\tconst additional = \"additional\" in props ? props.additional : undefined;\n\n\tconst colors = {\n\t\tsuccess: \"purple\",\n\t\terror: \"red\",\n\t\tinfo: \"blue\",\n\t\twarning: \"pink\",\n\t};\n\n\tconst Icons = {\n\t\tsuccess: CheckIcon,\n\t\terror: BanIcon,\n\t\tinfo: InfoIcon,\n\t\twarning: TriangleAlert,\n\t};\n\n\tlet color = $derived(colors[type]);\n\tlet Icon = $derived(Icons[type]);\n</script>\n\n<div\n\tclass=\"flex flex-col items-center justify-between w-full max-w-sm p-4 gap-6 bg-panel border-accent-{color}-alt rounded-lg shadow-md\"\n\tin:fly={{\n\t\tduration,\n\t\teasing: quintOut,\n\t\tx: 0,\n\t\ty: 100,\n\t}}\n\tout:fade={{\n\t\tduration,\n\t\teasing: quintOut,\n\t}}\n>\n\t<div class=\"flex justify-between w-full items-center\">\n\t\t<div class=\"flex items-center gap-3\">\n\t\t\t<div\n\t\t\t\tclass=\"rounded-full bg-accent-{color} p-2 inline-block w-8 h-8\"\n\t\t\t>\n\t\t\t\t<Icon size=\"16\" color=\"black\" />\n\t\t\t</div>\n\t\t\t<p class=\"text-lg font-semibold\">{title}</p>\n\t\t</div>\n\t</div>\n\t<div class=\"flex flex-col gap-1 w-full\">\n\t\t{#if typeof message === \"string\"}\n\t\t\t<p class=\"text-sm font-normal text-muted whitespace-pre-wrap\">{message}</p>\n\t\t{:else}\n\t\t\t{@const MessageComponent = message}\n\t\t\t<div class=\"text-sm font-normal text-muted\">\n\t\t\t\t<MessageComponent {id} {title} {type} {buttons} {additional} />\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n\t<div class=\"flex flex-row items-center gap-4 w-full\">\n\t\t{#each buttons as { text, action }, i}\n\t\t\t<button\n\t\t\t\tclass=\"hover:scale-105 active:scale-100 duration-200 flex items-center gap-2 p-2 rounded-md {i ===\n\t\t\t\t1\n\t\t\t\t\t? `bg-accent-${color} text-black`\n\t\t\t\t\t: 'bg-button text-black dynadark:text-white'} px-6\"\n\t\t\t\tonclick={() => {\n\t\t\t\t\taction();\n\t\t\t\t\tremoveDialog(id);\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{text}\n\t\t\t</button>\n\t\t{/each}\n\t</div>\n</div>\n"
  },
  {
    "path": "src/lib/components/functional/Dropdown.svelte",
    "content": "<script lang=\"ts\">\n\timport { duration, fade, transition } from \"$lib/util/animation\";\n\timport { ChevronDown } from \"lucide-svelte\";\n\timport { onMount } from \"svelte\";\n\timport { quintOut } from \"svelte/easing\";\n\n\ttype Props = {\n\t\toptions: string[];\n\t\tselected?: string;\n\t\tonselect?: (option: string) => void;\n\t\tdisabled?: boolean;\n\t\tsettingsStyle?: boolean;\n\t};\n\n\tlet {\n\t\toptions,\n\t\tselected = $bindable(options[0]),\n\t\tonselect,\n\t\tdisabled,\n\t\tsettingsStyle,\n\t}: Props = $props();\n\n\tlet open = $state(false);\n\tlet hover = $state(false);\n\tlet isUp = $state(false);\n\tlet dropdown = $state<HTMLDivElement>();\n\n\tconst toggle = () => {\n\t\topen = !open;\n\t};\n\n\tconst select = (option: string) => {\n\t\tconst oldIndex = options.indexOf(selected || \"\");\n\t\tconst newIndex = options.indexOf(option);\n\t\tisUp = oldIndex > newIndex;\n\t\tselected = option;\n\t\tonselect?.(option);\n\t\ttoggle();\n\t};\n\n\tonMount(() => {\n\t\tconst click = (e: MouseEvent) => {\n\t\t\tif (dropdown && !dropdown.contains(e.target as Node)) {\n\t\t\t\topen = false;\n\t\t\t}\n\t\t};\n\n\t\twindow.addEventListener(\"click\", click);\n\t\treturn () => window.removeEventListener(\"click\", click);\n\t});\n</script>\n\n<div\n\tclass=\"relative w-full min-w-fit {settingsStyle\n\t\t? 'font-normal'\n\t\t: 'text-xl font-medium'} text-center\"\n\tbind:this={dropdown}\n>\n\t<button\n\t\tclass=\"font-display w-full {settingsStyle\n\t\t\t? 'justify-between'\n\t\t\t: 'justify-center'} overflow-hidden relative cursor-pointer {settingsStyle\n\t\t\t? 'px-4'\n\t\t\t: 'px-3'} py-3.5 bg-button {disabled\n\t\t\t? 'opacity-50 cursor-auto'\n\t\t\t: 'cursor-pointer'} flex items-center {settingsStyle\n\t\t\t? 'rounded-xl'\n\t\t\t: 'rounded-full'} focus:!outline-none\"\n\t\tonclick={toggle}\n\t\tonmouseenter={() => (hover = true)}\n\t\tonmouseleave={() => (hover = false)}\n\t\t{disabled}\n\t>\n\t\t<!-- <p>{selected}</p> -->\n\t\t<div class=\"grid grid-cols-1 grid-rows-1 w-fit flex-grow-0\">\n\t\t\t{#key selected}\n\t\t\t\t<p\n\t\t\t\t\tin:fade={{\n\t\t\t\t\t\tduration,\n\t\t\t\t\t\teasing: quintOut,\n\t\t\t\t\t}}\n\t\t\t\t\tout:fade={{\n\t\t\t\t\t\tduration,\n\t\t\t\t\t\teasing: quintOut,\n\t\t\t\t\t}}\n\t\t\t\t\tclass=\"col-start-1 row-start-1 {settingsStyle\n\t\t\t\t\t\t? 'text-left'\n\t\t\t\t\t\t: 'text-center'} font-body {settingsStyle\n\t\t\t\t\t\t? 'font-normal'\n\t\t\t\t\t\t: 'font-medium'}\"\n\t\t\t\t>\n\t\t\t\t\t{selected}\n\t\t\t\t</p>\n\t\t\t{/key}\n\t\t\t{#each options as option}\n\t\t\t\t<p\n\t\t\t\t\tclass=\"col-start-1 row-start-1 invisible pointer-events-none\"\n\t\t\t\t>\n\t\t\t\t\t{option}\n\t\t\t\t</p>\n\t\t\t{/each}\n\t\t</div>\n\t\t<ChevronDown\n\t\t\tclass=\"w-4 h-4 ml-3 mt-0.5 flex-shrink-0\"\n\t\t\tstyle=\"transform: rotate({open\n\t\t\t\t? 180\n\t\t\t\t: 0}deg); transition: transform {duration}ms {transition};\"\n\t\t/>\n\t</button>\n\t{#if open}\n\t\t<div\n\t\t\tstyle={hover ? \"will-change: opacity, fade, transform\" : \"\"}\n\t\t\ttransition:fade={{\n\t\t\t\tduration,\n\t\t\t\teasing: quintOut,\n\t\t\t}}\n\t\t\tclass=\"w-full shadow-xl bg-panel-alt shadow-black/25 absolute overflow-hidden top-full mt-1 left-0 z-50 bg-background rounded-xl max-h-[30vh] overflow-y-auto\"\n\t\t>\n\t\t\t{#each options as option}\n\t\t\t\t<button\n\t\t\t\t\tclass=\"w-full p-2 px-4 text-left hover:bg-panel\"\n\t\t\t\t\tonclick={() => select(option)}\n\t\t\t\t>\n\t\t\t\t\t{option}\n\t\t\t\t</button>\n\t\t\t{/each}\n\t\t</div>\n\t{/if}\n</div>\n"
  },
  {
    "path": "src/lib/components/functional/FancyInput.svelte",
    "content": "<script lang=\"ts\">\n\ttype Props = {\n\t\tclass?: string;\n\t\tplaceholder?: string;\n\t\tvalue?: string;\n\t\tdisabled?: boolean;\n\t\textension?: string;\n\t\tprefix?: string;\n\t\ttype?: string;\n\t\tmin?: number;\n\t\tmax?: number;\n\t};\n\n\tlet {\n\t\tclass: className,\n\t\tplaceholder = \"\",\n\t\tvalue = $bindable(),\n\t\tdisabled = false,\n\t\textension,\n\t\tprefix,\n\t\ttype = \"text\",\n\t\tmin = 0,\n\t\tmax = 100,\n\t}: Props = $props();\n</script>\n\n<div class=\"relative flex w-full {className}\">\n\t<input\n\t\t{type}\n\t\t{min}\n\t\t{max}\n\t\tbind:value\n\t\t{placeholder}\n\t\t{disabled}\n\t\tclass=\"w-full p-3 rounded-lg bg-panel border-2 border-button\n            {prefix ? 'pl-[2rem]' : 'pl-3'} \n            {extension ? 'pr-[4rem]' : 'pr-3'}\n\t\t\t{disabled && 'opacity-50 cursor-not-allowed'}\"\n\t/>\n\t{#if prefix}\n\t\t<div class=\"absolute left-0 top-0 bottom-0 flex items-center px-2\">\n\t\t\t<span class=\"text-sm text-gray-400 px-2 py-1 rounded\">{prefix}</span\n\t\t\t>\n\t\t</div>\n\t{/if}\n\t{#if extension}\n\t\t<div class=\"absolute right-0 top-0 bottom-0 flex items-center px-4\">\n\t\t\t<span\n\t\t\t\tclass=\"text-sm bg-button text-black dynadark:text-white px-2 py-1 rounded\"\n\t\t\t\t>{extension}</span\n\t\t\t>\n\t\t</div>\n\t{/if}\n</div>\n"
  },
  {
    "path": "src/lib/components/functional/FancyMenu.svelte",
    "content": "<script lang=\"ts\">\n\timport { browser } from \"$app/environment\";\n\timport { page } from \"$app/stores\";\n\timport { duration, fly } from \"$lib/util/animation\";\n\timport clsx from \"clsx\";\n\timport { onMount, tick } from \"svelte\";\n\timport { quintOut } from \"svelte/easing\";\n\timport type { Writable } from \"svelte/store\";\n\n\tinterface Props {\n\t\tlinks: {\n\t\t\tname: string;\n\t\t\turl: string;\n\t\t\tactiveMatch: (pathname: string) => boolean;\n\t\t}[];\n\t\tshouldGoBack: Writable<boolean> | null;\n\t}\n\n\tlet { links, shouldGoBack = null }: Props = $props();\n\n\tlet hasLoaded = $state(false);\n\n\tlet navWidth = $state(1);\n\tlet linkCount = $derived(links.length);\n\tlet activeLinkIndex = $derived(\n\t\tlinks.findIndex((i) => i.activeMatch($page.url.pathname)),\n\t);\n\n\tonMount(async () => {\n\t\tawait tick();\n\t\tsetTimeout(() => {\n\t\t\thasLoaded = true;\n\t\t}, 16);\n\t});\n</script>\n\n<div\n\tbind:clientWidth={navWidth}\n\tclass=\"w-full flex bg-background relative h-16 items-center\"\n>\n\t{#if activeLinkIndex !== -1}\n\t\t<div\n\t\t\tclass=\"absolute pointer-events-none top-1 bg-foreground h-[calc(100%-8px)] rounded-xl\"\n\t\t\tstyle=\"width: {navWidth / linkCount - 8}px; left: {(navWidth /\n\t\t\t\tlinkCount) *\n\t\t\t\tactiveLinkIndex +\n\t\t\t\t4}px; transition: {hasLoaded ? duration - 200 : 0}ms ease left;\"\n\t\t></div>\n\t{/if}\n\t{#each links as { name, url } (url)}\n\t\t<a\n\t\t\tclass={clsx(\n\t\t\t\t\"w-1/2 px-2 ml-1 h-[calc(100%-8px)] mr-1 flex items-center justify-center rounded-xl relative overflow-hidden font-medium\",\n\t\t\t\t{\n\t\t\t\t\t\"bg-foreground\": $page.url.pathname === url && !browser,\n\t\t\t\t},\n\t\t\t)}\n\t\t\thref={url}\n\t\t\tonclick={() => {\n\t\t\t\tif (shouldGoBack) {\n\t\t\t\t\tconst currentIndex = links.findIndex((i) =>\n\t\t\t\t\t\ti.activeMatch($page.url.pathname),\n\t\t\t\t\t);\n\t\t\t\t\tconst nextIndex = links.findIndex((i) =>\n\t\t\t\t\t\ti.activeMatch(url),\n\t\t\t\t\t);\n\t\t\t\t\t$shouldGoBack = nextIndex < currentIndex;\n\t\t\t\t}\n\t\t\t}}\n\t\t>\n\t\t\t<div class=\"grid grid-cols-1 grid-rows-1\">\n\t\t\t\t{#key name}\n\t\t\t\t\t<span\n\t\t\t\t\t\tclass=\"mix-blend-difference invert dynadark:invert-0 col-start-1 row-start-1 text-center\"\n\t\t\t\t\t\tin:fly={{\n\t\t\t\t\t\t\tduration,\n\t\t\t\t\t\t\teasing: quintOut,\n\t\t\t\t\t\t\ty: -50,\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tout:fly={{\n\t\t\t\t\t\t\tduration,\n\t\t\t\t\t\t\teasing: quintOut,\n\t\t\t\t\t\t\ty: 50,\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{name}\n\t\t\t\t\t</span>\n\t\t\t\t{/key}\n\t\t\t</div>\n\t\t</a>\n\t{/each}\n</div>\n"
  },
  {
    "path": "src/lib/components/functional/FormatDropdown.svelte",
    "content": "<script lang=\"ts\">\n\timport { duration, fade, transition } from \"$lib/util/animation\";\n\timport { m } from \"$lib/paraglide/messages\";\n\timport { isMobile, files, dropdownStates } from \"$lib/store/index.svelte\";\n\timport type { Categories } from \"$lib/types\";\n\timport clsx from \"clsx\";\n\timport { ChevronDown, SearchIcon } from \"lucide-svelte\";\n\timport { onMount } from \"svelte\";\n\timport { quintOut } from \"svelte/easing\";\n\timport { VertFile } from \"$lib/types\";\n\n\ttype Props = {\n\t\tcategories: Categories;\n\t\tfrom?: string;\n\t\tselected?: string;\n\t\tonselect?: (option: string) => void;\n\t\tdisabled?: boolean;\n\t\tdropdownSize?: \"default\" | \"large\" | \"small\";\n\t\tfile?: VertFile;\n\t};\n\n\tlet {\n\t\tcategories,\n\t\tfrom,\n\t\tselected = $bindable(\"\"),\n\t\tonselect,\n\t\tdisabled,\n\t\tdropdownSize = \"default\",\n\t\tfile,\n\t}: Props = $props();\n\tlet open = $state(false);\n\tlet dropdown = $state<HTMLDivElement>();\n\tlet currentCategory = $state<string | null>();\n\tlet searchQuery = $state(\"\");\n\tlet dropdownMenu: HTMLElement | undefined = $state();\n\tlet rootCategory: string | null = null;\n\tlet dropdownPosition = $state<\"left\" | \"center\" | \"right\">(\"center\");\n\n\t// initialize current category\n\t$effect(() => {\n\t\tif (currentCategory) return;\n\n\t\t// find the category whose formats overlap most with the converters for this file (or all files)\n\t\t// this finds the best matching category based on the formats supported by the converters\n\t\tconst pickCategoryFromConverters = (\n\t\t\tconvList: VertFile[\"converters\"],\n\t\t) => {\n\t\t\tlet bestCategory: string | null = null;\n\t\t\tlet maxOverlap = 0;\n\n\t\t\tfor (const cat of Object.keys(categories)) {\n\t\t\t\tconst overlapCount = categories[cat].formats.filter((fmt) =>\n\t\t\t\t\tconvList.some((conv) => conv.formatStrings().includes(fmt)),\n\t\t\t\t).length;\n\n\t\t\t\tif (overlapCount > maxOverlap) {\n\t\t\t\t\tmaxOverlap = overlapCount;\n\t\t\t\t\tbestCategory = cat;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn bestCategory;\n\t\t};\n\n\t\t// decide which converters to use to detect category:\n\t\t// - if file provided, prefer its primary converter -- individual file dropdown\n\t\t// - if no file provided, use all converters from all files -- \"set all to\" dropdown\n\t\tconst convertersToCheck = file\n\t\t\t? file.findConverter()\n\t\t\t\t? [file.findConverter()!]\n\t\t\t\t: file.converters\n\t\t\t: files.files.flatMap((f) => f.converters);\n\n\t\t// pick the best matching category, or fall back to first category\n\t\t// TODO: if something fails for some reason, maybe show all categories?\n\t\tconst detectedCategory =\n\t\t\tpickCategoryFromConverters(convertersToCheck) ||\n\t\t\tObject.keys(categories)[0];\n\n\t\tcurrentCategory = detectedCategory;\n\t\trootCategory = detectedCategory;\n\t});\n\n\t// other available categories based on current category (e.g. converting between video and audio)\n\tconst availableCategories = $derived.by(() => {\n\t\tif (!rootCategory) return Object.keys(categories);\n\n\t\tlet finalCategories = Object.keys(categories).filter(\n\t\t\t(cat) =>\n\t\t\t\tcat === rootCategory ||\n\t\t\t\tcategories[rootCategory!]?.canConvertTo?.includes(cat),\n\t\t);\n\t\tif (from === \".gif\") finalCategories.push(\"video\");\n\n\t\t// filter out categories that can't handle large files (due to browser/device limitations)\n\t\tif (file && file.isLarge()) {\n\t\t\t// if file is large video, disable audio conversion\n\t\t\tif (rootCategory === \"video\")\n\t\t\t\tfinalCategories = finalCategories.filter(\n\t\t\t\t\t(cat) => cat !== \"audio\",\n\t\t\t\t);\n\t\t}\n\n\t\treturn finalCategories;\n\t});\n\n\tconst shouldInclude = (format: string, category: string): boolean => {\n\t\t// if converting from audio to video, dont show gifs\n\t\tif (\n\t\t\tcategories[\"audio\"]?.formats.includes(from ?? \"\") &&\n\t\t\tformat === \".gif\"\n\t\t) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t};\n\n\tconst filteredData = $derived.by(() => {\n\t\tconst normalize = (str: string) => str.replace(/^\\./, \"\").toLowerCase();\n\n\t\t// if no query, return formats for current category\n\t\tif (!searchQuery) {\n\t\t\tlet formats = currentCategory\n\t\t\t\t? categories[currentCategory].formats.filter((format) =>\n\t\t\t\t\t\tshouldInclude(format, currentCategory!),\n\t\t\t\t\t)\n\t\t\t\t: [];\n\n\t\t\treturn {\n\t\t\t\tcategories: availableCategories,\n\t\t\t\tformats,\n\t\t\t};\n\t\t}\n\t\tconst searchLower = normalize(searchQuery);\n\n\t\t// find all categories that have formats matching the search query\n\t\tconst matchingCategories = availableCategories.filter((cat) =>\n\t\t\tcategories[cat].formats.some(\n\t\t\t\t(format) =>\n\t\t\t\t\tnormalize(format).includes(searchLower) &&\n\t\t\t\t\tshouldInclude(format, cat),\n\t\t\t),\n\t\t);\n\t\tif (matchingCategories.length === 0) {\n\t\t\treturn {\n\t\t\t\tcategories: availableCategories,\n\t\t\t\tformats: [],\n\t\t\t};\n\t\t}\n\n\t\t// if current category has no matches, switch to first category that does\n\t\tconst currentCategoryHasMatches =\n\t\t\tcurrentCategory &&\n\t\t\tmatchingCategories.some((cat) => cat === currentCategory);\n\t\tif (!currentCategoryHasMatches && matchingCategories.length > 0) {\n\t\t\tconst newCategory = matchingCategories[0];\n\t\t\tcurrentCategory = newCategory;\n\t\t}\n\n\t\t// return formats only from the current category that match the search\n\t\tlet filteredFormats = currentCategory\n\t\t\t? categories[currentCategory].formats.filter(\n\t\t\t\t\t(format) =>\n\t\t\t\t\t\tnormalize(format).includes(searchLower) &&\n\t\t\t\t\t\tshouldInclude(format, currentCategory!),\n\t\t\t\t)\n\t\t\t: [];\n\n\t\t// sorting exact match first, then others\n\t\tfilteredFormats = filteredFormats.sort((a, b) => {\n\t\t\tconst aExact = normalize(a) === searchLower;\n\t\t\tconst bExact = normalize(b) === searchLower;\n\t\t\tif (aExact && !bExact) return -1;\n\t\t\tif (!aExact && bExact) return 1;\n\t\t\treturn 0;\n\t\t});\n\n\t\treturn {\n\t\t\tcategories:\n\t\t\t\tmatchingCategories.length > 0\n\t\t\t\t\t? matchingCategories\n\t\t\t\t\t: availableCategories,\n\t\t\tformats: filteredFormats,\n\t\t};\n\t});\n\n\tconst selectOption = (option: string) => {\n\t\tselected = option;\n\t\topen = false;\n\n\t\t// save user's selection to dropdownStates for this session\n\t\tif (file) {\n\t\t\tdropdownStates.update((states) => {\n\t\t\t\tconst updated = { ...states, [file.name]: option };\n\t\t\t\treturn updated;\n\t\t\t});\n\t\t}\n\n\t\t// find the category of this option if it's not in the current category\n\t\tif (\n\t\t\tcurrentCategory &&\n\t\t\t!categories[currentCategory].formats.includes(option)\n\t\t) {\n\t\t\tconst formatCategory = Object.keys(categories).find((cat) =>\n\t\t\t\tcategories[cat].formats.includes(option),\n\t\t\t);\n\n\t\t\tif (formatCategory) {\n\t\t\t\tcurrentCategory = formatCategory;\n\t\t\t}\n\t\t}\n\n\t\tonselect?.(option);\n\t};\n\n\tconst selectCategory = (category: string) => {\n\t\tif (!categories[category]) return;\n\t\tcurrentCategory = category;\n\t};\n\n\tconst handleSearch = (event: Event) => {\n\t\tconst query = (event.target as HTMLInputElement).value;\n\t\tsearchQuery = query;\n\n\t\t// find which categories have matching formats & switch\n\t\tif (query) {\n\t\t\tconst queryLower = query.toLowerCase();\n\t\t\tconst categoriesWithMatches = availableCategories.filter((cat) =>\n\t\t\t\tcategories[cat].formats.some((format) =>\n\t\t\t\t\tformat.toLowerCase().includes(queryLower),\n\t\t\t\t),\n\t\t\t);\n\n\t\t\tif (categoriesWithMatches.length > 0) {\n\t\t\t\tconst currentHasMatches =\n\t\t\t\t\tcurrentCategory &&\n\t\t\t\t\tcategories[currentCategory].formats.some((format) =>\n\t\t\t\t\t\tformat.toLowerCase().includes(queryLower),\n\t\t\t\t\t);\n\n\t\t\t\tif (!currentHasMatches) {\n\t\t\t\t\tcurrentCategory = categoriesWithMatches[0];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\tconst onEnter = (event: KeyboardEvent) => {\n\t\tif (event.key === \"Enter\") {\n\t\t\tevent.preventDefault();\n\t\t\tif (filteredData.formats.length > 0) {\n\t\t\t\tselectOption(filteredData.formats[0]);\n\t\t\t}\n\t\t}\n\t};\n\n\tconst clickDropdown = () => {\n\t\topen = !open;\n\t\tif (!open) return;\n\n\t\t// keep within viewport\n\t\tif (dropdown) {\n\t\t\tconst rect = dropdown.getBoundingClientRect();\n\t\t\tconst viewportWidth = window.innerWidth;\n\n\t\t\tlet dropdownWidth: number;\n\t\t\tif (dropdownSize === \"large\") {\n\t\t\t\tdropdownWidth = rect.width * 3.2;\n\t\t\t} else if (dropdownSize === \"default\") {\n\t\t\t\tdropdownWidth = rect.width * 2.5;\n\t\t\t} else {\n\t\t\t\tdropdownWidth = rect.width * 1.5;\n\t\t\t}\n\n\t\t\tconst centerX = rect.left + rect.width / 2;\n\t\t\tconst leftEdge = centerX - dropdownWidth / 2;\n\t\t\tconst rightEdge = centerX + dropdownWidth / 2;\n\n\t\t\tif (leftEdge < 0) {\n\t\t\t\tdropdownPosition = \"left\";\n\t\t\t} else if (rightEdge > viewportWidth) {\n\t\t\t\tdropdownPosition = \"right\";\n\t\t\t} else {\n\t\t\t\tdropdownPosition = \"center\";\n\t\t\t}\n\t\t}\n\n\t\tsetTimeout(() => {\n\t\t\tif (!dropdownMenu) return;\n\t\t\tconst searchInput = dropdownMenu.querySelector(\n\t\t\t\t\"#format-search\",\n\t\t\t) as HTMLInputElement;\n\t\t\tif (searchInput) {\n\t\t\t\tsearchInput.focus();\n\t\t\t\tsearchInput.select();\n\t\t\t}\n\t\t}, 0); // let dropdown open first\n\t};\n\n\tconst extract = async () => {\n\t\t// extract all files in zip, then add all extracted files to files store\n\t\tif (!file) return;\n\t\tconst { extractZip } = await import(\"$lib/util/zip\");\n\t\tconst extractedFiles = await extractZip(file.file);\n\n\t\tif (!Array.isArray(extractedFiles) || extractedFiles.length === 0)\n\t\t\treturn;\n\n\t\tconst newFiles = extractedFiles\n\t\t\t.map(({ filename, data }) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst f = new File([new Uint8Array(data)], filename, {\n\t\t\t\t\t\ttype: \"application/octet-stream\",\n\t\t\t\t\t});\n\t\t\t\t\tconst ext = filename.split(\".\").pop() ?? \"\";\n\t\t\t\t\treturn new VertFile(f, ext);\n\t\t\t\t} catch (err) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t})\n\t\t\t.filter(Boolean);\n\n\t\tfiles.files = files.files.filter((f) => f !== file);\n\t\tnewFiles.forEach((f) => files.add(f));\n\t};\n\n\tonMount(() => {\n\t\tconst handleClickOutside = (e: MouseEvent) => {\n\t\t\tif (dropdown && !dropdown.contains(e.target as Node)) {\n\t\t\t\topen = false;\n\t\t\t}\n\t\t};\n\n\t\tconst handleResize = () => {\n\t\t\tif (open) {\n\t\t\t\t// recalculate dropdown position on resize\n\t\t\t\tclickDropdown();\n\t\t\t\topen = true;\n\t\t\t}\n\t\t};\n\n\t\twindow.addEventListener(\"click\", handleClickOutside);\n\t\twindow.addEventListener(\"resize\", handleResize);\n\t\treturn () => {\n\t\t\twindow.removeEventListener(\"click\", handleClickOutside);\n\t\t\twindow.removeEventListener(\"resize\", handleResize);\n\t\t};\n\t});\n</script>\n\n<div\n\tclass=\"relative w-full min-w-fit text-xl font-medium text-center\"\n\tbind:this={dropdown}\n>\n\t<button\n\t\tclass=\"relative flex items-center justify-center w-full font-display px-3 py-3.5 bg-button rounded-full overflow-hidden cursor-pointer focus:!outline-none\n\t\t{disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}\"\n\t\tonclick={() => clickDropdown()}\n\t\t{disabled}\n\t>\n\t\t<!-- <p>{selected}</p> -->\n\t\t<div\n\t\t\tclass=\"grid grid-cols-1 grid-rows-1 w-fit flex-grow-0 max-h-[2.5rem] overflow-hidden\"\n\t\t>\n\t\t\t{#key selected}\n\t\t\t\t<p\n\t\t\t\t\tin:fade={{\n\t\t\t\t\t\tduration,\n\t\t\t\t\t\teasing: quintOut,\n\t\t\t\t\t}}\n\t\t\t\t\tout:fade={{\n\t\t\t\t\t\tduration,\n\t\t\t\t\t\teasing: quintOut,\n\t\t\t\t\t}}\n\t\t\t\t\tclass=\"col-start-1 row-start-1 text-center font-body font-medium truncate max-w-[4rem]\"\n\t\t\t\t>\n\t\t\t\t\t{selected || \"N/A\"}\n\t\t\t\t</p>\n\t\t\t{/key}\n\t\t\t{#if currentCategory}\n\t\t\t\t{#each categories[currentCategory].formats as option}\n\t\t\t\t\t<p\n\t\t\t\t\t\tclass=\"col-start-1 row-start-1 invisible pointer-events-none truncate max-w-[2.5rem]\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{option}\n\t\t\t\t\t</p>\n\t\t\t\t{/each}\n\t\t\t{/if}\n\t\t</div>\n\t\t<ChevronDown\n\t\t\tclass=\"w-4 h-4 ml-3 mt-0.5 flex-shrink-0\"\n\t\t\tstyle=\"transform: rotate({open\n\t\t\t\t? 180\n\t\t\t\t: 0}deg); transition: transform {duration}ms {transition};\"\n\t\t/>\n\t</button>\n\t{#if open}\n\t\t<div\n\t\t\tbind:this={dropdownMenu}\n\t\t\ttransition:fade={{\n\t\t\t\tduration,\n\t\t\t\teasing: quintOut,\n\t\t\t}}\n\t\t\tclass={clsx(\n\t\t\t\t$isMobile\n\t\t\t\t\t? \"fixed inset-x-0 bottom-0 w-full z-[200] shadow-xl bg-panel-alt shadow-black/25 rounded-t-2xl overflow-hidden\"\n\t\t\t\t\t: \"min-w-full shadow-xl bg-panel-alt shadow-black/25 absolute top-full mt-2 z-50 rounded-2xl overflow-hidden\",\n\t\t\t\t!$isMobile && {\n\t\t\t\t\t\"w-[320%]\": dropdownSize === \"large\",\n\t\t\t\t\t\"w-[250%]\": dropdownSize === \"default\",\n\t\t\t\t\t\"w-[150%]\": dropdownSize === \"small\",\n\t\t\t\t},\n\t\t\t\t!$isMobile && {\n\t\t\t\t\t\"-translate-x-1/2 left-1/2\": dropdownPosition === \"center\",\n\t\t\t\t\t\"left-0\": dropdownPosition === \"left\",\n\t\t\t\t\t\"right-0\": dropdownPosition === \"right\",\n\t\t\t\t},\n\t\t\t)}\n\t\t>\n\t\t\t<!-- search box -->\n\t\t\t<div class=\"p-3 w-full\">\n\t\t\t\t<div class=\"relative\">\n\t\t\t\t\t<input\n\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\tplaceholder={m[\"convert.dropdown.placeholder\"]()}\n\t\t\t\t\t\tclass=\"flex-grow w-full !pl-11 !pr-3 rounded-lg bg-panel text-foreground\"\n\t\t\t\t\t\tbind:value={searchQuery}\n\t\t\t\t\t\toninput={handleSearch}\n\t\t\t\t\t\tonkeydown={onEnter}\n\t\t\t\t\t\tonfocus={() => {}}\n\t\t\t\t\t\tid=\"format-search\"\n\t\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\t/>\n\t\t\t\t\t<span\n\t\t\t\t\t\tclass=\"absolute left-4 top-1/2 -translate-y-1/2 flex items-center\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<SearchIcon class=\"w-4 h-4\" />\n\t\t\t\t\t</span>\n\t\t\t\t\t{#if searchQuery}\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclass=\"absolute right-2 top-1/2 -translate-y-1/2 text-xs text-muted\"\n\t\t\t\t\t\t\tstyle=\"font-size: 0.7rem;\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{filteredData.formats.length}\n\t\t\t\t\t\t\t{filteredData.formats.length === 1\n\t\t\t\t\t\t\t\t? \"result\"\n\t\t\t\t\t\t\t\t: \"results\"}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t{/if}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<!-- available categories -->\n\t\t\t<div class=\"flex items-center justify-between\">\n\t\t\t\t{#each filteredData.categories as category}\n\t\t\t\t\t<button\n\t\t\t\t\t\tclass=\"flex-grow text-lg hover:text-muted/20 border-b-[1px] pb-2 capitalize\n                        {currentCategory === category\n\t\t\t\t\t\t\t? 'text-accent border-b-accent'\n\t\t\t\t\t\t\t: 'border-b-separator text-muted'}\"\n\t\t\t\t\t\tonclick={() => selectCategory(category)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{(m as any)[`convert.dropdown.${category}`]?.()}\n\t\t\t\t\t</button>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t\t<!-- available formats -->\n\t\t\t<div class=\"max-h-80 overflow-y-auto grid grid-cols-3 gap-2 p-2\">\n\t\t\t\t{#if filteredData.formats.length > 0}\n\t\t\t\t\t{#each filteredData.formats as format}\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tclass=\"w-full p-2 text-center rounded-xl\n\t\t\t\t\t\t\t{format === selected\n\t\t\t\t\t\t\t\t? 'bg-accent text-black'\n\t\t\t\t\t\t\t\t: format === from\n\t\t\t\t\t\t\t\t\t? 'bg-separator'\n\t\t\t\t\t\t\t\t\t: 'hover:bg-panel'}\"\n\t\t\t\t\t\t\tonclick={() => selectOption(format)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{format}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t{/each}\n\t\t\t\t{:else}\n\t\t\t\t\t<div class=\"col-span-3 text-center p-4 text-muted\">\n\t\t\t\t\t\t{searchQuery\n\t\t\t\t\t\t\t? m[\"convert.dropdown.no_results\"]()\n\t\t\t\t\t\t\t: m[\"convert.dropdown.no_formats\"]()}\n\t\t\t\t\t</div>\n\t\t\t\t{/if}\n\t\t\t</div>\n\t\t\t<!-- format options -->\n\t\t\t<!-- TODO: extract zip, image sequence & fps -->\n\t\t\t{#if file?.name.toLowerCase().endsWith(\".zip\")}\n\t\t\t\t<div class=\"border-t border-separator text-base p-2\">\n\t\t\t\t\t<button\n\t\t\t\t\t\tclass=\"w-full p-2 text-center rounded-lg bg-accent text-black\"\n\t\t\t\t\t\tonclick={() => extract()}\n\t\t\t\t\t>\n\t\t\t\t\t\t{m[\"convert.archive_file.extract\"]()}\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t{/if}\n\t\t</div>\n\t{/if}\n</div>\n"
  },
  {
    "path": "src/lib/components/functional/Uploader.svelte",
    "content": "<script lang=\"ts\">\n\timport { UploadIcon } from \"lucide-svelte\";\n\timport Panel from \"../visual/Panel.svelte\";\n\timport clsx from \"clsx\";\n\timport { onMount } from \"svelte\";\n\timport { effects, files } from \"$lib/store/index.svelte\";\n\timport { converters } from \"$lib/converters\";\n\timport { goto } from \"$app/navigation\";\n\timport { page } from \"$app/state\";\n\timport { m } from \"$lib/paraglide/messages\";\n\n\ttype Props = {\n\t\tclass?: string;\n\t};\n\n\tconst { class: classList }: Props = $props();\n\n\tlet uploaderButton = $state<HTMLButtonElement>();\n\tlet fileInput = $state<HTMLInputElement>();\n\n\tconst uploadFiles = async () => {\n\t\tif (!fileInput) return;\n\t\tfileInput.click();\n\t};\n\n\tconst handleFileChange = (e: Event) => {\n\t\tif (!fileInput) return;\n\t\tconst oldLength = files.files.length;\n\t\tfiles.add(fileInput.files);\n\t\tif (oldLength !== files.files.length) goto(\"/convert\");\n\t};\n\n\tonMount(() => {\n\t\tconst handler = (e: Event) => {\n\t\t\te.preventDefault();\n\t\t\treturn false;\n\t\t};\n\n\t\tuploaderButton?.addEventListener(\"dragover\", handler);\n\t\tuploaderButton?.addEventListener(\"dragenter\", handler);\n\t\tuploaderButton?.addEventListener(\"dragleave\", handler);\n\t\tuploaderButton?.addEventListener(\"drop\", handler);\n\n\t\treturn () => {\n\t\t\tuploaderButton?.removeEventListener(\"dragover\", handler);\n\t\t\tuploaderButton?.removeEventListener(\"dragenter\", handler);\n\t\t\tuploaderButton?.removeEventListener(\"dragleave\", handler);\n\t\t\tuploaderButton?.removeEventListener(\"drop\", handler);\n\t\t};\n\t});\n</script>\n\n<input\n\tbind:this={fileInput}\n\ttype=\"file\"\n\tmultiple\n\tclass=\"hidden\"\n\tonchange={handleFileChange}\n/>\n\n<button\n\tonclick={uploadFiles}\n\tbind:this={uploaderButton}\n\tclass={clsx(\n\t\t`hover:scale-105 active:scale-100 ${$effects ? \"\" : \"!scale-100\"} duration-200 ${classList}`,\n\t)}\n>\n\t<Panel\n\t\tclass=\"flex justify-center items-center w-full h-full flex-col pointer-events-none\"\n\t>\n\t\t<div\n\t\t\tclass=\"w-16 h-16 bg-accent rounded-full flex items-center justify-center p-4\"\n\t\t>\n\t\t\t<UploadIcon class=\"w-full h-full text-on-accent\" />\n\t\t</div>\n\t\t<h2 class=\"text-center text-2xl font-semibold mt-4\">\n\t\t\t{m[\"upload.uploader.text\"]({\n\t\t\t\taction: m[\"upload.uploader.convert\"]()\n\t\t\t})}\n\t\t</h2>\n\t</Panel>\n</button>\n"
  },
  {
    "path": "src/lib/components/functional/VertdError.svelte",
    "content": "<script lang=\"ts\" module>\n\texport interface VertdErrorProps {\n\t\tjobId: string;\n\t\tauth: string;\n\t\tfrom?: string;\n\t\tto?: string;\n\t\terrorMessage?: string;\n\t\tfileName?: string;\n\t}\n</script>\n\n<script lang=\"ts\">\n\timport { vertdFetch } from \"$lib/converters/vertd.svelte\";\n\n\timport { m } from \"$lib/paraglide/messages\";\n\timport { ToastManager, type ToastProps } from \"$lib/util/toast.svelte\";\n\timport { addDialog } from \"$lib/store/DialogProvider\";\n\timport VertdErrorDetails from \"./VertdErrorDetails.svelte\";\n\n\tconst toast: ToastProps<VertdErrorProps> = $props();\n\n\tlet submitting = $state(false);\n\n\texport const title = \"An error occurred\";\n\n\tconst remove = () => {\n\t\tToastManager.remove(toast.id);\n\t};\n\n\tconst submit = async () => {\n\t\tsubmitting = true;\n\t\ttry {\n\t\t\tawait submitInner();\n\t\t} catch (e) {}\n\t\tsubmitting = false;\n\t};\n\n\tconst submitInner = async () => {\n\t\ttry {\n\t\t\tawait vertdFetch(\n\t\t\t\t\"/api/keep\",\n\t\t\t\t{\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttoken: toast.additional.auth,\n\t\t\t\t\tid: toast.additional.jobId,\n\t\t\t\t},\n\t\t\t);\n\t\t} catch (e) {\n\t\t\tToastManager.add({\n\t\t\t\ttype: \"error\",\n\t\t\t\tmessage: m[\"convert.errors.vertd_failed_to_keep\"]({\n\t\t\t\t\terror: (e as Error).message || e || \"Unknown error\",\n\t\t\t\t}),\n\t\t\t});\n\t\t}\n\n\t\tToastManager.remove(toast.id);\n\t};\n\n\tconst showDetails = () => {\n\t\taddDialog(\n\t\t\tm[\"convert.errors.vertd_details\"](),\n\t\t\tVertdErrorDetails as any,\n\t\t\t[\n\t\t\t\t{\n\t\t\t\t\ttext: \"Close\",\n\t\t\t\t\taction: () => {},\n\t\t\t\t},\n\t\t\t],\n\t\t\t\"info\",\n\t\t\t{\n\t\t\t\tjobId: toast.additional.jobId || \"Unknown\",\n\t\t\t\tfrom: toast.additional.from || \"Unknown\",\n\t\t\t\tto: toast.additional.to || \"Unknown\",\n\t\t\t\terrorMessage: toast.additional.errorMessage || \"Unknown error\",\n\t\t\t},\n\t\t);\n\t};\n</script>\n\n<div class=\"flex flex-col gap-4\">\n\t<p class=\"text-black\">{m[\"convert.errors.vertd_generic_body\"]()}</p>\n\t<div class=\"flex flex-col gap-2\">\n\t\t<button\n\t\t\tonclick={showDetails}\n\t\t\tclass=\"btn rounded-lg h-fit py-2 w-full bg-accent-blue text-black\"\n\t\t\tdisabled={submitting}\n\t\t\t>{m[\"convert.errors.vertd_generic_view\"]()}</button\n\t\t>\n\t\t<div class=\"flex gap-4\">\n\t\t\t<button\n\t\t\t\tonclick={submit}\n\t\t\t\tclass=\"btn rounded-lg h-fit py-2 w-full bg-accent-red-alt text-white\"\n\t\t\t\tdisabled={submitting}\n\t\t\t\t>{m[\"convert.errors.vertd_generic_yes\"]()}</button\n\t\t\t>\n\t\t\t<button\n\t\t\t\tonclick={remove}\n\t\t\t\tclass=\"btn rounded-lg h-fit py-2 w-full\"\n\t\t\t\tdisabled={submitting}\n\t\t\t\t>{m[\"convert.errors.vertd_generic_no\"]()}</button\n\t\t\t>\n\t\t</div>\n\t</div>\n</div>\n"
  },
  {
    "path": "src/lib/components/functional/VertdErrorDetails.svelte",
    "content": "<script lang=\"ts\">\n\timport { m } from \"$lib/paraglide/messages\";\n\timport type { DialogProps } from \"$lib/store/DialogProvider\";\n\timport { link, sanitize } from \"$lib/store/index.svelte\";\n\n\tinterface VertdErrorDetailsProps {\n\t\tjobId: string;\n\t\tfrom: string;\n\t\tto: string;\n\t\terrorMessage: string;\n\t}\n\n\ttype Props = DialogProps<VertdErrorDetailsProps>;\n\n\tlet { additional }: Props = $props();\n</script>\n\n<div class=\"flex flex-col gap-2\">\n\t<p>{@html sanitize(m[\"convert.errors.vertd_details_body\"]())}</p>\n\t<p>\n\t\t<span class=\"text-black dynadark:text-white\">\n\t\t\t{@html sanitize(m[\"convert.errors.vertd_details_job_id\"]({\n\t\t\t\tjobId: additional.jobId,\n\t\t\t}))}\n\t\t</span>\n\t</p>\n\t<p>\n\t\t<span class=\"text-black dynadark:text-white\">\n\t\t\t{@html sanitize(m[\"convert.errors.vertd_details_from\"]({\n\t\t\t\tfrom: additional.from,\n\t\t\t}))}\n\t\t</span>\n\t</p>\n\t<p>\n\t\t<span class=\"text-black dynadark:text-white\">\n\t\t\t{@html sanitize(m[\"convert.errors.vertd_details_to\"]({ to: additional.to }))}\n\t\t</span>\n\t</p>\n\t<p>\n\t\t<span class=\"text-black dynadark:text-white\">\n\t\t\t{@html sanitize(link(\n\t\t\t\t[\"view_link\"],\n\t\t\t\tm[\"convert.errors.vertd_details_error_message\"](),\n\t\t\t\t[\n\t\t\t\t\tURL.createObjectURL(\n\t\t\t\t\t\tnew Blob([additional.errorMessage], {\n\t\t\t\t\t\t\ttype: \"text/plain\",\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t],\n\t\t\t\t[true],\n\t\t\t\t[\"text-blue-500 font-normal\"],\n\t\t\t))}\n\t\t</span>\n\t</p>\n\t<p>\n\t\t{@html sanitize(link(\n\t\t\t[\"privacy_link\"],\n\t\t\tm[\"convert.errors.vertd_details_footer\"](),\n\t\t\t\"/privacy\",\n\t\t\t[true],\n\t\t))}\n\t</p>\n</div>\n"
  },
  {
    "path": "src/lib/components/layout/Dialogs.svelte",
    "content": "<script lang=\"ts\">\n\timport { duration, fade } from \"$lib/util/animation\";\n\timport { quintOut } from \"svelte/easing\";\n\timport Dialog from \"../functional/Dialog.svelte\";\n\timport {\n\t\ttype Dialog as DialogType,\n\t\tdialogs,\n\t} from \"$lib/store/DialogProvider\";\n\n\tlet dialogList = $state<DialogType[]>([]);\n\n\tdialogs.subscribe((value) => {\n\t\tdialogList = value as DialogType[];\n\t});\n</script>\n\n{#if dialogList.length > 0}\n\t<div\n\t\tclass=\"fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 backdrop-blur-sm z-40\"\n\t\tin:fade={{\n\t\t\tduration,\n\t\t\teasing: quintOut,\n\t\t}}\n\t\tout:fade={{\n\t\t\tduration,\n\t\t\teasing: quintOut,\n\t\t}}\n\t>\n\t\t{#each dialogList as dialog, i}\n\t\t\t{#if i === 0}\n\t\t\t\t<Dialog {...dialog} />\n\t\t\t{/if}\n\t\t{/each}\n\t</div>\n{/if}\n"
  },
  {
    "path": "src/lib/components/layout/Footer.svelte",
    "content": "<script lang=\"ts\">\n\timport { GITHUB_URL_VERT, DISCORD_URL } from \"$lib/util/consts\";\n\timport { m } from \"$lib/paraglide/messages\";\n\n\tconst commitHash =\n\t\t__COMMIT_HASH__ && __COMMIT_HASH__ !== \"unknown\"\n\t\t\t? __COMMIT_HASH__\n\t\t\t: null;\n\n\tconst year = new Date().getFullYear();\n\n\t// we can't use svelte snippets or a derived object to render the footer as it causes a full-page reload\n\t// ...for some reason. i have no idea, maybe it's to do with the {#key $locale} in +layout.svelte\n</script>\n\n<footer\n\tclass=\"hidden md:block w-full h-14 border-t border-separator fixed bottom-0 mt-12\"\n>\n\t<div\n\t\tclass=\"w-full h-full flex items-center justify-center text-muted gap-3 relative\"\n\t>\n\t\t<p>{m[\"footer.copyright\"]({ year })}</p>\n\t\t<p>•</p>\n\t\t<a\n\t\t\tclass=\"hover:underline font-normal\"\n\t\t\thref={GITHUB_URL_VERT}\n\t\t\ttarget=\"_blank\"\n\t\t>\n\t\t\t{m[\"footer.source_code\"]()}\n\t\t</a>\n\t\t<p>•</p>\n\t\t<a\n\t\t\tclass=\"hover:underline font-normal\"\n\t\t\thref={DISCORD_URL}\n\t\t\ttarget=\"_blank\"\n\t\t>\n\t\t\t{m[\"footer.discord_server\"]()}\n\t\t</a>\n\t\t<p>•</p>\n\t\t<a\n\t\t\tclass=\"hover:underline font-normal\"\n\t\t\thref=\"/privacy/\"\n\t\t>\n\t\t\t{m[\"footer.privacy_policy\"]()}\n\t\t</a>\n\t\t{#if commitHash}\n\t\t\t<p>•</p>\n\t\t\t<a\n\t\t\t\tclass=\"hover:underline font-normal\"\n\t\t\t\thref=\"{GITHUB_URL_VERT}/commit/{commitHash}\"\n\t\t\t\ttarget=\"_blank\"\n\t\t\t>\n\t\t\t\t{commitHash}\n\t\t\t</a>\n\t\t{/if}\n\t</div>\n\n\t<div\n\t\tclass=\"absolute bottom-0 left-0 w-full h-24 -z-10 pointer-events-none\"\n\t\tstyle=\"background: linear-gradient(to bottom, transparent, var(--bg) 100%)\"\n\t></div>\n</footer>\n"
  },
  {
    "path": "src/lib/components/layout/Gradients.svelte",
    "content": "<script lang=\"ts\">\n\timport { page } from \"$app/state\";\n\timport { duration, transition } from \"$lib/util/animation\";\n\timport VertVBig from \"$lib/assets/vert-bg.svg?component\";\n\timport {\n\t\tfiles,\n\t\tgradientColor,\n\t\tshowGradient,\n\t} from \"$lib/store/index.svelte\";\n\timport { quintOut } from \"svelte/easing\";\n\timport { fade } from \"$lib/util/animation\";\n\timport { Tween } from \"svelte/motion\";\n\n\tconst colors: {\n\t\tmatcher: (path: string) => boolean;\n\t\tcolor: string;\n\t\tat: number;\n\t}[] = $derived([\n\t\t{\n\t\t\tmatcher: (path) => path === \"/\",\n\t\t\tcolor: \"var(--bg-gradient-from)\",\n\t\t\tat: 100,\n\t\t},\n\t\t{\n\t\t\tmatcher: (path) => path === \"/convert/\",\n\t\t\tcolor: `var(--bg-gradient-${$gradientColor ? $gradientColor + \"-\" : \"\"}from)`,\n\t\t\tat: 25,\n\t\t},\n\t\t{\n\t\t\tmatcher: (path) => path === \"/settings/\",\n\t\t\tcolor: \"var(--bg-gradient-blue-from)\",\n\t\t\tat: 25,\n\t\t},\n\t\t{\n\t\t\tmatcher: (path) => path === \"/about/\",\n\t\t\tcolor: \"var(--bg-gradient-from)\",\n\t\t\tat: 25,\n\t\t},\n\t\t{\n\t\t\tmatcher: (path) => path === \"/privacy/\",\n\t\t\tcolor: \"var(--bg-gradient-red-from)\",\n\t\t\tat: 100,\n\t\t},\n\t]);\n\n\tconst color = $derived(\n\t\tObject.values(colors).find((p) => p.matcher(page.url.pathname)) || {\n\t\t\tmatcher: () => false,\n\t\t\tcolor: \"transparent\",\n\t\t\tat: 0,\n\t\t},\n\t);\n\n\t// svelte-ignore state_referenced_locally This is handled in the effect below\n\tlet at = new Tween(color.at, {\n\t\tduration,\n\t\teasing: quintOut,\n\t});\n\n\t$effect(() => {\n\t\tat.set(color.at);\n\t});\n\n\tconst maskImage = $derived(\n\t\t`linear-gradient(to top, transparent ${100 - at.current}%, black 100%)`,\n\t);\n</script>\n\n{#if page.url.pathname === \"/\"}\n\t<div\n\t\tclass=\"fixed -z-30 top-0 left-0 w-screen h-screen flex items-center justify-center overflow-hidden\"\n\t\ttransition:fade={{\n\t\t\tduration,\n\t\t\teasing: quintOut,\n\t\t}}\n\t>\n\t\t<VertVBig\n\t\t\tclass=\"fill-[--fg] opacity-10 dynadark:opacity-5 scale-[200%] md:scale-[80%]\"\n\t\t/>\n\t</div>\n{/if}\n\n<div\n\tclass=\"fixed top-0 left-0 w-screen h-screen -z-40 pointer-events-none\"\n\tstyle=\"background-color: {color.color}; \n\tmask-image: {maskImage}; \n\t-webkit-mask-image: {maskImage};\n\ttransition: background-color {duration}ms {transition};\"\n></div>\n\n{#if page.url.pathname === \"/convert/\" && files.files.length === 1}\n\t{@const bgMask =\n\t\t\"linear-gradient(to top, transparent 5%, rgba(0, 0, 0, 0.5) 100%)\"}\n\t<div\n\t\tclass=\"fixed top-0 left-0 w-screen h-screen -z-50\"\n\t\tstyle=\"background-image: url({files.files[0].blobUrl});\n\t\tbackground-size: cover;\n\t\tbackground-position: center;\n\t\tbackground-repeat: no-repeat;\n\t\tfilter: blur(10px);\n\t\tmask-image: {bgMask};\n\t\t-webkit-mask-image: {bgMask};\"\n\t\ttransition:fade={{ duration, easing: quintOut }}\n\t></div>\n{/if}\n\n<!-- \n\t<div\n\t\tid=\"gradient-bg\"\n\t\tclass=\"fixed top-0 left-0 w-screen h-screen -z-40 pointer-events-none\"\n\t\tstyle=\"background: var(--bg-gradient);\"\n\t\ttransition:fade={{\n\t\t\tduration,\n\t\t\teasing: quintOut,\n\t\t}}\n\t></div>\n{:else if (page.url.pathname === \"/convert/\" || page.url.pathname === \"/jpegify/\") && $showGradient}\n\t{#key $gradientColor}\n\t\t<div\n\t\t\tid=\"gradient-bg\"\n\t\t\tclass=\"fixed top-0 left-0 w-screen h-screen -z-40 pointer-events-none\"\n\t\t\tstyle=\"background: var(--bg-gradient-{$gradientColor || 'pink'});\"\n\t\t\ttransition:fade={{\n\t\t\t\tduration,\n\t\t\t\teasing: quintOut,\n\t\t\t}}\n\t\t></div>\n\t{/key}\n{:else if page.url.pathname === \"/convert/\" && files.files.length === 1 && files.files[0].blobUrl}\n\t<div\n\t\tclass=\"fixed w-screen h-screen opacity-75 overflow-hidden top-0 left-0 -z-50 pointer-events-none grid grid-cols-1 grid-rows-1 scale-105\"\n\t>\n\t\t<div\n\t\t\tclass=\"w-full relative\"\n\t\t\ttransition:fade={{\n\t\t\t\tduration,\n\t\t\t\teasing: quintOut,\n\t\t\t}}\n\t\t>\n\t\t\t<img\n\t\t\t\tclass=\"object-cover w-full h-full blur-md\"\n\t\t\t\tsrc={files.files[0].blobUrl}\n\t\t\t\talt={files.files[0].name}\n\t\t\t/>\n\t\t\t<div\n\t\t\t\tclass=\"absolute top-0 left-0 w-full h-full\"\n\t\t\t\tstyle=\"background: var(--bg-gradient-image);\"\n\t\t\t></div>\n\t\t</div>\n\t</div>\n{:else if page.url.pathname === \"/settings/\"}\n\t<div\n\t\tid=\"gradient-bg\"\n\t\tclass=\"fixed top-0 left-0 w-screen h-screen -z-40 pointer-events-none\"\n\t\tstyle=\"background: var(--bg-gradient-blue);\"\n\t\ttransition:fade={{\n\t\t\tduration,\n\t\t\teasing: quintOut,\n\t\t}}\n\t></div>\n{:else if page.url.pathname === \"/about/\"}\n\t<div\n\t\tid=\"gradient-bg\"\n\t\tclass=\"fixed top-0 left-0 w-screen h-screen -z-40 pointer-events-none\"\n\t\tstyle=\"background: var(--bg-gradient-pink);\"\n\t\ttransition:fade={{\n\t\t\tduration,\n\t\t\teasing: quintOut,\n\t\t}}\n\t></div>\n{/if} -->\n"
  },
  {
    "path": "src/lib/components/layout/MobileLogo.svelte",
    "content": "<script>\n\timport Logo from \"$lib/components/visual/svg/Logo.svelte\";\n</script>\n\n<div class=\"flex md:hidden justify-center items-center pb-8 pt-4\">\n\t<a\n\t\tclass=\"flex items-center justify-center bg-panel p-2 rounded-[20px] shadow-panel\"\n\t\thref=\"/\"\n\t>\n\t\t<div\n\t\t\tclass=\"h-14 bg-accent rounded-[14px] flex items-center justify-center\"\n\t\t>\n\t\t\t<div class=\"w-28 h-5\">\n\t\t\t\t<Logo />\n\t\t\t</div>\n\t\t</div>\n\t</a>\n</div>\n"
  },
  {
    "path": "src/lib/components/layout/Navbar/Base.svelte",
    "content": "<script lang=\"ts\">\r\n\timport { browser } from \"$app/environment\";\r\n\timport { page } from \"$app/state\";\r\n\timport { duration, fade } from \"$lib/util/animation\";\r\n\timport {\r\n\t\teffects,\r\n\t\tfiles,\r\n\t\tgoingLeft,\r\n\t\tsetTheme,\r\n\t} from \"$lib/store/index.svelte\";\r\n\timport clsx from \"clsx\";\r\n\timport {\r\n\t\tInfoIcon,\r\n\t\tMoonIcon,\r\n\t\tRefreshCw,\r\n\t\tSettingsIcon,\r\n\t\tSunIcon,\r\n\t\tUploadIcon,\r\n\t\ttype Icon as IconType,\r\n\t} from \"lucide-svelte\";\r\n\timport { quintOut } from \"svelte/easing\";\r\n\timport Panel from \"../../visual/Panel.svelte\";\r\n\timport Logo from \"../../visual/svg/Logo.svelte\";\r\n\timport { beforeNavigate } from \"$app/navigation\";\r\n\timport Tooltip from \"$lib/components/visual/Tooltip.svelte\";\r\n\timport { m } from \"$lib/paraglide/messages\";\r\n\r\n\tconst items = $derived<\r\n\t\t{\r\n\t\t\tname: string;\r\n\t\t\turl: string;\r\n\t\t\tactiveMatch: (pathname: string) => boolean;\r\n\t\t\ticon: typeof IconType;\r\n\t\t\tbadge?: number;\r\n\t\t}[]\r\n\t>([\r\n\t\t{\r\n\t\t\tname: m[\"navbar.upload\"](),\r\n\t\t\turl: \"/\",\r\n\t\t\tactiveMatch: (pathname) => pathname === \"/\",\r\n\t\t\ticon: UploadIcon,\r\n\t\t},\r\n\t\t{\r\n\t\t\tname: m[\"navbar.convert\"](),\r\n\t\t\turl: \"/convert/\",\r\n\t\t\tactiveMatch: (pathname) =>\r\n\t\t\t\tpathname === \"/convert/\" || pathname === \"/convert\",\r\n\t\t\ticon: RefreshCw,\r\n\t\t\tbadge: files.files.length,\r\n\t\t},\r\n\t\t{\r\n\t\t\tname: m[\"navbar.settings\"](),\r\n\t\t\turl: \"/settings/\",\r\n\t\t\tactiveMatch: (pathname) => pathname.startsWith(\"/settings\"),\r\n\t\t\ticon: SettingsIcon,\r\n\t\t},\r\n\t\t{\r\n\t\t\tname: m[\"navbar.about\"](),\r\n\t\t\turl: \"/about/\",\r\n\t\t\tactiveMatch: (pathname) => pathname.startsWith(\"/about\"),\r\n\t\t\ticon: InfoIcon,\r\n\t\t},\r\n\t]);\r\n\r\n\tlet links = $state<HTMLAnchorElement[]>([]);\r\n\tlet container = $state<HTMLDivElement>();\r\n\tlet containerRect = $derived(container?.getBoundingClientRect());\r\n\tlet isInitialized = $state(false);\r\n\r\n\tconst linkRects = $derived(links.map((l) => l.getBoundingClientRect()));\r\n\r\n\tconst selectedIndex = $derived(\r\n\t\titems.findIndex((i) => i.activeMatch(page.url.pathname)),\r\n\t);\r\n\r\n\tconst isSecretPage = $derived(selectedIndex === -1);\r\n\r\n\t$effect(() => {\r\n\t\tif (containerRect && linkRects.length > 0 && links.length > 0) {\r\n\t\t\tsetTimeout(() => {\r\n\t\t\t\tisInitialized = true;\r\n\t\t\t}, 10);\r\n\t\t} else {\r\n\t\t\tisInitialized = false;\r\n\t\t}\r\n\t});\r\n\r\n\tbeforeNavigate((e) => {\r\n\t\tconst oldIndex = items.findIndex((i) =>\r\n\t\t\ti.activeMatch(e.from?.url.pathname || \"\"),\r\n\t\t);\r\n\t\tconst newIndex = items.findIndex((i) =>\r\n\t\t\ti.activeMatch(e.to?.url.pathname || \"\"),\r\n\t\t);\r\n\t\tif (newIndex < oldIndex) {\r\n\t\t\tgoingLeft.set(true);\r\n\t\t} else {\r\n\t\t\tgoingLeft.set(false);\r\n\t\t}\r\n\t});\r\n</script>\r\n\r\n{#snippet link(item: (typeof items)[0], index: number)}\r\n\t{@const Icon = item.icon}\r\n\t<a\r\n\t\tbind:this={links[index]}\r\n\t\thref={item.url}\r\n\t\taria-label={item.name}\r\n\t\tclass={clsx(\r\n\t\t\t\"min-w-16 md:min-w-32 h-full relative z-10 rounded-xl flex flex-1 items-center justify-center gap-3 overflow-hidden\",\r\n\t\t\t{\r\n\t\t\t\t\"bg-panel-highlight\":\r\n\t\t\t\t\titem.activeMatch(page.url.pathname) && !browser,\r\n\t\t\t},\r\n\t\t)}\r\n\t\tdraggable={false}\r\n\t>\r\n\t\t<div class=\"grid grid-rows-1 grid-cols-1\">\r\n\t\t\t{#key item.name}\r\n\t\t\t\t<div\r\n\t\t\t\t\tclass=\"w-full row-start-1 col-start-1 h-full flex items-center justify-center gap-3\"\r\n\t\t\t\t\tin:fade={{\r\n\t\t\t\t\t\tduration,\r\n\t\t\t\t\t\teasing: quintOut,\r\n\t\t\t\t\t}}\r\n\t\t\t\t\tout:fade={{\r\n\t\t\t\t\t\tduration,\r\n\t\t\t\t\t\teasing: quintOut,\r\n\t\t\t\t\t}}\r\n\t\t\t\t>\r\n\t\t\t\t\t<div class=\"relative\">\r\n\t\t\t\t\t\t<Icon />\r\n\t\t\t\t\t\t{#if item.badge}\r\n\t\t\t\t\t\t\t<div\r\n\t\t\t\t\t\t\t\tclass=\"absolute overflow-hidden grid grid-rows-1 grid-cols-1 -top-1 font-display -right-1 w-fit px-1.5 h-4 rounded-full bg-badge text-on-badge font-medium\"\r\n\t\t\t\t\t\t\t\tstyle=\"font-size: 0.7rem;\"\r\n\t\t\t\t\t\t\t\ttransition:fade={{\r\n\t\t\t\t\t\t\t\t\tduration,\r\n\t\t\t\t\t\t\t\t\teasing: quintOut,\r\n\t\t\t\t\t\t\t\t}}\r\n\t\t\t\t\t\t\t>\r\n\t\t\t\t\t\t\t\t{#key item.badge}\r\n\t\t\t\t\t\t\t\t\t<div\r\n\t\t\t\t\t\t\t\t\t\tclass=\"flex items-center justify-center w-full h-full col-start-1 row-start-1\"\r\n\t\t\t\t\t\t\t\t\t\tin:fade={{\r\n\t\t\t\t\t\t\t\t\t\t\tduration,\r\n\t\t\t\t\t\t\t\t\t\t\teasing: quintOut,\r\n\t\t\t\t\t\t\t\t\t\t}}\r\n\t\t\t\t\t\t\t\t\t\tout:fade={{\r\n\t\t\t\t\t\t\t\t\t\t\tduration,\r\n\t\t\t\t\t\t\t\t\t\t\teasing: quintOut,\r\n\t\t\t\t\t\t\t\t\t\t}}\r\n\t\t\t\t\t\t\t\t\t>\r\n\t\t\t\t\t\t\t\t\t\t{item.badge}\r\n\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t{/key}\r\n\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t{/if}\r\n\t\t\t\t\t</div>\r\n\t\t\t\t\t<p\r\n\t\t\t\t\t\tclass=\"font-medium hidden hyphens-auto break-all md:flex min-w-0\"\r\n\t\t\t\t\t>\r\n\t\t\t\t\t\t{item.name}\r\n\t\t\t\t\t</p>\r\n\t\t\t\t</div>\r\n\t\t\t{/key}\r\n\t\t</div>\r\n\t</a>\r\n{/snippet}\r\n\r\n<div bind:this={container}>\r\n\t<Panel class=\"max-w-[778px] w-screen h-20 flex items-center gap-3 relative\">\r\n\t\t{@const linkRect = linkRects.at(selectedIndex) || linkRects[0]}\r\n\t\t{#if linkRect && isInitialized}\r\n\t\t\t<div\r\n\t\t\t\tclass=\"absolute bg-panel-highlight rounded-xl\"\r\n\t\t\t\tstyle=\"width: {linkRect.width}px; height: {linkRect.height}px; top: {linkRect.top -\r\n\t\t\t\t\t(containerRect?.top || 0)}px; left: {linkRect.left -\r\n\t\t\t\t\t(containerRect?.left || 0)}px; opacity: {isSecretPage\r\n\t\t\t\t\t? 0\r\n\t\t\t\t\t: 1}; {$effects\r\n\t\t\t\t\t? `transition: left var(--transition) ${duration}ms, top var(--transition) ${duration}ms, opacity var(--transition) ${duration}ms;`\r\n\t\t\t\t\t: ''}\"\r\n\t\t\t></div>\r\n\t\t{/if}\r\n\t\t<a\r\n\t\t\tclass=\"w-28 h-full bg-accent rounded-xl items-center justify-center hidden md:flex\"\r\n\t\t\thref=\"/\"\r\n\t\t>\r\n\t\t\t<div class=\"h-5 w-full\">\r\n\t\t\t\t<Logo />\r\n\t\t\t</div>\r\n\t\t</a>\r\n\t\t{#each items as item, i (item.url)}\r\n\t\t\t{@render link(item, i)}\r\n\t\t{/each}\r\n\t\t<div class=\"w-0.5 bg-separator h-full hidden md:flex\"></div>\r\n\t\t<Tooltip text={m[\"navbar.toggle_theme\"]()} position=\"right\">\r\n\t\t\t<button\r\n\t\t\t\tonclick={() => {\r\n\t\t\t\t\tconst isDark =\r\n\t\t\t\t\t\tdocument.documentElement.classList.contains(\"dark\");\r\n\t\t\t\t\tsetTheme(isDark ? \"light\" : \"dark\");\r\n\t\t\t\t}}\r\n\t\t\t\tclass=\"w-14 h-full items-center justify-center hidden md:flex\"\r\n\t\t\t>\r\n\t\t\t\t<SunIcon class=\"dynadark:hidden block\" />\r\n\t\t\t\t<MoonIcon class=\"dynadark:block hidden\" />\r\n\t\t\t</button>\r\n\t\t</Tooltip>\r\n\t</Panel>\r\n</div>\r\n"
  },
  {
    "path": "src/lib/components/layout/Navbar/Desktop.svelte",
    "content": "<script lang=\"ts\">\n\timport Navbar from \"./Base.svelte\";\n</script>\n\n<div class=\"hidden md:flex p-8 w-screen justify-center\">\n\t<Navbar />\n</div>\n"
  },
  {
    "path": "src/lib/components/layout/Navbar/Mobile.svelte",
    "content": "<script lang=\"ts\">\n\timport Navbar from \"./Base.svelte\";\n</script>\n\n<div class=\"fixed md:hidden bottom-0 left-0 w-screen p-8 justify-center z-100\">\n\t<div class=\"flex flex-col justify-center items-center\">\n\t\t<Navbar />\n\t</div>\n</div>\n"
  },
  {
    "path": "src/lib/components/layout/Navbar/index.ts",
    "content": "export { default as Desktop } from \"./Desktop.svelte\";\nexport { default as Mobile } from \"./Mobile.svelte\";\n"
  },
  {
    "path": "src/lib/components/layout/PageContent.svelte",
    "content": "<script lang=\"ts\">\n\timport { page } from \"$app/state\";\n\timport { duration } from \"$lib/util/animation\";\n\timport { goingLeft, isMobile } from \"$lib/store/index.svelte\";\n\timport { quintOut } from \"svelte/easing\";\n\timport { fly, fade } from \"$lib/util/animation\";\n\n\tlet { children } = $props();\n</script>\n\n<div class=\"grid grid-rows-1 grid-cols-1 h-full flex-grow\">\n\t{#key page.url.pathname}\n\t\t<div\n\t\t\tclass=\"row-start-1 col-start-1\"\n\t\t\tin:fly={{\n\t\t\t\tx: $goingLeft ? -window.innerWidth : window.innerWidth,\n\t\t\t\tduration,\n\t\t\t\teasing: quintOut,\n\t\t\t\tdelay: 25,\n\t\t\t}}\n\t\t\tout:fly={{\n\t\t\t\tx: $goingLeft ? window.innerWidth : -window.innerWidth,\n\t\t\t\tduration,\n\t\t\t\teasing: quintOut,\n\t\t\t}}\n\t\t>\n\t\t\t<div\n\t\t\t\tclass=\"flex flex-col h-full pb-32\"\n\t\t\t\tin:fade={{\n\t\t\t\t\tduration,\n\t\t\t\t\teasing: quintOut,\n\t\t\t\t\tdelay: $isMobile ? 0 : 100,\n\t\t\t\t}}\n\t\t\t\tout:fade={{\n\t\t\t\t\tduration,\n\t\t\t\t\teasing: quintOut,\n\t\t\t\t\tdelay: $isMobile ? 0 : 200,\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{@render children()}\n\t\t\t</div>\n\t\t</div>\n\t{/key}\n</div>\n"
  },
  {
    "path": "src/lib/components/layout/Toasts.svelte",
    "content": "<script lang=\"ts\">\n\timport Toast from \"$lib/components/visual/Toast.svelte\";\n\timport { ToastManager } from \"$lib/util/toast.svelte\";\n</script>\n\n<div\n\tclass=\"fixed bottom-28 md:bottom-0 right-0 p-4 flex flex-col-reverse gap-4 z-50\"\n>\n\t{#each ToastManager.toasts as toast (toast.id)}\n\t\t<div class=\"flex justify-end\">\n\t\t\t<Toast {toast} />\n\t\t</div>\n\t{/each}\n</div>\n"
  },
  {
    "path": "src/lib/components/layout/UploadRegion.svelte",
    "content": "<script lang=\"ts\">\n\timport { duration, fade } from \"$lib/util/animation\";\n\timport { dropping, effects } from \"$lib/store/index.svelte\";\n\timport { quintOut } from \"svelte/easing\";\n</script>\n\n{#if $dropping}\n\t<div\n\t\tclass=\"fixed w-screen h-screen opacity-40 dynadark:opacity-20 z-[100] pointer-events-none blur-2xl {$effects\n\t\t\t? 'dragoverlay'\n\t\t\t: 'bg-accent-blue'}\"\n\t\tclass:_dragover={dropping && $effects}\n\t\ttransition:fade={{\n\t\t\tduration,\n\t\t\teasing: quintOut,\n\t\t}}\n\t></div>\n{/if}\n\n<style lang=\"postcss\">\n\t.dragoverlay {\n\t\tanimation: dragoverlay-animation 3s infinite linear;\n\t}\n\n\t@keyframes dragoverlay-animation {\n\t\t0% {\n\t\t\t@apply bg-accent-pink;\n\t\t}\n\n\t\t25% {\n\t\t\t@apply bg-accent-blue;\n\t\t}\n\n\t\t50% {\n\t\t\t@apply bg-accent-purple;\n\t\t}\n\n\t\t75% {\n\t\t\t@apply bg-accent-red;\n\t\t}\n\n\t\t100% {\n\t\t\t@apply bg-accent-pink;\n\t\t}\n\t}\n</style>\n"
  },
  {
    "path": "src/lib/components/layout/index.ts",
    "content": "export { default as UploadRegion } from \"./UploadRegion.svelte\";\nexport { default as Gradients } from \"./Gradients.svelte\";\nexport { default as Toasts } from \"./Toasts.svelte\";\nexport { default as Dialogs } from \"./Dialogs.svelte\";\nexport { default as PageContent } from \"./PageContent.svelte\";\nexport { default as MobileLogo } from \"./MobileLogo.svelte\";\nexport { default as Footer } from \"./Footer.svelte\";\n"
  },
  {
    "path": "src/lib/components/visual/Panel.svelte",
    "content": "<script lang=\"ts\">\n\timport type { Snippet } from \"svelte\";\n\n\ttype Props = {\n\t\tclass?: string;\n\t\tchildren: Snippet<[]>;\n\t};\n\n\tconst { class: classList, children }: Props = $props();\n</script>\n\n<div class=\"bg-panel {classList} p-3 rounded-2.5xl shadow-panel\">\n\t{@render children?.()}\n</div>\n"
  },
  {
    "path": "src/lib/components/visual/ProgressBar.svelte",
    "content": "<script lang=\"ts\">\n\ttype Props = {\n\t\tprogress: number | null;\n\t\tmin: number;\n\t\tmax: number;\n\t};\n\n\tlet { progress, min, max }: Props = $props();\n\n\tconst percent = $derived(\n\t\tprogress ? ((progress - min) / (max - min)) * 100 : null,\n\t);\n</script>\n\n<div class=\"w-full h-1 bg-panel-alt rounded-full overflow-hidden relative\">\n\t<div\n\t\tclass=\"h-full bg-accent absolute left-0 top-0\"\n\t\tclass:percentless-animation={progress === null}\n\t\tstyle={percent\n\t\t\t? `width: ${percent}%; transition: 500ms linear width;`\n\t\t\t: \"\"}\n\t></div>\n</div>\n\n<style>\n\t.percentless-animation {\n\t\twidth: 100%;\n\t\tanimation:\n\t\t\tpercentless-animation 1s ease infinite,\n\t\t\tleft-right 1s ease infinite;\n\t}\n\n\t@keyframes percentless-animation {\n\t\t0% {\n\t\t\twidth: 0%;\n\t\t}\n\n\t\t50% {\n\t\t\twidth: 100%;\n\t\t}\n\n\t\t100% {\n\t\t\twidth: 0%;\n\t\t}\n\t}\n\n\t@keyframes left-right {\n\t\t49% {\n\t\t\tleft: 0;\n\t\t\tright: auto;\n\t\t}\n\n\t\t50% {\n\t\t\tleft: auto;\n\t\t\tright: 0;\n\t\t}\n\n\t\t100% {\n\t\t\tleft: auto;\n\t\t\tright: 0;\n\t\t}\n\t}\n</style>\n"
  },
  {
    "path": "src/lib/components/visual/Toast.svelte",
    "content": "<script lang=\"ts\">\n\timport { fade, fly } from \"$lib/util/animation\";\n\timport {\n\t\tBanIcon,\n\t\tCheckIcon,\n\t\tInfoIcon,\n\t\tTriangleAlert,\n\t\tXIcon,\n\t} from \"lucide-svelte\";\n\timport { quintOut } from \"svelte/easing\";\n\timport { ToastManager } from \"$lib/util/toast.svelte\";\n\timport type { ToastProps } from \"$lib/util/toast.svelte\";\n\timport type { SvelteComponent } from \"svelte\";\n\timport clsx from \"clsx\";\n\timport type { Toast as ToastType } from \"$lib/util/toast.svelte\";\n\n\tconst props: {\n\t\ttoast: ToastType<unknown>;\n\t} = $props();\n\n\tconst { id, type, message, durations } = props.toast;\n\n\tconst additional =\n\t\t\"additional\" in props.toast ? props.toast.additional : {};\n\n\tconst colors = {\n\t\tsuccess: \"purple\",\n\t\terror: \"red\",\n\t\tinfo: \"blue\",\n\t\twarning: \"pink\",\n\t};\n\n\tconst Icons = {\n\t\tsuccess: CheckIcon,\n\t\terror: BanIcon,\n\t\tinfo: InfoIcon,\n\t\twarning: TriangleAlert,\n\t};\n\n\tlet color = $derived(colors[type]);\n\tlet Icon = $derived(Icons[type]);\n\n\tlet msg = $state<SvelteComponent<ToastProps>>();\n\tconst title = $derived(((msg as any)?.title as string) ?? \"\");\n\n\t// intentionally unused. this is so tailwind can generate the css for these colours as it doesn't detect if it's dynamically loaded\n\t// this would lead to the colours not being generated in the final css file by tailwind\n\tconst colourVariants = [\n\t\t\"border-accent-pink-alt\",\n\t\t\"border-accent-red-alt\",\n\t\t\"border-accent-purple-alt\",\n\t\t\"border-accent-blue-alt\",\n\t];\n</script>\n\n<div\n\tclass=\"flex flex-col max-w-[100%] md:max-w-md p-4 gap-2 bg-accent-{color} border-accent-{color}-alt border-l-4 rounded-lg shadow-md\"\n\tin:fly={{\n\t\tduration: durations.enter,\n\t\teasing: quintOut,\n\t\tx: 0,\n\t\ty: 100,\n\t}}\n\tout:fade={{\n\t\tduration: durations.exit,\n\t\teasing: quintOut,\n\t}}\n>\n\t<div class=\"flex flex-row items-center justify-between w-full gap-4\">\n\t\t<div class=\"flex items-center gap-2\">\n\t\t\t<Icon\n\t\t\t\tclass=\"w-6 h-6 text-black flex-shrink-0\"\n\t\t\t\tsize=\"24\"\n\t\t\t\tstroke=\"2\"\n\t\t\t\tfill=\"none\"\n\t\t\t/>\n\t\t\t<p\n\t\t\t\tclass={clsx(\"text-black whitespace-pre-wrap\", {\n\t\t\t\t\t\"font-normal\": !title,\n\t\t\t\t})}\n\t\t\t>\n\t\t\t\t{title || message}\n\t\t\t</p>\n\t\t</div>\n\t\t<button\n\t\t\tclass=\"text-gray-600 hover:text-black flex-shrink-0\"\n\t\t\tonclick={() => ToastManager.remove(id)}\n\t\t>\n\t\t\t<XIcon size=\"16\" />\n\t\t</button>\n\t</div>\n\t{#if typeof message !== \"string\"}\n\t\t{@const MessageComponent = message}\n\t\t<div class=\"font-normal\">\n\t\t\t<MessageComponent\n\t\t\t\tbind:this={msg}\n\t\t\t\t{durations}\n\t\t\t\t{id}\n\t\t\t\t{message}\n\t\t\t\t{type}\n\t\t\t\t{additional}\n\t\t\t/>\n\t\t</div>\n\t{/if}\n</div>\n"
  },
  {
    "path": "src/lib/components/visual/Tooltip.svelte",
    "content": "<script lang=\"ts\">\n\timport { fade } from \"$lib/util/animation\";\n\tinterface Props {\n\t\tchildren: () => any;\n\t\ttext: string;\n\t\tclassName?: string;\n\t\tposition?: \"top\" | \"bottom\" | \"left\" | \"right\";\n\t}\n\n\tlet { children, text, className, position = \"top\" }: Props = $props();\n\tlet showTooltip = $state(false);\n\tlet timeout: NodeJS.Timeout | null = null;\n\tlet triggerElement: HTMLElement;\n\tlet tooltipElement = $state<HTMLElement>();\n\tlet tooltipPosition = $state({ x: 0, y: 0 });\n\n\tfunction show() {\n\t\ttimeout = setTimeout(() => {\n\t\t\tif (!triggerElement) return;\n\t\t\tconst rect = triggerElement.getBoundingClientRect();\n\n\t\t\tswitch (position) {\n\t\t\t\tcase \"top\":\n\t\t\t\t\ttooltipPosition = {\n\t\t\t\t\t\tx: rect.left + rect.width / 2,\n\t\t\t\t\t\ty: rect.top - 10,\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"bottom\":\n\t\t\t\t\ttooltipPosition = {\n\t\t\t\t\t\tx: rect.left + rect.width / 2,\n\t\t\t\t\t\ty: rect.bottom + 10,\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"left\":\n\t\t\t\t\ttooltipPosition = {\n\t\t\t\t\t\tx: rect.left - 10,\n\t\t\t\t\t\ty: rect.top + rect.height / 2,\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"right\":\n\t\t\t\t\ttooltipPosition = {\n\t\t\t\t\t\tx: rect.right + 10,\n\t\t\t\t\t\ty: rect.top + rect.height / 2,\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t\tshowTooltip = true;\n\t\t}, 500);\n\t}\n\n\tfunction hide() {\n\t\tshowTooltip = false;\n\t\tif (timeout) clearTimeout(timeout);\n\t}\n\n\tfunction handleGlobalMouseMove(e: MouseEvent) {\n\t\tif (!showTooltip || !triggerElement) return;\n\n\t\tconst triggerRect = triggerElement.getBoundingClientRect();\n\t\tconst isOverTrigger =\n\t\t\te.clientX >= triggerRect.left &&\n\t\t\te.clientX <= triggerRect.right &&\n\t\t\te.clientY >= triggerRect.top &&\n\t\t\te.clientY <= triggerRect.bottom;\n\n\t\tif (!isOverTrigger) hide();\n\t}\n\n\t$effect(() => {\n\t\tif (showTooltip && tooltipElement) {\n\t\t\tdocument.body.appendChild(tooltipElement);\n\t\t\tdocument.addEventListener(\"mousemove\", handleGlobalMouseMove);\n\t\t}\n\n\t\treturn () => {\n\t\t\tif (tooltipElement && tooltipElement.parentNode === document.body) {\n\t\t\t\tdocument.body.removeChild(tooltipElement);\n\t\t\t}\n\t\t\tdocument.removeEventListener(\"mousemove\", handleGlobalMouseMove);\n\t\t};\n\t});\n</script>\n\n<span\n\tbind:this={triggerElement}\n\tclass=\"relative inline-block {className}\"\n\tonmouseenter={show}\n\tonmouseleave={hide}\n\tonfocusin={show}\n\tonfocusout={hide}\n\tontouchstart={show}\n\tontouchend={hide}\n\trole=\"tooltip\"\n>\n\t{@render children()}\n</span>\n\n{#if showTooltip}\n\t<span\n\t\tbind:this={tooltipElement}\n\t\tclass=\"tooltip tooltip-{position}\"\n\t\tstyle=\"left: {tooltipPosition.x}px; top: {tooltipPosition.y}px;\"\n\t\ttransition:fade={{\n\t\t\tduration: 100,\n\t\t}}\n\t>\n\t\t{text}\n\t</span>\n{/if}\n\n<style lang=\"postcss\">\n\t.tooltip {\n\t\t--border-size: 1px;\n\t\t@apply fixed bg-panel-alt text-foreground border border-stone-400 dynadark:border-white drop-shadow-lg text-xs rounded-full pointer-events-none z-[999] max-w-xs break-words whitespace-normal;\n\t\t@apply px-5 py-2.5;\n\t}\n\n\t.tooltip-top {\n\t\ttransform: translate(-50%, -100%);\n\t}\n\n\t.tooltip-top::after {\n\t\t@apply content-[\"\"] absolute top-full left-1/2 -translate-x-1/2 border-8 border-x-transparent border-b-transparent;\n\t}\n\n\t.tooltip-top::before {\n\t\tborder-width: calc(var(--border-size) + 8px);\n\t\tmargin-left: calc(-1 * (var(--border-size) + 8px));\n\t\t@apply content-[\"\"] absolute top-full left-1/2 border-x-transparent border-b-transparent border-t-inherit;\n\t}\n\n\t.tooltip-bottom {\n\t\ttransform: translate(-50%, 20%);\n\t}\n\n\t.tooltip-bottom::after {\n\t\t@apply content-[\"\"] absolute bottom-full left-1/2 -ml-2 border-8 border-x-transparent border-t-transparent;\n\t}\n\n\t.tooltip-bottom::before {\n\t\tborder-width: calc(var(--border-size) + 8px);\n\t\tmargin-left: calc(-1 * (var(--border-size) + 8px));\n\t\t@apply content-[\"\"] absolute bottom-full left-1/2 border-x-transparent border-t-transparent border-b-inherit;\n\t}\n\n\t.tooltip-left {\n\t\ttransform: translate(-100%, -50%);\n\t}\n\n\t.tooltip-left::after {\n\t\t@apply content-[\"\"] absolute top-1/2 left-full -mt-2 border-8 border-y-transparent border-r-transparent border-l-inherit;\n\t}\n\n\t.tooltip-right {\n\t\ttransform: translate(0%, -50%);\n\t}\n\n\t.tooltip-right::after {\n\t\tmargin-right: -2px;\n\t\t@apply content-[\"\"] absolute top-1/2 right-full -mt-2 border-8 border-y-transparent border-l-transparent;\n\t}\n\n\t.tooltip-right::before {\n\t\tmargin-right: -2px;\n\t\tborder-width: calc(var(--border-size) + 8px);\n\t\tmargin-top: calc(-1 * (var(--border-size) + 8px));\n\t\t@apply content-[\"\"] absolute top-1/2 right-full border-y-transparent border-l-transparent border-r-inherit;\n\t}\n</style>\n"
  },
  {
    "path": "src/lib/components/visual/effects/ProgressiveBlur.svelte",
    "content": "<script lang=\"ts\">\n\ttype Props = {\n\t\titerations: number;\n\t\tendIntensity: number;\n\t\tdirection: \"top\" | \"left\" | \"bottom\" | \"right\";\n\t\tfadeTo?: string;\n\t};\n\n\tlet {\n\t\titerations,\n\t\tendIntensity,\n\t\tdirection,\n\t\tfadeTo = \"transparent\",\n\t}: Props = $props();\n\n\tconst getGradientDirection = () => {\n\t\tswitch (direction) {\n\t\t\tcase \"top\":\n\t\t\t\treturn \"to top\";\n\t\t\tcase \"left\":\n\t\t\t\treturn \"to left\";\n\t\t\tcase \"bottom\":\n\t\t\t\treturn \"to bottom\";\n\t\t\tcase \"right\":\n\t\t\t\treturn \"to right\";\n\t\t}\n\t};\n\n\tconst blurSteps = $derived(\n\t\tArray.from({ length: iterations }, (_, i) => {\n\t\t\tconst blurIntensity =\n\t\t\t\t(endIntensity / 2 ** (iterations - 1)) * 2 ** i;\n\t\t\tconst gradientStart = (i / iterations) * 100;\n\t\t\tconst gradientEnd = ((i + 1) / iterations) * 100;\n\n\t\t\treturn {\n\t\t\t\tblurIntensity,\n\t\t\t\tmask: `linear-gradient(${getGradientDirection()}, rgba(0, 0, 0, 0) ${gradientStart}%, rgba(0, 0, 0, 1) ${gradientEnd}%)`,\n\t\t\t};\n\t\t}),\n\t);\n</script>\n\n<div class=\"w-full h-full relative\">\n\t{#each blurSteps as { blurIntensity, mask }, index}\n\t\t<div\n\t\t\tclass=\"absolute w-full h-full\"\n\t\t\tstyle=\"\n        z-index: {index + 2};\n        backdrop-filter: blur( calc({blurIntensity}px * var(--blur-amount, 1)) );\n        mask: {mask};\n      \"\n\t\t></div>\n\t{/each}\n\t<div\n\t\tstyle=\"\n      z-index: {iterations + 2};\n      backdrop-filter: blur({endIntensity}px);\n      mask: linear-gradient({getGradientDirection()}, rgba(0, 0, 0, 0) ${(iterations /\n\t\t\t(iterations + 1)) *\n\t\t\t100}%, rgba(0, 0, 0, 1) 100%);\n    \"\n\t></div>\n\t<div\n\t\tclass=\"absolute top-0 left-0 w-full h-full z-50\"\n\t\tstyle=\"background: linear-gradient({getGradientDirection()}, transparent 0%, {fadeTo} 100%); opacity: var(--blur-amount, 1);\"\n\t></div>\n</div>\n"
  },
  {
    "path": "src/lib/components/visual/svg/Logo.svelte",
    "content": "<svg\n\twidth=\"100%\"\n\theight=\"100%\"\n\tviewBox=\"0 0 300 83\"\n\tversion=\"1.1\"\n\txmlns=\"http://www.w3.org/2000/svg\"\n\txmlns:xlink=\"http://www.w3.org/1999/xlink\"\n\txml:space=\"preserve\"\n\tstyle=\"fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;\"\n>\n\t<g transform=\"matrix(0.172257,0,0,0.172257,-160.012,-223.436)\">\n\t\t<path\n\t\t\td=\"M1082.77,1777.46L928.913,1297.11L1043.78,1297.11L1191.37,1777.46L1082.77,1777.46ZM1188.94,1620.03L1285.35,1297.11L1398.82,1297.11L1261.8,1724.91L1188.94,1620.03ZM1803.99,1777.46L1441.99,1777.46L1441.99,1297.11L1801.21,1297.11L1801.21,1398.05L1549.89,1398.05L1549.89,1485.77L1771.27,1485.77L1771.27,1581.14L1549.89,1581.14L1549.89,1676.52L1803.99,1676.52L1803.99,1777.46ZM1980.12,1615.25L1980.12,1777.46L1872.22,1777.46L1872.22,1297.11L2069.23,1297.11C2127.24,1297.11 2171.57,1311.49 2202.2,1340.27C2232.83,1369.04 2248.14,1407.57 2248.14,1455.83C2248.14,1504.1 2232.83,1542.74 2202.2,1571.74C2187.36,1585.8 2169.3,1596.44 2148.04,1603.69L2261.37,1777.46L2140.24,1777.46L2042.05,1615.25L1980.12,1615.25ZM1980.12,1398.05L1980.12,1514.31L2062.96,1514.31C2089.42,1514.31 2108.56,1509.32 2120.4,1499.34C2132.23,1489.36 2138.15,1474.86 2138.15,1455.83C2138.15,1436.8 2132.23,1422.42 2120.4,1412.67C2108.56,1402.92 2089.42,1398.05 2062.96,1398.05L1980.12,1398.05ZM2422.18,1398.05L2282.95,1398.05L2282.95,1297.11L2668.62,1297.11L2668.62,1398.05L2529.39,1398.05L2529.39,1777.46L2422.18,1777.46L2422.18,1398.05Z\"\n\t\t/>\n\t</g>\n</svg>\n"
  },
  {
    "path": "src/lib/components/visual/svg/LogoBeta.svelte",
    "content": "<svg\n\twidth=\"100%\"\n\theight=\"100%\"\n\tviewBox=\"0 0 303 72\"\n\tstyle=\"fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;\"\n\t><path\n\t\td=\"M27.874,72l20.469,0l27.977,-72l-20.263,0l-17.794,49.68l-17.691,-49.68l-20.572,0l27.874,72Z\"\n\t\tstyle=\"fill-rule:nonzero;\"\n\t/><path\n\t\td=\"M145.543,72l-0,-16.252l-43.92,0l-0,-12.24l42.48,0l-0,-16.251l-42.48,0l-0,-11.005l43.405,-0l0,-16.252l-61.508,-0l0,72l62.023,0Z\"\n\t\tstyle=\"fill-rule:nonzero;\"\n\t/><path\n\t\td=\"M189.837,57.862l-32.98,-32.765l0,-25.097l44.023,0c18.209,0 27.464,8.855 27.764,23.042l-13.077,-0c-1.721,-0 -3.424,0.165 -5.09,0.486c-0.615,-4.48 -5.158,-7.585 -11.654,-7.585l-23.863,-0l-0,17.589l19.284,-0c-3.607,4.682 -5.593,10.452 -5.593,16.426c-0,2.704 0.407,5.366 1.186,7.904Zm-32.98,-23.01l18.103,37.148l-18.103,0l0,-37.148Z\"\n\t/><path\n\t\td=\"M260.126,23.042l-0,-6.688l-24.686,0l0,-16.354l67.474,-0l0,16.354l-24.686,0l0,6.688l-18.103,-0Z\"\n\t/><path\n\t\td=\"M280.873,27.917c5.846,-0 11.452,2.322 15.586,6.456c4.133,4.133 6.456,9.74 6.455,15.585c0.001,5.846 -2.322,11.453 -6.455,15.586c-4.134,4.134 -9.74,6.456 -15.586,6.456l-65.306,-0c-5.846,0 -11.452,-2.322 -15.585,-6.456c-4.134,-4.133 -6.456,-9.74 -6.456,-15.586c-0,-5.845 2.322,-11.452 6.456,-15.585c4.133,-4.134 9.739,-6.456 15.585,-6.456l65.306,-0Zm-44.772,23.632l7.039,-0l-0,-3.555l-7.039,0l0,-3.133l8.243,-0l0,-3.555l-12.674,0l0,17.305l12.791,-0l0,-3.555l-8.36,0l0,-3.507Zm19.257,7.062l4.502,-0l-0,-13.598l5.343,-0l-0,-3.707l-15.188,0l-0,3.707l5.343,-0l0,13.598Zm18.278,-4.268l6.058,0l1.246,4.268l4.397,-0l-5.355,-17.305l-6.583,0l-5.367,17.305l4.362,-0l1.242,-4.268Zm5.027,-3.531l-3.998,0l1.996,-6.855l2.002,6.855Zm-56.75,-1.123c0.475,-0.143 0.896,-0.36 1.263,-0.654c0.448,-0.359 0.795,-0.799 1.04,-1.321c0.246,-0.523 0.369,-1.103 0.369,-1.743c-0,-1.083 -0.265,-1.989 -0.795,-2.718c-0.53,-0.729 -1.359,-1.272 -2.485,-1.631c-1.126,-0.359 -2.582,-0.538 -4.367,-0.538c-0.639,0 -1.302,0.027 -1.988,0.082c-0.686,0.055 -1.36,0.131 -2.022,0.228c-0.663,0.097 -1.271,0.209 -1.824,0.333l-0,16.732c0.506,0.078 1.062,0.142 1.666,0.193c0.604,0.05 1.206,0.093 1.806,0.128c0.6,0.035 1.142,0.053 1.625,0.053c1.63,-0 3.007,-0.115 4.134,-0.345c1.126,-0.23 2.036,-0.563 2.73,-1c0.693,-0.436 1.198,-0.972 1.514,-1.607c0.316,-0.636 0.473,-1.363 0.473,-2.181c0,-1.224 -0.323,-2.169 -0.97,-2.835c-0.579,-0.597 -1.302,-0.989 -2.169,-1.176Zm-6.425,1.754l2.526,0c1.052,0 1.8,0.164 2.245,0.491c0.444,0.328 0.666,0.854 0.666,1.579c0,0.468 -0.105,0.855 -0.316,1.163c-0.21,0.308 -0.569,0.538 -1.075,0.69c-0.507,0.152 -1.205,0.228 -2.093,0.228c-0.32,0 -0.632,-0.006 -0.936,-0.017c-0.304,-0.012 -0.643,-0.034 -1.017,-0.065l0,-4.069Zm0,-3.332l0,-3.601c0.32,-0.062 0.649,-0.109 0.988,-0.14c0.339,-0.031 0.696,-0.047 1.07,-0.047c1.052,-0 1.789,0.15 2.21,0.45c0.421,0.3 0.631,0.781 0.631,1.444c0,0.429 -0.087,0.781 -0.263,1.058c-0.175,0.277 -0.464,0.485 -0.865,0.626c-0.401,0.14 -0.941,0.21 -1.619,0.21l-2.152,0Z\"\n\t/></svg\n>\n"
  },
  {
    "path": "src/lib/components/visual/svg/VertVBig.svelte",
    "content": "<svg\n\twidth=\"1389\"\n\theight=\"1080\"\n\tviewBox=\"0 0 1389 1080\"\n\tfill=\"none\"\n\txmlns=\"http://www.w3.org/2000/svg\"\n>\n\t<path\n\t\td=\"M418.719 1080L0.480804 0H2.62554L420.863 1080H418.719Z\"\n\t\tfill=\"url(#paint0_linear_6_220)\"\n\t\tfill-opacity=\"0.1\"\n\t/>\n\t<path\n\t\td=\"M829.044 1080L412.359 0H410.215L826.9 1080H829.044Z\"\n\t\tfill=\"url(#paint1_linear_6_220)\"\n\t\tfill-opacity=\"0.1\"\n\t/>\n\t<path\n\t\td=\"M788.673 555.925L987.856 0H989.981L790.985 555.402L1064.61 827.169L1386.13 0H1388.27L1065.37 830.741L788.673 555.925Z\"\n\t\tfill=\"url(#paint2_linear_6_220)\"\n\t\tfill-opacity=\"0.1\"\n\t/>\n\t<defs>\n\t\t<linearGradient\n\t\t\tid=\"paint0_linear_6_220\"\n\t\t\tx1=\"694.377\"\n\t\t\ty1=\"0\"\n\t\t\tx2=\"694.377\"\n\t\t\ty2=\"1080\"\n\t\t\tgradientUnits=\"userSpaceOnUse\"\n\t\t>\n\t\t\t<stop stop-color=\"white\" />\n\t\t\t<stop offset=\"0.75\" stop-color=\"white\" />\n\t\t\t<stop offset=\"1\" stop-color=\"white\" stop-opacity=\"0\" />\n\t\t</linearGradient>\n\t\t<linearGradient\n\t\t\tid=\"paint1_linear_6_220\"\n\t\t\tx1=\"694.377\"\n\t\t\ty1=\"0\"\n\t\t\tx2=\"694.377\"\n\t\t\ty2=\"1080\"\n\t\t\tgradientUnits=\"userSpaceOnUse\"\n\t\t>\n\t\t\t<stop stop-color=\"white\" />\n\t\t\t<stop offset=\"0.75\" stop-color=\"white\" />\n\t\t\t<stop offset=\"1\" stop-color=\"white\" stop-opacity=\"0\" />\n\t\t</linearGradient>\n\t\t<linearGradient\n\t\t\tid=\"paint2_linear_6_220\"\n\t\t\tx1=\"694.377\"\n\t\t\ty1=\"0\"\n\t\t\tx2=\"694.377\"\n\t\t\ty2=\"1080\"\n\t\t\tgradientUnits=\"userSpaceOnUse\"\n\t\t>\n\t\t\t<stop stop-color=\"white\" />\n\t\t\t<stop offset=\"0.75\" stop-color=\"white\" />\n\t\t\t<stop offset=\"1\" stop-color=\"white\" stop-opacity=\"0\" />\n\t\t</linearGradient>\n\t</defs>\n</svg>\n"
  },
  {
    "path": "src/lib/converters/converter.svelte.ts",
    "content": "import type { VertFile } from \"$lib/types\";\n\nexport type WorkerStatus = \"not-ready\" | \"downloading\" | \"ready\" | \"error\";\n\nexport class FormatInfo {\n\tpublic name: string;\n\n\tconstructor(\n\t\tname: string,\n\t\tpublic fromSupported = true,\n\t\tpublic toSupported = true,\n\t\tpublic isNative = true,\n\t) {\n\t\tthis.name = name;\n\t\tif (!this.name.startsWith(\".\")) {\n\t\t\tthis.name = `.${this.name}`;\n\t\t}\n\n\t\tif (!this.fromSupported && !this.toSupported) {\n\t\t\tthrow new Error(\"Format must support at least one direction\");\n\t\t}\n\t}\n}\n\n/**\n * Base class for all converters.\n */\nexport class Converter {\n\t/**\n\t * The public name of the converter.\n\t */\n\tpublic name: string = \"Unknown\";\n\t/**\n\t * List of supported formats.\n\t */\n\tpublic supportedFormats: FormatInfo[] = [];\n\n\tpublic status: WorkerStatus = $state(\"not-ready\");\n\tpublic readonly reportsProgress: boolean = false;\n\n\tprivate timeoutId?: NodeJS.Timeout;\n\n\tconstructor(public readonly timeout: number = 10) {\n\t\tthis.startTimeout();\n\t}\n\n\tprivate startTimeout() {\n\t\tthis.timeoutId = setTimeout(() => {\n\t\t\tif (this.status !== \"ready\") this.status = \"not-ready\";\n\t\t}, this.timeout * 1000);\n\t}\n\n\tprotected clearTimeout() {\n\t\tif (this.timeoutId) {\n\t\t\tclearTimeout(this.timeoutId);\n\t\t\tthis.timeoutId = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Convert a file to a different format.\n\t * @param input The input file.\n\t * @param to The format to convert to. Includes the dot.\n\t */\n\tpublic async convert(\n\t\t// eslint-disable-next-line @typescript-eslint/no-unused-vars\n\t\tinput: VertFile,\n\t\t// eslint-disable-next-line @typescript-eslint/no-unused-vars\n\t\tto: string,\n\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars\n\t\t...args: any[]\n\t): Promise<VertFile> {\n\t\tthrow new Error(\"Not implemented\");\n\t}\n\n\t/**\n\t * Cancel the active conversion of a file.\n\t * @param input The input file.\n\t */\n\t// eslint-disable-next-line @typescript-eslint/no-unused-vars\n\tpublic async cancel(input: VertFile): Promise<void> {\n\t\tthrow new Error(\"Not implemented\");\n\t}\n\n\tpublic async valid(): Promise<boolean> {\n\t\treturn true;\n\t}\n\n\tpublic formatStrings(predicate?: (f: FormatInfo) => boolean) {\n\t\tif (predicate) {\n\t\t\treturn this.supportedFormats.filter(predicate).map((f) => f.name);\n\t\t}\n\t\treturn this.supportedFormats.map((f) => f.name);\n\t}\n}\n"
  },
  {
    "path": "src/lib/converters/ffmpeg.svelte.ts",
    "content": "import { VertFile } from \"$lib/types\";\nimport { Converter, FormatInfo } from \"./converter.svelte\";\nimport { FFmpeg } from \"@ffmpeg/ffmpeg\";\nimport { browser } from \"$app/environment\";\nimport { error, log } from \"$lib/util/logger\";\nimport { m } from \"$lib/paraglide/messages\";\nimport { Settings } from \"$lib/sections/settings/index.svelte\";\nimport { ToastManager } from \"$lib/util/toast.svelte\";\n\n// TODO: differentiate in UI? (not native formats)\nconst videoFormats = [\n\t\"mkv\",\n\t\"mp4\",\n\t\"avi\",\n\t\"mov\",\n\t\"webm\",\n\t\"ts\",\n\t\"mts\",\n\t\"m2ts\",\n\t\"wmv\",\n\t\"mpg\",\n\t\"mpeg\",\n\t\"flv\",\n\t\"f4v\",\n\t\"vob\",\n\t\"m4v\",\n\t\"3gp\",\n\t\"3g2\",\n\t\"mxf\",\n\t\"ogv\",\n\t\"rm\",\n\t\"rmvb\",\n\t\"divx\",\n];\n\nexport class FFmpegConverter extends Converter {\n\tprivate ffmpeg: FFmpeg = null!;\n\tpublic name = \"ffmpeg\";\n\tpublic ready = $state(false);\n\n\tprivate activeConversions = new Map<string, FFmpeg>();\n\n\tpublic supportedFormats = [\n\t\tnew FormatInfo(\"mp3\", true, true),\n\t\tnew FormatInfo(\"wav\", true, true),\n\t\tnew FormatInfo(\"flac\", true, true),\n\t\tnew FormatInfo(\"ogg\", true, true),\n\t\tnew FormatInfo(\"mogg\", true, false),\n\t\tnew FormatInfo(\"oga\", true, true),\n\t\tnew FormatInfo(\"opus\", true, true),\n\t\tnew FormatInfo(\"aac\", true, true),\n\t\tnew FormatInfo(\"alac\", true, true), // outputted as m4a\n\t\tnew FormatInfo(\"m4a\", true, true), // can be alac\n\t\tnew FormatInfo(\"caf\", true, false), // can be alac\n\t\tnew FormatInfo(\"wma\", true, true),\n\t\tnew FormatInfo(\"amr\", true, true),\n\t\tnew FormatInfo(\"ac3\", true, true),\n\t\tnew FormatInfo(\"aiff\", true, true),\n\t\tnew FormatInfo(\"aifc\", true, true),\n\t\tnew FormatInfo(\"aif\", true, true),\n\t\tnew FormatInfo(\"mp1\", true, false),\n\t\tnew FormatInfo(\"mp2\", true, true),\n\t\tnew FormatInfo(\"mpc\", true, false), // unknown if it works, can't find sample file but ffmpeg should support i think?\n\t\t//new FormatInfo(\"raw\", true, false), // usually pcm\n\t\tnew FormatInfo(\"dsd\", true, false), // dsd\n\t\tnew FormatInfo(\"dsf\", true, false), // dsd\n\t\tnew FormatInfo(\"dff\", true, false), // dsd\n\t\tnew FormatInfo(\"mqa\", true, false),\n\t\tnew FormatInfo(\"au\", true, true),\n\t\tnew FormatInfo(\"m4b\", true, true),\n\t\tnew FormatInfo(\"voc\", true, true),\n\t\tnew FormatInfo(\"weba\", true, true),\n\t\t...videoFormats.map((f) => new FormatInfo(f, true, true, false)),\n\t];\n\n\tpublic readonly reportsProgress = true;\n\n\tconstructor() {\n\t\tsuper();\n\t\tlog([\"converters\", this.name], `created converter`);\n\t\tif (!browser) return;\n\t\ttry {\n\t\t\t// this is just to cache the wasm and js for when we actually use it. we're not using this ffmpeg instance\n\t\t\tthis.ffmpeg = new FFmpeg();\n\t\t\t(async () => {\n\t\t\t\tconst baseURL =\n\t\t\t\t\t\"https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/esm\";\n\n\t\t\t\tthis.status = \"downloading\";\n\n\t\t\t\tawait this.ffmpeg.load({\n\t\t\t\t\tcoreURL: `${baseURL}/ffmpeg-core.js`,\n\t\t\t\t\twasmURL: `${baseURL}/ffmpeg-core.wasm`,\n\t\t\t\t});\n\n\t\t\t\tthis.status = \"ready\";\n\t\t\t})();\n\t\t} catch (err) {\n\t\t\terror([\"converters\", this.name], `Error loading ffmpeg: ${err}`);\n\t\t\tthis.status = \"error\";\n\t\t\tToastManager.add({\n\t\t\t\ttype: \"error\",\n\t\t\t\tmessage: m[\"workers.errors.ffmpeg\"](),\n\t\t\t});\n\t\t}\n\t}\n\n\tpublic async convert(input: VertFile, to: string): Promise<VertFile> {\n\t\tif (!to.startsWith(\".\")) to = `.${to}`;\n\n\t\tconst isAlac = to === \".alac\";\n\t\tif (isAlac) to = \".m4a\";\n\n\t\tlet conversionError: string | null = null;\n\t\tconst ffmpeg = await this.setupFFmpeg(input);\n\n\t\tthis.activeConversions.set(input.id, ffmpeg);\n\n\t\t// listen for errors during conversion\n\t\tconst errorListener = (l: { message: string }) => {\n\t\t\tconst msg = l.message;\n\t\t\tif (\n\t\t\t\tmsg.includes(\"Specified sample rate\") &&\n\t\t\t\tmsg.includes(\"is not supported\")\n\t\t\t) {\n\t\t\t\tconst rate = Settings.instance.settings.ffmpegCustomSampleRate;\n\t\t\t\tconversionError = m[\"workers.errors.invalid_rate\"]({\n\t\t\t\t\trate,\n\t\t\t\t});\n\t\t\t} else if (msg.includes(\"Stream map '0:a:0' matches no streams.\")) {\n\t\t\t\tconversionError = m[\"workers.errors.no_audio\"]();\n\t\t\t} else if (\n\t\t\t\tmsg.includes(\"Error initializing output stream\") ||\n\t\t\t\tmsg.includes(\"Error while opening encoder\") ||\n\t\t\t\tmsg.includes(\"Error while opening decoder\") ||\n\t\t\t\t(msg.includes(\"Error\") && msg.includes(\"stream\")) ||\n\t\t\t\tmsg.includes(\"Conversion failed!\")\n\t\t\t) {\n\t\t\t\t// other general errors\n\t\t\t\tif (!conversionError) conversionError = msg;\n\t\t\t}\n\t\t};\n\n\t\tffmpeg.on(\"log\", errorListener);\n\n\t\tconst buf = new Uint8Array(await input.file.arrayBuffer());\n\t\tawait ffmpeg.writeFile(\"input\", buf);\n\t\tlog(\n\t\t\t[\"converters\", this.name],\n\t\t\t`wrote ${input.name} to ffmpeg virtual fs`,\n\t\t);\n\n\t\tconst command = await this.buildConversionCommand(\n\t\t\tffmpeg,\n\t\t\tinput,\n\t\t\tto,\n\t\t\tisAlac,\n\t\t);\n\t\tlog([\"converters\", this.name], `FFmpeg command: ${command.join(\" \")}`);\n\t\tawait ffmpeg.exec(command);\n\t\tlog([\"converters\", this.name], \"executed ffmpeg command\");\n\n\t\tif (conversionError) {\n\t\t\tffmpeg.off(\"log\", errorListener);\n\t\t\tffmpeg.terminate();\n\t\t\tthrow new Error(conversionError);\n\t\t}\n\n\t\tconst output = (await ffmpeg.readFile(\n\t\t\t\"output\" + to,\n\t\t)) as unknown as Uint8Array;\n\n\t\tif (!output || output.length === 0) {\n\t\t\tffmpeg.off(\"log\", errorListener);\n\t\t\tffmpeg.terminate();\n\t\t\tthrow new Error(\"empty file returned\");\n\t\t}\n\n\t\tconst outputFileName =\n\t\t\tinput.name.split(\".\").slice(0, -1).join(\".\") + to;\n\t\tlog(\n\t\t\t[\"converters\", this.name],\n\t\t\t`read ${outputFileName} from ffmpeg virtual fs`,\n\t\t);\n\n\t\tffmpeg.off(\"log\", errorListener);\n\t\tffmpeg.terminate();\n\n\t\tconst outBuf = new Uint8Array(output).buffer.slice(0);\n\t\treturn new VertFile(new File([outBuf], outputFileName), to);\n\t}\n\n\tpublic async cancel(input: VertFile): Promise<void> {\n\t\tconst ffmpeg = this.activeConversions.get(input.id);\n\t\tif (!ffmpeg) {\n\t\t\terror(\n\t\t\t\t[\"converters\", this.name],\n\t\t\t\t`no active conversion found for file ${input.name}`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tlog(\n\t\t\t[\"converters\", this.name],\n\t\t\t`cancelling conversion for file ${input.name}`,\n\t\t);\n\n\t\tffmpeg.terminate();\n\t\tthis.activeConversions.delete(input.id);\n\t}\n\n\tprivate async setupFFmpeg(input: VertFile): Promise<FFmpeg> {\n\t\tconst ffmpeg = new FFmpeg();\n\n\t\tffmpeg.on(\"progress\", (progress) => {\n\t\t\tinput.progress = progress.progress * 100;\n\t\t});\n\n\t\tffmpeg.on(\"log\", (l) => {\n\t\t\tlog([\"converters\", this.name], l.message);\n\t\t});\n\n\t\tconst baseURL =\n\t\t\t\"https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/esm\";\n\t\tawait ffmpeg.load({\n\t\t\tcoreURL: `${baseURL}/ffmpeg-core.js`,\n\t\t\twasmURL: `${baseURL}/ffmpeg-core.wasm`,\n\t\t});\n\n\t\treturn ffmpeg;\n\t}\n\n\tprivate async detectAudioBitrate(ffmpeg: FFmpeg): Promise<number | null> {\n\t\tconst args = [\n\t\t\t\"-v\",\n\t\t\t\"quiet\",\n\t\t\t\"-select_streams\",\n\t\t\t\"a:0\",\n\t\t\t\"-show_entries\",\n\t\t\t\"stream=bit_rate\",\n\t\t\t\"-of\",\n\t\t\t\"default=noprint_wrappers=1:nokey=1\",\n\t\t\t\"input\",\n\t\t];\n\n\t\ttry {\n\t\t\tlet bitrate: number | null = null;\n\n\t\t\tconst bitrateListener = (event: { message: string }) => {\n\t\t\t\tif (bitrate !== null) return;\n\t\t\t\tconst n = parseInt(event.message.trim(), 10);\n\t\t\t\tif (!n) return;\n\t\t\t\tbitrate = Math.round(n / 1000);\n\t\t\t\tlog(\n\t\t\t\t\t[\"converters\", this.name],\n\t\t\t\t\t`Detected stream audio bitrate: ${bitrate} kbps`,\n\t\t\t\t);\n\t\t\t};\n\n\t\t\tffmpeg.on(\"log\", bitrateListener);\n\n\t\t\ttry {\n\t\t\t\tawait ffmpeg.ffprobe.call(ffmpeg, args);\n\t\t\t\treturn bitrate;\n\t\t\t} finally {\n\t\t\t\tffmpeg.off(\"log\", bitrateListener);\n\t\t\t}\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate async detectAudioSampleRate(\n\t\tffmpeg: FFmpeg,\n\t): Promise<number | null> {\n\t\tconst args = [\n\t\t\t\"-v\",\n\t\t\t\"quiet\",\n\t\t\t\"-select_streams\",\n\t\t\t\"a:0\",\n\t\t\t\"-show_entries\",\n\t\t\t\"stream=sample_rate\",\n\t\t\t\"-of\",\n\t\t\t\"default=noprint_wrappers=1:nokey=1\",\n\t\t\t\"input\",\n\t\t];\n\n\t\ttry {\n\t\t\tlet sampleRate: number | null = null;\n\n\t\t\tconst sampleRateListener = (event: { message: string }) => {\n\t\t\t\tif (sampleRate !== null) return;\n\t\t\t\tconst n = parseInt(event.message.trim(), 10);\n\t\t\t\tif (!n) return;\n\t\t\t\tsampleRate = n;\n\t\t\t\tlog(\n\t\t\t\t\t[\"converters\", this.name],\n\t\t\t\t\t`Detected stream audio sample rate: ${sampleRate} Hz`,\n\t\t\t\t);\n\t\t\t};\n\n\t\t\tffmpeg.on(\"log\", sampleRateListener);\n\n\t\t\ttry {\n\t\t\t\tawait ffmpeg.ffprobe.call(ffmpeg, args);\n\t\t\t\treturn sampleRate;\n\t\t\t} finally {\n\t\t\t\tffmpeg.off(\"log\", sampleRateListener);\n\t\t\t}\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate async buildConversionCommand(\n\t\tffmpeg: FFmpeg,\n\t\tinput: VertFile,\n\t\tto: string,\n\t\tisAlac: boolean = false,\n\t): Promise<string[]> {\n\t\tconst inputFormat = input.from.slice(1);\n\t\tconst outputFormat = to.slice(1);\n\t\tconst m4a = isAlac || to === \".m4a\";\n\n\t\tconst lossless = [\n\t\t\t\"flac\",\n\t\t\t\"m4a\",\n\t\t\t\"caf\",\n\t\t\t\"alac\",\n\t\t\t\"wav\",\n\t\t\t\"dsd\",\n\t\t\t\"dsf\",\n\t\t\t\"dff\",\n\t\t];\n\t\tconst userSetting = Settings.instance.settings.ffmpegQuality;\n\t\tconst userSampleRate = Settings.instance.settings.ffmpegSampleRate;\n\t\tconst customSampleRate =\n\t\t\tSettings.instance.settings.ffmpegCustomSampleRate ?? 44100;\n\t\tconst keepMetadata = Settings.instance.settings.metadata;\n\n\t\tlet audioBitrateArgs: string[] = [];\n\t\tlet sampleRateArgs: string[] = [];\n\t\tlet metadataArgs: string[] = [];\n\t\tlet m4aArgs: string[] = [];\n\n\t\tlog([\"converters\", this.name], `keep metadata: ${keepMetadata}`);\n\t\tif (!keepMetadata) {\n\t\t\tmetadataArgs = [\n\t\t\t\t\"-map_metadata\", // remove metadata\n\t\t\t\t\"-1\",\n\t\t\t\t\"-map_chapters\", // remove chapters\n\t\t\t\t\"-1\",\n\t\t\t\t\"-map\", // remove cover art\n\t\t\t\t\"a\",\n\t\t\t];\n\t\t}\n\n\t\tconst isLosslessToLossy =\n\t\t\tlossless.includes(inputFormat) && !lossless.includes(outputFormat);\n\t\tif (userSetting !== \"auto\") {\n\t\t\t// user's setting\n\t\t\taudioBitrateArgs = [\"-b:a\", `${userSetting}k`];\n\t\t\tlog(\n\t\t\t\t[\"converters\", this.name],\n\t\t\t\t`using user setting for audio bitrate: ${userSetting}`,\n\t\t\t);\n\t\t} else {\n\t\t\t// detect bitrate of original file and use\n\t\t\tif (isLosslessToLossy) {\n\t\t\t\t// use safe default\n\t\t\t\taudioBitrateArgs = [\"-b:a\", \"128k\"];\n\t\t\t\tlog(\n\t\t\t\t\t[\"converters\", this.name],\n\t\t\t\t\t`converting from lossless to lossy, using default audio bitrate: 128k`,\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tconst inputBitrate = await this.detectAudioBitrate(ffmpeg);\n\t\t\t\taudioBitrateArgs = inputBitrate\n\t\t\t\t\t? [\"-b:a\", `${inputBitrate}k`]\n\t\t\t\t\t: [];\n\t\t\t\tlog(\n\t\t\t\t\t[\"converters\", this.name],\n\t\t\t\t\t`using detected audio bitrate: ${inputBitrate}k`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// sample rate setting\n\t\tif (userSampleRate !== \"auto\") {\n\t\t\tconst rate =\n\t\t\t\tuserSampleRate === \"custom\"\n\t\t\t\t\t? customSampleRate.toString()\n\t\t\t\t\t: userSampleRate;\n\t\t\tsampleRateArgs = [\"-ar\", rate];\n\t\t\tlog(\n\t\t\t\t[\"converters\", this.name],\n\t\t\t\t`using user setting for sample rate: ${rate}`,\n\t\t\t);\n\t\t} else {\n\t\t\t// detect sample rate of original file and use\n\t\t\tif (isLosslessToLossy) {\n\t\t\t\t// use safe default\n\t\t\t\tconst defaultRate = to === \".opus\" ? \"48000\" : \"44100\";\n\t\t\t\tlog(\n\t\t\t\t\t[\"converters\", this.name],\n\t\t\t\t\t`converting from lossless to lossy, using default sample rate: ${defaultRate}Hz`,\n\t\t\t\t);\n\t\t\t\tsampleRateArgs = [\"-ar\", defaultRate];\n\t\t\t} else {\n\t\t\t\tlet inputSampleRate = await this.detectAudioSampleRate(ffmpeg);\n\t\t\t\tif (to === \".opus\" && inputSampleRate === 44100) {\n\t\t\t\t\t// special case: opus does not support 44100Hz which is more common - adjust to 48000Hz\n\t\t\t\t\tlog(\n\t\t\t\t\t\t[\"converters\", this.name],\n\t\t\t\t\t\t\"conversion to opus with 44100Hz sample rate detected, adjusting to 48000Hz\",\n\t\t\t\t\t);\n\t\t\t\t\tinputSampleRate = 48000;\n\t\t\t\t}\n\n\t\t\t\tsampleRateArgs = inputSampleRate\n\t\t\t\t\t? [\"-ar\", inputSampleRate.toString()]\n\t\t\t\t\t: [];\n\t\t\t\tlog(\n\t\t\t\t\t[\"converters\", this.name],\n\t\t\t\t\t`using detected audio sample rate: ${inputSampleRate}Hz`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// video to audio\n\t\tif (videoFormats.includes(inputFormat)) {\n\t\t\tlog(\n\t\t\t\t[\"converters\", this.name],\n\t\t\t\t`Converting video ${input.from} to audio ${to}`,\n\t\t\t);\n\t\t\treturn [\n\t\t\t\t\"-i\",\n\t\t\t\t\"input\",\n\t\t\t\t\"-map\",\n\t\t\t\t\"0:a:0\",\n\t\t\t\t...metadataArgs,\n\t\t\t\t...audioBitrateArgs,\n\t\t\t\t...sampleRateArgs,\n\t\t\t\t\"output\" + to,\n\t\t\t];\n\t\t}\n\n\t\t// audio to video\n\t\tif (videoFormats.includes(outputFormat)) {\n\t\t\tlog(\n\t\t\t\t[\"converters\", this.name],\n\t\t\t\t`Converting audio ${input.from} to video ${to}`,\n\t\t\t);\n\n\t\t\tconst hasAlbumArt = keepMetadata\n\t\t\t\t? await this.extractAlbumArt(ffmpeg)\n\t\t\t\t: false;\n\t\t\tconst codecArgs = toArgs(to, isAlac);\n\n\t\t\tif (hasAlbumArt) {\n\t\t\t\tlog(\n\t\t\t\t\t[\"converters\", this.name],\n\t\t\t\t\t\"Using album art as video background\",\n\t\t\t\t);\n\t\t\t\treturn [\n\t\t\t\t\t\"-loop\",\n\t\t\t\t\t\"1\",\n\t\t\t\t\t\"-i\",\n\t\t\t\t\t\"cover.jpg\",\n\t\t\t\t\t\"-i\",\n\t\t\t\t\t\"input\",\n\t\t\t\t\t\"-vf\",\n\t\t\t\t\t\"scale=trunc(iw/2)*2:trunc(ih/2)*2\",\n\t\t\t\t\t\"-shortest\",\n\t\t\t\t\t\"-pix_fmt\",\n\t\t\t\t\t\"yuv420p\",\n\t\t\t\t\t\"-r\",\n\t\t\t\t\t\"1\",\n\t\t\t\t\t...codecArgs,\n\t\t\t\t\t...metadataArgs,\n\t\t\t\t\t...audioBitrateArgs,\n\t\t\t\t\t...sampleRateArgs,\n\t\t\t\t\t\"output\" + to,\n\t\t\t\t];\n\t\t\t} else {\n\t\t\t\tlog([\"converters\", this.name], \"Using solid color background\");\n\t\t\t\treturn [\n\t\t\t\t\t\"-f\",\n\t\t\t\t\t\"lavfi\",\n\t\t\t\t\t\"-i\",\n\t\t\t\t\t\"color=c=black:s=512x512:rate=1\",\n\t\t\t\t\t\"-i\",\n\t\t\t\t\t\"input\",\n\t\t\t\t\t\"-shortest\",\n\t\t\t\t\t\"-pix_fmt\",\n\t\t\t\t\t\"yuv420p\",\n\t\t\t\t\t\"-r\",\n\t\t\t\t\t\"1\",\n\t\t\t\t\t...toArgs(to, isAlac),\n\t\t\t\t\t...metadataArgs,\n\t\t\t\t\t...audioBitrateArgs,\n\t\t\t\t\t...sampleRateArgs,\n\t\t\t\t\t\"output\" + to,\n\t\t\t\t];\n\t\t\t}\n\t\t}\n\n\t\t// audio to audio\n\t\tlog(\n\t\t\t[\"converters\", this.name],\n\t\t\t`Converting audio ${input.from} to audio ${to}`,\n\t\t);\n\t\tconst { audio: audioCodec } = getCodecs(to, isAlac);\n\t\tif (m4a && keepMetadata) m4aArgs = [\"-c:v\", \"copy\"]; // for album art\n\n\t\treturn [\n\t\t\t\"-i\",\n\t\t\t\"input\",\n\t\t\t...m4aArgs,\n\t\t\t\"-c:a\",\n\t\t\taudioCodec,\n\t\t\t...metadataArgs,\n\t\t\t...audioBitrateArgs,\n\t\t\t...sampleRateArgs,\n\t\t\t\"output\" + to,\n\t\t];\n\t}\n\n\tprivate async extractAlbumArt(ffmpeg: FFmpeg): Promise<boolean> {\n\t\t//  extract using stream mapping (should work for most)\n\t\tif (\n\t\t\tawait this.tryExtractAlbumArt(ffmpeg, [\n\t\t\t\t\"-i\",\n\t\t\t\t\"input\",\n\t\t\t\t\"-map\",\n\t\t\t\t\"0:1\",\n\t\t\t\t\"-c:v\",\n\t\t\t\t\"copy\",\n\t\t\t\t\"-update\",\n\t\t\t\t\"1\",\n\t\t\t\t\"cover.jpg\",\n\t\t\t])\n\t\t) {\n\t\t\tlog(\n\t\t\t\t[\"converters\", this.name],\n\t\t\t\t\"Successfully extracted album art from stream 0:1\",\n\t\t\t);\n\t\t\treturn true;\n\t\t}\n\n\t\t// fallback: extract without stream mapping (this probably won't happen)\n\t\tif (\n\t\t\tawait this.tryExtractAlbumArt(ffmpeg, [\n\t\t\t\t\"-i\",\n\t\t\t\t\"input\",\n\t\t\t\t\"-an\",\n\t\t\t\t\"-c:v\",\n\t\t\t\t\"copy\",\n\t\t\t\t\"-update\",\n\t\t\t\t\"1\",\n\t\t\t\t\"cover.jpg\",\n\t\t\t])\n\t\t) {\n\t\t\tlog(\n\t\t\t\t[\"converters\", this.name],\n\t\t\t\t\"Successfully extracted album art (fallback method)\",\n\t\t\t);\n\t\t\treturn true;\n\t\t}\n\n\t\tlog(\n\t\t\t[\"converters\", this.name],\n\t\t\t\"No album art found, will create solid color background\",\n\t\t);\n\t\treturn false;\n\t}\n\n\tprivate async tryExtractAlbumArt(\n\t\tffmpeg: FFmpeg,\n\t\tcommand: string[],\n\t): Promise<boolean> {\n\t\ttry {\n\t\t\tawait ffmpeg.exec(command);\n\t\t\tconst coverData = await ffmpeg.readFile(\"cover.jpg\");\n\t\t\treturn !!(coverData && (coverData as Uint8Array).length > 0);\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n}\n\n// and here i was, thinking i'd be done with ffmpeg after finishing vertd\n// but OH NO we just HAD to have someone suggest to allow album art video generation.\n//\n// i hate you SO much.\n// - love, maddie\nconst toArgs = (ext: string, isAlac: boolean = false): string[] => {\n\tconst codecs = getCodecs(ext, isAlac);\n\tconst args = [\"-c:v\", codecs.video];\n\n\tswitch (codecs.video) {\n\t\tcase \"libx264\": {\n\t\t\targs.push(\n\t\t\t\t\"-preset\",\n\t\t\t\t\"ultrafast\",\n\t\t\t\t\"-crf\",\n\t\t\t\t\"18\",\n\t\t\t\t\"-tune\",\n\t\t\t\t\"stillimage\",\n\t\t\t);\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"libvpx\": {\n\t\t\targs.push(\"-c:v\", \"libvpx-vp9\");\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"mpeg2video\": {\n\t\t\t// for mpeg, mpg, vob, mxf\n\t\t\tif (ext === \".mxf\") args.push(\"-ar\", \"48000\"); // force 48kHz sample rate\n\t\t\tbreak;\n\t\t}\n\t}\n\n\targs.push(\"-c:a\", codecs.audio);\n\n\tif (codecs.audio === \"aac\") args.push(\"-strict\", \"experimental\");\n\n\tif (ext === \".divx\") args.unshift(\"-f\", \"avi\");\n\tif (ext === \".mxf\") args.push(\"-strict\", \"unofficial\");\n\n\treturn args;\n};\n\nconst getCodecs = (\n\text: string,\n\tisAlac: boolean = false,\n): { video: string; audio: string } => {\n\tswitch (ext) {\n\t\t// video <-> audio\n\t\tcase \".mp4\":\n\t\tcase \".mkv\":\n\t\tcase \".mov\":\n\t\tcase \".mts\":\n\t\tcase \".ts\":\n\t\tcase \".m2ts\":\n\t\tcase \".flv\":\n\t\tcase \".f4v\":\n\t\tcase \".m4v\":\n\t\tcase \".3gp\":\n\t\tcase \".3g2\":\n\t\t\treturn { video: \"libx264\", audio: \"aac\" };\n\t\tcase \".wmv\":\n\t\t\treturn { video: \"wmv2\", audio: \"wmav2\" };\n\t\tcase \".webm\":\n\t\tcase \".ogv\":\n\t\t\treturn {\n\t\t\t\tvideo: ext === \".webm\" ? \"libvpx\" : \"libtheora\",\n\t\t\t\taudio: \"libvorbis\",\n\t\t\t};\n\t\tcase \".avi\":\n\t\tcase \".divx\":\n\t\t\treturn { video: \"mpeg4\", audio: \"libmp3lame\" };\n\t\tcase \".mpg\":\n\t\tcase \".mpeg\":\n\t\tcase \".vob\":\n\t\t\treturn { video: \"mpeg2video\", audio: \"mp2\" };\n\t\tcase \".mxf\":\n\t\t\treturn { video: \"mpeg2video\", audio: \"pcm_s16le\" };\n\n\t\t// audio\n\t\tcase \".mp3\":\n\t\t\treturn { video: \"libx264\", audio: \"libmp3lame\" };\n\t\tcase \".flac\":\n\t\t\treturn { video: \"libx264\", audio: \"flac\" };\n\t\tcase \".wav\":\n\t\t\treturn { video: \"libx264\", audio: \"pcm_s16le\" };\n\t\tcase \".ogg\":\n\t\tcase \".oga\":\n\t\t\treturn { video: \"libx264\", audio: \"libvorbis\" };\n\t\tcase \".opus\":\n\t\t\treturn { video: \"libx264\", audio: \"libopus\" };\n\t\tcase \".aac\":\n\t\t\treturn { video: \"libx264\", audio: \"aac\" };\n\t\tcase \".m4a\":\n\t\t\treturn {\n\t\t\t\tvideo: \"libx264\",\n\t\t\t\taudio: isAlac ? \"alac\" : \"aac\",\n\t\t\t};\n\t\tcase \".alac\":\n\t\t\treturn { video: \"libx264\", audio: \"alac\" };\n\t\tcase \".wma\":\n\t\t\treturn { video: \"libx264\", audio: \"wmav2\" };\n\n\t\tdefault:\n\t\t\treturn { video: \"libx264\", audio: \"aac\" };\n\t}\n};\n\nexport const CONVERSION_BITRATES = [\n\t\"auto\",\n\t320,\n\t256,\n\t192,\n\t128,\n\t96,\n\t64,\n\t32,\n] as const;\nexport type ConversionBitrate = (typeof CONVERSION_BITRATES)[number];\n\nexport const SAMPLE_RATES = [\n\t\"auto\",\n\t\"custom\",\n\t\"48000\",\n\t\"44100\",\n\t\"32000\",\n\t\"22050\",\n\t\"16000\",\n\t\"11025\",\n\t\"8000\",\n] as const;\nexport type SampleRate = (typeof SAMPLE_RATES)[number];\n"
  },
  {
    "path": "src/lib/converters/index.ts",
    "content": "import type { Categories } from \"$lib/types\";\nimport type { Converter } from \"./converter.svelte\";\nimport { FFmpegConverter } from \"./ffmpeg.svelte\";\nimport { PandocConverter } from \"./pandoc.svelte\";\nimport { VertdConverter } from \"./vertd.svelte\";\nimport { MagickConverter } from \"./magick.svelte\";\nimport { DISABLE_ALL_EXTERNAL_REQUESTS } from \"$lib/util/consts\";\n\nconst getConverters = (): Converter[] => {\n\tconst converters: Converter[] = [\n\t\tnew MagickConverter(),\n\t\tnew FFmpegConverter(),\n\t];\n\n\tif (!DISABLE_ALL_EXTERNAL_REQUESTS) {\n\t\tconverters.push(new VertdConverter());\n\t}\n\n\tconverters.push(new PandocConverter());\n\treturn converters;\n};\n\nexport const converters = getConverters();\n\nexport function getConverterByFormat(format: string) {\n\tfor (const converter of converters) {\n\t\tif (converter.supportedFormats.some((f) => f.name === format)) {\n\t\t\treturn converter;\n\t\t}\n\t}\n\treturn null;\n}\n\nexport const categories: Categories = {\n\timage: { formats: [\"\"], canConvertTo: [] },\n\tvideo: { formats: [\"\"], canConvertTo: [\"audio\"] },\n\taudio: { formats: [\"\"], canConvertTo: [\"video\"] },\n\tdoc: { formats: [\"\"], canConvertTo: [] },\n};\n\ncategories.audio.formats =\n\tconverters\n\t\t.find((c) => c.name === \"ffmpeg\")\n\t\t?.supportedFormats.filter((f) => f.toSupported && f.isNative)\n\t\t.map((f) => f.name) || [];\ncategories.video.formats =\n\tconverters\n\t\t.find((c) => c.name === \"vertd\")\n\t\t?.supportedFormats.filter((f) => f.toSupported && f.isNative)\n\t\t.map((f) => f.name) || [];\ncategories.image.formats =\n\tconverters\n\t\t.find((c) => c.name === \"imagemagick\")\n\t\t?.formatStrings((f) => f.toSupported) || [];\ncategories.doc.formats =\n\tconverters\n\t\t.find((c) => c.name === \"pandoc\")\n\t\t?.supportedFormats.filter((f) => f.toSupported && f.isNative)\n\t\t.map((f) => f.name) || [];\n\nexport const byNative = (format: string) => {\n\treturn (a: Converter, b: Converter) => {\n\t\tconst aFormat = a.supportedFormats.find((f) => f.name === format);\n\t\tconst bFormat = b.supportedFormats.find((f) => f.name === format);\n\n\t\tif (aFormat && bFormat) {\n\t\t\treturn aFormat.isNative ? -1 : 1;\n\t\t}\n\t\treturn 0;\n\t};\n};\n"
  },
  {
    "path": "src/lib/converters/magick-automated.ts",
    "content": "import { FormatInfo } from \"./converter.svelte\";\n\n// formats added from maya's somewhat automated testing\n// placed into this file to easily differentiate (and also clean up the main magick file)\n// some formats also have a comment from what i saw during testing\nexport const imageFormats = [\n\tnew FormatInfo(\"a\", false, true),\n\tnew FormatInfo(\"aai\", true, true),\n\tnew FormatInfo(\"ai\", false, true),\n\tnew FormatInfo(\"art\", false, true),\n\tnew FormatInfo(\"avs\", true, true),\n\tnew FormatInfo(\"b\", false, true),\n\tnew FormatInfo(\"bgr\", false, true),\n\tnew FormatInfo(\"bgra\", false, true),\n\tnew FormatInfo(\"bgro\", false, true),\n\tnew FormatInfo(\"bmp2\", true, true),\n\tnew FormatInfo(\"bmp3\", true, true),\n\tnew FormatInfo(\"brf\", false, true),\n\tnew FormatInfo(\"cal\", false, true),\n\tnew FormatInfo(\"cals\", false, true),\n\tnew FormatInfo(\"cin\", true, true), // not ideal (made the image more \"shadowy\"?)\n\tnew FormatInfo(\"cip\", false, true),\n\tnew FormatInfo(\"cmyk\", false, true),\n\tnew FormatInfo(\"cmyka\", false, true),\n\tnew FormatInfo(\"dcx\", true, true),\n\tnew FormatInfo(\"dds\", true, true),\n\tnew FormatInfo(\"dpx\", true, true),\n\tnew FormatInfo(\"dxt1\", true, true),\n\tnew FormatInfo(\"dxt5\", true, true),\n\tnew FormatInfo(\"epdf\", false, true),\n\tnew FormatInfo(\"epi\", false, true),\n\tnew FormatInfo(\"eps2\", false, true),\n\tnew FormatInfo(\"eps3\", false, true),\n\tnew FormatInfo(\"epsf\", false, true),\n\tnew FormatInfo(\"epsi\", false, true),\n\tnew FormatInfo(\"ept\", false, true),\n\tnew FormatInfo(\"ept2\", false, true),\n\tnew FormatInfo(\"ept3\", false, true),\n\tnew FormatInfo(\"exr\", true, true),\n\tnew FormatInfo(\"farbfeld\", true, true),\n\tnew FormatInfo(\"fax\", true, true), // not ideal (image became super long for some reason)\n\tnew FormatInfo(\"ff\", true, true),\n\tnew FormatInfo(\"fit\", true, true), // not ideal (grayscale)\n\tnew FormatInfo(\"fits\", true, true), // not ideal (grayscale)\n\tnew FormatInfo(\"fl32\", true, true),\n\tnew FormatInfo(\"fts\", true, true), // not ideal (grayscale)\n\tnew FormatInfo(\"ftxt\", false, true),\n\tnew FormatInfo(\"g\", false, true),\n\tnew FormatInfo(\"g3\", true, true), // not ideal (image became super long for some reason)\n\tnew FormatInfo(\"g4\", false, true),\n\tnew FormatInfo(\"gif87\", true, true),\n\tnew FormatInfo(\"gray\", false, true),\n\tnew FormatInfo(\"graya\", false, true),\n\tnew FormatInfo(\"group4\", false, true),\n\tnew FormatInfo(\"hrz\", true, true),\n\tnew FormatInfo(\"icb\", true, true),\n\tnew FormatInfo(\"icon\", true, true),\n\tnew FormatInfo(\"info\", false, true),\n\tnew FormatInfo(\"ipl\", true, true),\n\tnew FormatInfo(\"isobrl\", false, true),\n\tnew FormatInfo(\"isobrl6\", false, true),\n\tnew FormatInfo(\"j2c\", true, true),\n\tnew FormatInfo(\"j2k\", true, true),\n\tnew FormatInfo(\"jng\", true, true),\n\tnew FormatInfo(\"jp2\", true, true),\n\tnew FormatInfo(\"jpc\", true, true),\n\tnew FormatInfo(\"jpm\", true, true),\n\tnew FormatInfo(\"jps\", true, true),\n\t//new FormatInfo(\"json\", false, true),\n\tnew FormatInfo(\"map\", false, true),\n\tnew FormatInfo(\"miff\", true, true),\n\tnew FormatInfo(\"mng\", true, true),\n\tnew FormatInfo(\"mono\", false, true),\n\tnew FormatInfo(\"mtv\", true, true),\n\tnew FormatInfo(\"o\", false, true),\n\tnew FormatInfo(\"otb\", true, true), // not ideal (completely black and white - maybe format is like that)\n\tnew FormatInfo(\"pal\", false, true),\n\tnew FormatInfo(\"palm\", true, true), // not ideal (screwed up colours)\n\tnew FormatInfo(\"pam\", true, true),\n\tnew FormatInfo(\"pcd\", true, true), // not ideal (turned big, bg orange, and colour just shifted? - maybe format)\n\tnew FormatInfo(\"pcds\", true, true), // not ideal (turned big, bg orange, and colour just shifted? - maybe format)\n\tnew FormatInfo(\"pcl\", false, true),\n\tnew FormatInfo(\"pct\", true, true),\n\tnew FormatInfo(\"pcx\", true, true),\n\tnew FormatInfo(\"pdb\", true, true), // not ideal (completely black and white - maybe format is like that)\n\t// new FormatInfo(\"pdf\", false, true),\n\t// new FormatInfo(\"pdfa\", false, true),\n\tnew FormatInfo(\"pgx\", true, true), // not ideal (grayscale - maybe format is like that)\n\tnew FormatInfo(\"phm\", true, true),\n\tnew FormatInfo(\"picon\", true, true), // not ideal (smudged out colours - format probably)\n\tnew FormatInfo(\"pict\", true, true),\n\tnew FormatInfo(\"pjpeg\", true, true),\n\tnew FormatInfo(\"png00\", true, true),\n\tnew FormatInfo(\"png24\", true, true),\n\tnew FormatInfo(\"png32\", true, true),\n\tnew FormatInfo(\"png48\", true, true),\n\tnew FormatInfo(\"png64\", true, true),\n\tnew FormatInfo(\"png8\", true, true),\n\tnew FormatInfo(\"ps\", false, true),\n\tnew FormatInfo(\"ps1\", false, true),\n\tnew FormatInfo(\"ps2\", false, true),\n\tnew FormatInfo(\"ps3\", false, true),\n\tnew FormatInfo(\"psb\", true, true),\n\tnew FormatInfo(\"ptif\", true, true),\n\tnew FormatInfo(\"qoi\", true, true),\n\tnew FormatInfo(\"r\", false, true),\n\tnew FormatInfo(\"ras\", true, true),\n\tnew FormatInfo(\"rgb\", false, true),\n\tnew FormatInfo(\"rgba\", false, true),\n\tnew FormatInfo(\"rgbo\", false, true),\n\tnew FormatInfo(\"rgf\", true, true), // not ideal (completely black and white - maybe format is like that)\n\tnew FormatInfo(\"sgi\", true, true),\n\tnew FormatInfo(\"six\", true, true),\n\tnew FormatInfo(\"sixel\", true, true),\n\tnew FormatInfo(\"sparse-color\", false, true),\n\tnew FormatInfo(\"strimg\", false, true),\n\tnew FormatInfo(\"sun\", true, true),\n\tnew FormatInfo(\"svgz\", false, true),\n\tnew FormatInfo(\"tga\", true, true),\n\tnew FormatInfo(\"tiff64\", true, true),\n\t//new FormatInfo(\"txt\", true, true),\n\tnew FormatInfo(\"ubrl\", false, true),\n\tnew FormatInfo(\"ubrl6\", false, true),\n\tnew FormatInfo(\"uil\", false, true),\n\tnew FormatInfo(\"uyvy\", false, true),\n\tnew FormatInfo(\"vda\", true, true),\n\tnew FormatInfo(\"vicar\", true, true), // not ideal (grayscale - maybe format is like that)\n\tnew FormatInfo(\"viff\", true, true),\n\tnew FormatInfo(\"vips\", true, true),\n\tnew FormatInfo(\"vst\", true, true),\n\tnew FormatInfo(\"wbmp\", true, true), // not ideal (completely black and white - maybe format is like that)\n\tnew FormatInfo(\"wpg\", true, true),\n\tnew FormatInfo(\"xbm\", true, true), // not ideal (completely black and white - maybe format is like that)\n\tnew FormatInfo(\"xpm\", true, true),\n\tnew FormatInfo(\"xv\", true, true),\n\t//new FormatInfo(\"yaml\", false, true),\n\tnew FormatInfo(\"ycbcr\", false, true),\n\tnew FormatInfo(\"ycbcra\", false, true),\n\tnew FormatInfo(\"yuv\", false, true),\n];\n"
  },
  {
    "path": "src/lib/converters/magick.svelte.ts",
    "content": "import { browser } from \"$app/environment\";\nimport { error, log } from \"$lib/util/logger\";\nimport { m } from \"$lib/paraglide/messages\";\nimport { VertFile, type WorkerMessage } from \"$lib/types\";\nimport MagickWorker from \"$lib/workers/magick?worker&url\";\nimport { Converter, FormatInfo } from \"./converter.svelte\";\nimport { imageFormats } from \"./magick-automated\";\nimport { Settings } from \"$lib/sections/settings/index.svelte\";\nimport magickWasm from \"@imagemagick/magick-wasm/magick.wasm?url\";\nimport { ToastManager } from \"$lib/util/toast.svelte\";\n\nexport class MagickConverter extends Converter {\n\tpublic name = \"imagemagick\";\n\tpublic ready = $state(false);\n\tpublic wasm: ArrayBuffer = null!;\n\n\tprivate activeConversions = new Map<string, Worker>();\n\n\tpublic supportedFormats = [\n\t\t// manually tested formats\n\t\tnew FormatInfo(\"png\", true, true),\n\t\tnew FormatInfo(\"jpeg\", true, true),\n\t\tnew FormatInfo(\"jpg\", true, true),\n\t\tnew FormatInfo(\"webp\", true, true),\n\t\tnew FormatInfo(\"gif\", true, true),\n\t\tnew FormatInfo(\"svg\", true, true),\n\t\tnew FormatInfo(\"jxl\", true, true),\n\t\tnew FormatInfo(\"avif\", true, true),\n\t\tnew FormatInfo(\"heic\", true, false), // seems to be unreliable? HEIC/HEIF is very weird if it will actually work\n\t\tnew FormatInfo(\"heif\", true, false),\n\t\t// TODO: .ico files can encode multiple images at various\n\t\t// sizes, bitdepths, etc. we should support that in future\n\t\tnew FormatInfo(\"ico\", true, true),\n\t\tnew FormatInfo(\"bmp\", true, true),\n\t\tnew FormatInfo(\"cur\", true, true),\n\t\tnew FormatInfo(\"ani\", true, false),\n\t\tnew FormatInfo(\"icns\", true, false),\n\t\tnew FormatInfo(\"nef\", true, false),\n\t\tnew FormatInfo(\"cr2\", true, false),\n\t\tnew FormatInfo(\"hdr\", true, true),\n\t\tnew FormatInfo(\"jpe\", true, true),\n\t\tnew FormatInfo(\"mat\", true, true),\n\t\tnew FormatInfo(\"pbm\", true, true),\n\t\tnew FormatInfo(\"pfm\", true, true),\n\t\tnew FormatInfo(\"pgm\", true, true),\n\t\tnew FormatInfo(\"pnm\", true, true),\n\t\tnew FormatInfo(\"ppm\", true, true),\n\t\tnew FormatInfo(\"tiff\", true, true),\n\t\tnew FormatInfo(\"jfif\", true, true),\n\t\tnew FormatInfo(\"eps\", false, true),\n\t\tnew FormatInfo(\"psd\", true, true),\n\n\t\t// raw camera formats\n\t\tnew FormatInfo(\"arw\", true, false),\n\t\tnew FormatInfo(\"tif\", true, true),\n\t\tnew FormatInfo(\"dng\", true, false),\n\t\tnew FormatInfo(\"xcf\", true, false),\n\t\tnew FormatInfo(\"rw2\", true, false),\n\t\tnew FormatInfo(\"raf\", true, false),\n\t\tnew FormatInfo(\"orf\", true, false),\n\t\tnew FormatInfo(\"pef\", true, false),\n\t\tnew FormatInfo(\"mos\", true, false),\n\t\tnew FormatInfo(\"raw\", true, false),\n\t\tnew FormatInfo(\"dcr\", true, false),\n\t\tnew FormatInfo(\"crw\", true, false),\n\t\tnew FormatInfo(\"cr3\", true, false),\n\t\tnew FormatInfo(\"3fr\", true, false),\n\t\tnew FormatInfo(\"erf\", true, false),\n\t\tnew FormatInfo(\"mrw\", true, false),\n\t\tnew FormatInfo(\"mef\", true, false),\n\t\tnew FormatInfo(\"nrw\", true, false),\n\t\tnew FormatInfo(\"srw\", true, false),\n\t\tnew FormatInfo(\"sr2\", true, false),\n\t\tnew FormatInfo(\"srf\", true, false),\n\n\t\t// formats added from maya's somewhat automated testing\n\t\t...imageFormats,\n\t];\n\n\tpublic readonly reportsProgress = false;\n\n\tconstructor() {\n\t\tsuper();\n\t\tlog([\"converters\", this.name], `created converter`);\n\t\tif (!browser) return;\n\t\tthis.initializeWasm();\n\t}\n\n\tprivate async initializeWasm() {\n\t\ttry {\n\t\t\tthis.status = \"downloading\";\n\t\t\tconst response = await fetch(magickWasm);\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Failed to fetch WASM: ${response.status} ${response.statusText}`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tthis.wasm = await response.arrayBuffer();\n\t\t\tthis.status = \"ready\";\n\t\t} catch (err) {\n\t\t\tthis.status = \"error\";\n\t\t\terror(\n\t\t\t\t[\"converters\", this.name],\n\t\t\t\t`Failed to load ImageMagick WASM: ${err}`,\n\t\t\t);\n\n\t\t\tToastManager.add({\n\t\t\t\ttype: \"error\",\n\t\t\t\tmessage: m[\"workers.errors.magick\"](),\n\t\t\t});\n\t\t}\n\t}\n\n\tpublic async convert(\n\t\tinput: VertFile,\n\t\tto: string,\n\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\t\t...args: any[]\n\t): Promise<VertFile> {\n\t\tlet compression: number | undefined = args.at(0);\n\t\tif (!compression) {\n\t\t\tcompression = Settings.instance.settings.magickQuality ?? 100;\n\t\t\tlog(\n\t\t\t\t[\"converters\", this.name],\n\t\t\t\t`using user setting for quality: ${compression}%`,\n\t\t\t);\n\t\t}\n\t\tlog([\"converters\", this.name], `converting ${input.name} to ${to}`);\n\n\t\t// handle converting from SVG manually because magick-wasm doesn't support it\n\t\tif (input.from === \".svg\") {\n\t\t\ttry {\n\t\t\t\tconst blob = await this.svgToImage(input);\n\t\t\t\tconst pngFile = new VertFile(\n\t\t\t\t\tnew File([blob], input.name.replace(/\\.svg$/i, \".png\")),\n\t\t\t\t\tinput.to,\n\t\t\t\t);\n\t\t\t\tif (to === \".png\") return pngFile; // if target is png, return it directly\n\t\t\t\treturn await this.convert(pngFile, to, ...args); // otherwise, recursively convert png to user's target format\n\t\t\t} catch (err) {\n\t\t\t\terror(\n\t\t\t\t\t[\"converters\", this.name],\n\t\t\t\t\t`SVG conversion failed: ${err}`,\n\t\t\t\t);\n\t\t\t\tthrow err;\n\t\t\t}\n\t\t}\n\n\t\tconst worker = new Worker(MagickWorker, {\n\t\t\ttype: \"module\",\n\t\t});\n\t\tthis.activeConversions.set(input.id, worker);\n\n\t\ttry {\n\t\t\tawait Promise.race([\n\t\t\t\tthis.waitForMessage(worker, \"ready\"),\n\t\t\t\tnew Promise((_, reject) =>\n\t\t\t\t\tsetTimeout(\n\t\t\t\t\t\t() =>\n\t\t\t\t\t\t\treject(\n\t\t\t\t\t\t\t\tnew Error(\n\t\t\t\t\t\t\t\t\t\"Magick worker ready timeout after 10 seconds\",\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t10000,\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t]);\n\n\t\t\tconst loadMsg: WorkerMessage = {\n\t\t\t\ttype: \"load\",\n\t\t\t\twasm: this.wasm,\n\t\t\t\tid: input.id,\n\t\t\t};\n\t\t\tworker.postMessage(loadMsg);\n\n\t\t\tawait Promise.race([\n\t\t\t\tthis.waitForMessage(worker, \"loaded\"),\n\t\t\t\tnew Promise((_, reject) =>\n\t\t\t\t\tsetTimeout(\n\t\t\t\t\t\t() =>\n\t\t\t\t\t\t\treject(\n\t\t\t\t\t\t\t\tnew Error(\n\t\t\t\t\t\t\t\t\t\"Magick worker initialization timeout after 30 seconds\",\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t30000,\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t]);\n\n\t\t\t// every other format handled by magick worker\n\t\t\tconst keepMetadata: boolean =\n\t\t\t\tSettings.instance.settings.metadata ?? true;\n\t\t\tlog([\"converters\", this.name], `keep metadata: ${keepMetadata}`);\n\t\t\tconst convertMsg: WorkerMessage = {\n\t\t\t\ttype: \"convert\",\n\t\t\t\tid: input.id,\n\t\t\t\tinput: {\n\t\t\t\t\tfile: input.file,\n\t\t\t\t\tname: input.name,\n\t\t\t\t\tfrom: input.from,\n\t\t\t\t\tto: input.to,\n\t\t\t\t},\n\t\t\t\tto,\n\t\t\t\tcompression,\n\t\t\t\tkeepMetadata,\n\t\t\t};\n\t\t\tworker.postMessage(convertMsg);\n\n\t\t\tconst res = await this.waitForMessage(worker);\n\t\t\tif (res.type === \"finished\") {\n\t\t\t\tlog(\n\t\t\t\t\t[\"converters\", this.name],\n\t\t\t\t\t`converted ${input.name} to ${to}`,\n\t\t\t\t);\n\t\t\t\treturn new VertFile(\n\t\t\t\t\tnew File([res.output as unknown as BlobPart], input.name),\n\t\t\t\t\tres.zip ? \".zip\" : to,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (res.type === \"error\") {\n\t\t\t\tthrow new Error(res.error);\n\t\t\t}\n\n\t\t\tthrow new Error(\"Unknown message type\");\n\t\t} finally {\n\t\t\tthis.activeConversions.delete(input.id);\n\t\t\tworker.terminate();\n\t\t}\n\t}\n\n\tpublic async cancel(input: VertFile): Promise<void> {\n\t\tconst worker = this.activeConversions.get(input.id);\n\t\tif (!worker) {\n\t\t\terror(\n\t\t\t\t[\"converters\", this.name],\n\t\t\t\t`no active conversion found for file ${input.name}`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tlog(\n\t\t\t[\"converters\", this.name],\n\t\t\t`cancelling conversion for file ${input.name}`,\n\t\t);\n\n\t\tworker.terminate();\n\t\tthis.activeConversions.delete(input.id);\n\t}\n\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tprivate waitForMessage(worker: Worker, type?: string): Promise<any> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst onMessage = (e: MessageEvent) => {\n\t\t\t\tif (type && e.data.type === type) {\n\t\t\t\t\tworker.removeEventListener(\"message\", onMessage);\n\t\t\t\t\tworker.removeEventListener(\"error\", onError);\n\t\t\t\t\tresolve(e.data);\n\t\t\t\t} else if (!type) {\n\t\t\t\t\tworker.removeEventListener(\"message\", onMessage);\n\t\t\t\t\tworker.removeEventListener(\"error\", onError);\n\t\t\t\t\tresolve(e.data);\n\t\t\t\t} else if (e.data.type === \"error\") {\n\t\t\t\t\tworker.removeEventListener(\"message\", onMessage);\n\t\t\t\t\tworker.removeEventListener(\"error\", onError);\n\t\t\t\t\treject(new Error(e.data.error));\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tconst onError = (e: ErrorEvent) => {\n\t\t\t\tworker.removeEventListener(\"message\", onMessage);\n\t\t\t\tworker.removeEventListener(\"error\", onError);\n\t\t\t\treject(new Error(`Worker error: ${e.message}`));\n\t\t\t};\n\n\t\t\tworker.addEventListener(\"message\", onMessage);\n\t\t\tworker.addEventListener(\"error\", onError);\n\t\t});\n\t}\n\n\tprivate async svgToImage(input: VertFile): Promise<Blob> {\n\t\tlog([\"converters\", this.name], `converting SVG to image (PNG)`);\n\n\t\tconst svgText = await input.file.text();\n\t\tconst svgBlob = new Blob([svgText], { type: \"image/svg+xml\" });\n\t\tconst svgUrl = URL.createObjectURL(svgBlob);\n\n\t\tconst canvas = document.createElement(\"canvas\");\n\t\tconst ctx = canvas.getContext(\"2d\");\n\t\tif (!ctx) throw new Error(\"Failed to get canvas context\");\n\n\t\tconst img = new Image();\n\n\t\t// try to extract dimensions from SVG, and if not fallback to default\n\t\tlet width = 512;\n\t\tlet height = 512;\n\t\tconst widthMatch = svgText.match(/width=[\"'](\\d+)[\"']/);\n\t\tconst heightMatch = svgText.match(/height=[\"'](\\d+)[\"']/);\n\t\tconst viewBoxMatch = svgText.match(\n\t\t\t/viewBox=[\"'][^\"']*\\s+(\\d+)\\s+(\\d+)[\"']/,\n\t\t);\n\n\t\tif (widthMatch && heightMatch) {\n\t\t\twidth = parseInt(widthMatch[1]);\n\t\t\theight = parseInt(heightMatch[1]);\n\t\t} else if (viewBoxMatch) {\n\t\t\twidth = parseInt(viewBoxMatch[1]);\n\t\t\theight = parseInt(viewBoxMatch[2]);\n\t\t}\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\timg.onload = () => {\n\t\t\t\ttry {\n\t\t\t\t\tcanvas.width = img.naturalWidth || width;\n\t\t\t\t\tcanvas.height = img.naturalHeight || height;\n\n\t\t\t\t\tctx.drawImage(img, 0, 0);\n\n\t\t\t\t\tcanvas.toBlob((blob) => {\n\t\t\t\t\t\tURL.revokeObjectURL(svgUrl);\n\t\t\t\t\t\tif (blob) {\n\t\t\t\t\t\t\tresolve(blob);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treject(\n\t\t\t\t\t\t\t\tnew Error(\"Failed to convert canvas to Blob\"),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}, \"image/png\");\n\t\t\t\t} catch (err) {\n\t\t\t\t\tURL.revokeObjectURL(svgUrl);\n\t\t\t\t\treject(err);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\timg.onerror = () => {\n\t\t\t\tURL.revokeObjectURL(svgUrl);\n\t\t\t\treject(new Error(\"Failed to load SVG image\"));\n\t\t\t};\n\n\t\t\timg.src = svgUrl;\n\t\t});\n\t}\n}\n"
  },
  {
    "path": "src/lib/converters/pandoc.svelte.ts",
    "content": "import { VertFile, type WorkerMessage } from \"$lib/types\";\nimport { Converter, FormatInfo } from \"./converter.svelte\";\nimport { browser } from \"$app/environment\";\nimport PandocWorker from \"$lib/workers/pandoc?worker&url\";\nimport { error, log } from \"$lib/util/logger\";\nimport { ToastManager } from \"$lib/util/toast.svelte\";\nimport { m } from \"$lib/paraglide/messages\";\n\nexport class PandocConverter extends Converter {\n\tpublic name = \"pandoc\";\n\tpublic ready = $state(false);\n\tpublic wasm: ArrayBuffer = null!;\n\n\tprivate activeConversions = new Map<string, Worker>();\n\n\tconstructor() {\n\t\tsuper();\n\t\tif (!browser) return;\n\t\t(async () => {\n\t\t\ttry {\n\t\t\t\tthis.status = \"downloading\";\n\t\t\t\tthis.wasm = await fetch(\"/pandoc.wasm\").then((r) =>\n\t\t\t\t\tr.arrayBuffer(),\n\t\t\t\t);\n\n\t\t\t\tthis.status = \"ready\";\n\t\t\t} catch (err) {\n\t\t\t\tthis.status = \"error\";\n\t\t\t\terror(\n\t\t\t\t\t[\"converters\", this.name],\n\t\t\t\t\t`Failed to load Pandoc worker: ${err}`,\n\t\t\t\t);\n\t\t\t\tToastManager.add({\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t\tmessage: m[\"workers.errors.pandoc\"](),\n\t\t\t\t});\n\t\t\t}\n\t\t})();\n\t}\n\n\tpublic async convert(file: VertFile, to: string): Promise<VertFile> {\n\t\tconst worker = new Worker(PandocWorker, {\n\t\t\ttype: \"module\",\n\t\t});\n\n\t\tthis.activeConversions.set(file.id, worker);\n\n\t\tconst loadMsg: WorkerMessage = {\n\t\t\ttype: \"load\",\n\t\t\twasm: this.wasm,\n\t\t\tid: file.id,\n\t\t};\n\t\tworker.postMessage(loadMsg);\n\t\tawait waitForMessage(worker, \"loaded\");\n\t\tconst convertMsg: WorkerMessage = {\n\t\t\ttype: \"convert\",\n\t\t\tto,\n\t\t\tinput: {\n\t\t\t\tfile: file.file,\n\t\t\t\tname: file.name,\n\t\t\t\tfrom: file.from,\n\t\t\t\tto,\n\t\t\t},\n\t\t\tcompression: null,\n\t\t\tid: file.id,\n\t\t};\n\t\tworker.postMessage(convertMsg);\n\t\tconst result = await waitForMessage(worker);\n\t\tif (result.type === \"error\") {\n\t\t\tworker.terminate();\n\t\t\t// throw new Error(result.error);\n\t\t\tconst error = result.error.toString();\n\t\t\tswitch (result.errorKind) {\n\t\t\t\tcase \"PandocUnknownReaderError\": {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`${file.from} is not a supported input format for documents.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tcase \"PandocUnknownWriterError\": {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`${to} is not a supported output format for documents.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tcase \"PandocParseError\": {\n\t\t\t\t\tif (error.includes(\"JSON missing pandoc-api-version\")) {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t`This JSON file is not a pandoc-converted JSON file. It must be converted with pandoc / VERT to be converted again.`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// eslint-disable-next-line no-fallthrough\n\t\t\t\tdefault:\n\t\t\t\t\tif (result.errorKind)\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t`[${result.errorKind}] ${result.error}`,\n\t\t\t\t\t\t);\n\t\t\t\t\telse throw new Error(result.error);\n\t\t\t}\n\t\t}\n\n\t\tif (!to.startsWith(\".\")) to = `.${to}`;\n\t\tthis.activeConversions.delete(file.id);\n\t\tworker.terminate();\n\t\treturn new VertFile(\n\t\t\tnew File([result.output], file.name),\n\t\t\tresult.isZip ? \".zip\" : to,\n\t\t);\n\t}\n\n\tpublic async cancel(input: VertFile): Promise<void> {\n\t\tconst worker = this.activeConversions.get(input.id);\n\t\tif (!worker) {\n\t\t\terror(\n\t\t\t\t[\"converters\", this.name],\n\t\t\t\t`no active conversion found for file ${input.name}`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tlog(\n\t\t\t[\"converters\", this.name],\n\t\t\t`cancelling conversion for file ${input.name}`,\n\t\t);\n\n\t\tworker.terminate();\n\t\tthis.activeConversions.delete(input.id);\n\t}\n\n\tpublic supportedFormats = [\n\t\tnew FormatInfo(\"docx\", true, true),\n\t\tnew FormatInfo(\"doc\", true, true),\n\t\tnew FormatInfo(\"md\", true, true),\n\t\tnew FormatInfo(\"html\", true, true),\n\t\tnew FormatInfo(\"rtf\", true, true),\n\t\tnew FormatInfo(\"csv\", true, true),\n\t\tnew FormatInfo(\"tsv\", true, true),\n\t\tnew FormatInfo(\"json\", true, true), // must be a pandoc-converted json\n\t\tnew FormatInfo(\"rst\", true, true),\n\t\tnew FormatInfo(\"epub\", true, true),\n\t\tnew FormatInfo(\"odt\", true, true),\n\t\tnew FormatInfo(\"docbook\", true, true),\n\t];\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction waitForMessage(worker: Worker, type?: string): Promise<any> {\n\treturn new Promise((resolve) => {\n\t\tconst onMessage = (e: MessageEvent) => {\n\t\t\tif (type && e.data.type === type) {\n\t\t\t\tworker.removeEventListener(\"message\", onMessage);\n\t\t\t\tresolve(e.data);\n\t\t\t} else {\n\t\t\t\tworker.removeEventListener(\"message\", onMessage);\n\t\t\t\tresolve(e.data);\n\t\t\t}\n\t\t};\n\t\tworker.addEventListener(\"message\", onMessage);\n\t});\n}\n"
  },
  {
    "path": "src/lib/converters/vertd.svelte.ts",
    "content": "import VertdErrorComponent from \"$lib/components/functional/VertdError.svelte\";\nimport { error, log } from \"$lib/util/logger\";\nimport { m } from \"$lib/paraglide/messages\";\nimport { Settings } from \"$lib/sections/settings/index.svelte\";\nimport { VertdInstance } from \"$lib/sections/settings/vertdSettings.svelte\";\nimport { VertFile } from \"$lib/types\";\nimport { Converter, FormatInfo } from \"./converter.svelte\";\nimport { PUB_DISABLE_FAILURE_BLOCKS } from \"$env/static/public\";\n\ninterface UploadResponse {\n\tid: string;\n\tauth: string;\n\tfrom: string;\n\tto: null;\n\tcompleted: false;\n\ttotalFrames: number;\n}\n\ninterface RouteRequestMap {\n\t\"/api/keep\": {\n\t\tid: string;\n\t\ttoken: string;\n\t};\n}\n\ninterface RouteResponseMap {\n\t\"/api/upload\": UploadResponse;\n\t\"/api/version\": string;\n\t\"/api/keep\": void;\n}\n\nexport const vertdFetch: {\n\t<U extends keyof RouteRequestMap>(\n\t\turl: U,\n\t\toptions: RequestInit,\n\t\tbody: RouteRequestMap[U],\n\t): Promise<RouteResponseMap[U]>;\n\t<U extends Exclude<keyof RouteResponseMap, keyof RouteRequestMap>>(\n\t\turl: U,\n\t\toptions: RequestInit,\n\t): Promise<RouteResponseMap[U]>;\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n} = async (url: any, options: RequestInit, body?: any) => {\n\tconst domain = await VertdInstance.instance.url();\n\n\t// if there is a body, insert a Content-Type: application/json header\n\tif (body) {\n\t\toptions.headers = {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t...(options.headers || {}),\n\t\t};\n\t\toptions.body = JSON.stringify(body);\n\t}\n\n\tconst res = await fetch(domain + url, options);\n\n\tconst text = await res.text();\n\tlet json = null;\n\ttry {\n\t\tjson = JSON.parse(text);\n\t} catch {\n\t\tthrow new Error(text);\n\t}\n\n\tif (json.type === \"error\") {\n\t\tthrow new Error(json.data);\n\t}\n\n\treturn json.data;\n};\n\n// ws types\n\nexport type ConversionSpeed =\n\t| \"verySlow\"\n\t| \"slower\"\n\t| \"slow\"\n\t| \"medium\"\n\t| \"fast\"\n\t| \"ultraFast\";\n\ninterface StartJobMessage {\n\ttype: \"startJob\";\n\tdata: {\n\t\ttoken: string;\n\t\tjobId: string;\n\t\tto: string;\n\t\tspeed: ConversionSpeed;\n\t\tkeepMetadata: boolean;\n\t};\n}\n\ninterface ErrorMessage {\n\ttype: \"error\";\n\tdata: {\n\t\tmessage: string;\n\t};\n}\n\ninterface ProgressMessage {\n\ttype: \"progressUpdate\";\n\tdata: ProgressData;\n}\n\ninterface CompletedMessage {\n\ttype: \"jobFinished\";\n\tdata: {\n\t\tjobId: string;\n\t};\n}\n\ninterface CancelJobMessage {\n\ttype: \"cancelJob\";\n\tdata: {\n\t\tjobId: string;\n\t\ttoken: string;\n\t};\n}\n\ninterface JobCancelledMessage {\n\ttype: \"jobCancelled\";\n\tdata: {\n\t\tjobId: string;\n\t};\n}\n\ninterface FpsProgress {\n\ttype: \"fps\";\n\tdata: number;\n}\n\ninterface FrameProgress {\n\ttype: \"frame\";\n\tdata: number;\n}\n\ntype ProgressData = FpsProgress | FrameProgress;\n\ntype VertdMessage =\n\t| StartJobMessage\n\t| ErrorMessage\n\t| ProgressMessage\n\t| CancelJobMessage\n\t| JobCancelledMessage\n\t| CompletedMessage;\n\nconst progressEstimates = {\n\tupload: 25,\n\tconvert: 50,\n\tdownload: 25,\n};\n\nconst progressEstimate = (\n\tprogress: number,\n\ttype: keyof typeof progressEstimates,\n) => {\n\tconst previousValues = Object.values(progressEstimates)\n\t\t.filter((_, i) => i < Object.keys(progressEstimates).indexOf(type))\n\t\t.reduce((a, b) => a + b, 0);\n\treturn progress * progressEstimates[type] + previousValues;\n};\n\nconst uploadFile = async (file: VertFile): Promise<UploadResponse> => {\n\tconst apiUrl = await VertdInstance.instance.url();\n\tconst formData = new FormData();\n\tformData.append(\"file\", file.file, file.name);\n\tconst xhr = new XMLHttpRequest();\n\txhr.open(\"POST\", `${apiUrl}/api/upload`, true);\n\n\treturn new Promise((resolve, reject) => {\n\t\txhr.upload.addEventListener(\"progress\", (e) => {\n\t\t\tconsole.log(e);\n\t\t\tif (e.lengthComputable) {\n\t\t\t\tfile.progress = progressEstimate(e.loaded / e.total, \"upload\");\n\t\t\t}\n\t\t});\n\n\t\tconsole.log(\"meow\");\n\n\t\txhr.onload = () => {\n\t\t\ttry {\n\t\t\t\tconsole.log(\"xhr.responseText\");\n\t\t\t\tconst res = JSON.parse(xhr.responseText);\n\t\t\t\tif (res.type === \"error\") {\n\t\t\t\t\treject(res.data);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tresolve(res.data);\n\t\t\t} catch {\n\t\t\t\tconsole.log(xhr.responseText);\n\t\t\t\treject(xhr.statusText);\n\t\t\t}\n\t\t};\n\n\t\txhr.onerror = () => {\n\t\t\tconsole.log(xhr.statusText);\n\t\t\treject(xhr.statusText);\n\t\t};\n\n\t\txhr.send(formData);\n\t\tconsole.log(\"sent!\");\n\t});\n};\n\nconst downloadFile = async (url: string, file: VertFile): Promise<Blob> => {\n\tconst xhr = new XMLHttpRequest();\n\txhr.open(\"GET\", url, true);\n\txhr.responseType = \"blob\";\n\n\treturn new Promise((resolve, reject) => {\n\t\txhr.addEventListener(\"progress\", (e) => {\n\t\t\tif (e.lengthComputable) {\n\t\t\t\tfile.progress = progressEstimate(\n\t\t\t\t\te.loaded / e.total,\n\t\t\t\t\t\"download\",\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\n\t\txhr.onload = () => {\n\t\t\tif (xhr.status === 200) {\n\t\t\t\tresolve(xhr.response);\n\t\t\t} else {\n\t\t\t\treject(xhr.statusText);\n\t\t\t}\n\t\t};\n\n\t\txhr.onerror = () => {\n\t\t\treject(xhr.statusText);\n\t\t};\n\n\t\txhr.send();\n\t});\n};\n\nexport class VertdConverter extends Converter {\n\tpublic name = \"vertd\";\n\tpublic ready = $state(false);\n\tpublic reportsProgress = true;\n\n\tprivate activeConversions = new Map<\n\t\tstring,\n\t\t{\n\t\t\tws: WebSocket;\n\t\t\tjobId: string;\n\t\t\ttoken: string;\n\t\t}\n\t>();\n\n\tpublic supportedFormats = [\n\t\tnew FormatInfo(\"mkv\", true, true),\n\t\tnew FormatInfo(\"mp4\", true, true),\n\t\tnew FormatInfo(\"webm\", true, true),\n\t\tnew FormatInfo(\"avi\", true, true),\n\t\tnew FormatInfo(\"wmv\", true, true),\n\t\tnew FormatInfo(\"mov\", true, true),\n\t\tnew FormatInfo(\"gif\", true, true),\n\t\tnew FormatInfo(\"mts\", true, true),\n\t\tnew FormatInfo(\"ts\", true, true),\n\t\tnew FormatInfo(\"m2ts\", true, true),\n\t\tnew FormatInfo(\"mpg\", true, true),\n\t\tnew FormatInfo(\"mpeg\", true, true),\n\t\tnew FormatInfo(\"flv\", true, true),\n\t\tnew FormatInfo(\"f4v\", true, true),\n\t\tnew FormatInfo(\"vob\", true, true),\n\t\tnew FormatInfo(\"m4v\", true, true),\n\t\tnew FormatInfo(\"3gp\", true, true),\n\t\tnew FormatInfo(\"3g2\", true, true),\n\t\tnew FormatInfo(\"mxf\", true, true),\n\t\tnew FormatInfo(\"ogv\", true, true),\n\t\tnew FormatInfo(\"rm\", true, false),\n\t\tnew FormatInfo(\"rmvb\", true, false),\n\t\tnew FormatInfo(\"h264\", true, true),\n\t\tnew FormatInfo(\"divx\", true, true),\n\t\tnew FormatInfo(\"swf\", true, true),\n\t\tnew FormatInfo(\"amv\", true, true),\n\t\tnew FormatInfo(\"asf\", true, true),\n\t\tnew FormatInfo(\"nut\", true, true),\n\t];\n\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tprivate log: (...msg: any[]) => void = () => {};\n\n\tconstructor() {\n\t\tsuper();\n\t\tthis.log = (msg) => log([\"converters\", this.name], msg);\n\t\tthis.log(\"created converter\");\n\t\tthis.log(\"not rly sure how to implement this :P\");\n\t\tthis.status = \"ready\";\n\t}\n\n\tprivate blocked(hash: string): boolean {\n\t\tlet blockedHashes = Settings.instance.settings.vertdBlockedHashes;\n\n\t\t// ensure it's a map\n\t\t// this might fix the \"e.get\" isn't a function error, but i can't reproduce it\n\t\tif (!(blockedHashes instanceof Map) || blockedHashes === null) {\n\t\t\tblockedHashes = new Map(Object.entries(blockedHashes || {}));\n\t\t\tSettings.instance.settings.vertdBlockedHashes = blockedHashes;\n\t\t\tSettings.instance.save();\n\t\t}\n\n\t\tconst now = new Date();\n\t\tconst dates = blockedHashes.get(hash) || [];\n\t\tconst filteredDates = dates.filter(\n\t\t\t(date) => now.getTime() - date.getTime() < 60 * 60 * 1000,\n\t\t);\n\n\t\tif (filteredDates.length === 0) {\n\t\t\tblockedHashes.delete(hash);\n\t\t\treturn false;\n\t\t}\n\n\t\tblockedHashes.set(hash, filteredDates);\n\n\t\tSettings.instance.save();\n\n\t\treturn filteredDates.length >= 3;\n\t}\n\n\tprivate failure(hash: string): void {\n\t\tlet blockedHashes = Settings.instance.settings.vertdBlockedHashes;\n\n\t\t// same as above (blocked())\n\t\tif (!(blockedHashes instanceof Map) || blockedHashes === null) {\n\t\t\tblockedHashes = new Map(Object.entries(blockedHashes || {}));\n\t\t\tSettings.instance.settings.vertdBlockedHashes = blockedHashes;\n\t\t\tSettings.instance.save();\n\t\t}\n\n\t\tconst now = new Date();\n\t\tconst dates = blockedHashes.get(hash) || [];\n\t\tdates.push(now);\n\t\tblockedHashes.set(hash, dates);\n\t\tSettings.instance.save();\n\t}\n\n\tpublic async convert(input: VertFile, to: string): Promise<VertFile> {\n\t\tif (to.startsWith(\".\")) to = to.slice(1);\n\n\t\tlet hash: string;\n\t\tif (PUB_DISABLE_FAILURE_BLOCKS === \"false\") {\n\t\t\thash = await input.hash();\n\n\t\t\tif (this.blocked(hash)) {\n\t\t\t\tthis.log(`conversion blocked for file ${input.name}`);\n\t\t\t\tthrow new Error(\n\t\t\t\t\tm[\"convert.errors.vertd_ratelimit\"]({\n\t\t\t\t\t\tfilename: input.name,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tconst uploadRes = await uploadFile(input);\n\t\tconst apiUrl = await VertdInstance.instance.url();\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst protocol = apiUrl.startsWith(\"https\") ? \"wss:\" : \"ws:\";\n\t\t\tconst ws = new WebSocket(\n\t\t\t\t`${protocol}//${apiUrl.replace(\"http://\", \"\").replace(\"https://\", \"\")}/api/ws`,\n\t\t\t);\n\n\t\t\tthis.activeConversions.set(input.id, {\n\t\t\t\tws,\n\t\t\t\tjobId: uploadRes.id,\n\t\t\t\ttoken: uploadRes.auth,\n\t\t\t});\n\n\t\t\tws.onopen = () => {\n\t\t\t\tconst speed = Settings.instance.settings.vertdSpeed;\n\t\t\t\tconst keepMetadata = Settings.instance.settings.metadata;\n\t\t\t\tthis.log(\"opened ws connection to vertd\");\n\t\t\t\tconst msg: StartJobMessage = {\n\t\t\t\t\ttype: \"startJob\",\n\t\t\t\t\tdata: {\n\t\t\t\t\t\tjobId: uploadRes.id,\n\t\t\t\t\t\ttoken: uploadRes.auth,\n\t\t\t\t\t\tto,\n\t\t\t\t\t\tspeed,\n\t\t\t\t\t\tkeepMetadata,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t\tws.send(JSON.stringify(msg));\n\t\t\t\tthis.log(\"sent startJob message\");\n\t\t\t};\n\n\t\t\tws.onmessage = async (e) => {\n\t\t\t\tconst msg: VertdMessage = JSON.parse(e.data);\n\t\t\t\tthis.log(`received message ${msg.type}`);\n\t\t\t\tswitch (msg.type) {\n\t\t\t\t\tcase \"progressUpdate\": {\n\t\t\t\t\t\tconst data = msg.data;\n\t\t\t\t\t\tif (data.type !== \"frame\") break;\n\t\t\t\t\t\tconst frame = data.data;\n\t\t\t\t\t\tinput.progress = progressEstimate(\n\t\t\t\t\t\t\tframe / uploadRes.totalFrames,\n\t\t\t\t\t\t\t\"convert\",\n\t\t\t\t\t\t);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"jobFinished\": {\n\t\t\t\t\t\tthis.log(\"job finished\");\n\t\t\t\t\t\tws.close();\n\t\t\t\t\t\tthis.activeConversions.delete(input.id);\n\t\t\t\t\t\tconst url = `${apiUrl}/api/download/${msg.data.jobId}/${uploadRes.auth}`;\n\t\t\t\t\t\tthis.log(`downloading from ${url}`);\n\t\t\t\t\t\t// const res = await fetch(url).then((res) => res.blob());\n\t\t\t\t\t\tconst res = await downloadFile(url, input);\n\t\t\t\t\t\tresolve(new VertFile(new File([res], input.name), to));\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"jobCancelled\": {\n\t\t\t\t\t\tthis.log(\"job cancelled\");\n\t\t\t\t\t\tws.close();\n\t\t\t\t\t\tthis.activeConversions.delete(input.id);\n\t\t\t\t\t\treject(\"Conversion cancelled\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"error\": {\n\t\t\t\t\t\tthis.log(`error: ${msg.data.message}`);\n\t\t\t\t\t\tthis.activeConversions.delete(input.id);\n\t\t\t\t\t\tif (hash) this.failure(hash);\n\n\t\t\t\t\t\treject({\n\t\t\t\t\t\t\tcomponent: VertdErrorComponent,\n\t\t\t\t\t\t\tadditional: {\n\t\t\t\t\t\t\t\tjobId: uploadRes.id,\n\t\t\t\t\t\t\t\tauth: uploadRes.auth,\n\t\t\t\t\t\t\t\tfrom: input.from,\n\t\t\t\t\t\t\t\tto: to,\n\t\t\t\t\t\t\t\terrorMessage: msg.data.message,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t});\n\t}\n\n\tpublic async cancel(input: VertFile): Promise<void> {\n\t\tconst activeConversion = this.activeConversions.get(input.id);\n\t\tif (!activeConversion) {\n\t\t\terror(\n\t\t\t\t[\"converters\", this.name],\n\t\t\t\t`no active conversion found for file ${input.name}`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tlog(\n\t\t\t[\"converters\", this.name],\n\t\t\t`cancelling conversion for file ${input.name}`,\n\t\t);\n\n\t\tconst { ws, jobId, token } = activeConversion;\n\n\t\tif (ws.readyState === WebSocket.OPEN) {\n\t\t\tconst cancelMsg: CancelJobMessage = {\n\t\t\t\ttype: \"cancelJob\",\n\t\t\t\tdata: {\n\t\t\t\t\tjobId,\n\t\t\t\t\ttoken,\n\t\t\t\t},\n\t\t\t};\n\t\t\tws.send(JSON.stringify(cancelMsg));\n\t\t\tthis.log(\"sent cancelJob message\");\n\t\t}\n\n\t\tws.close();\n\t\tthis.activeConversions.delete(input.id);\n\t}\n\n\tpublic async valid(): Promise<boolean> {\n\t\tif (!(await VertdInstance.instance.url())) {\n\t\t\treturn false;\n\t\t}\n\n\t\ttry {\n\t\t\tawait vertdFetch(\"/api/version\", {\n\t\t\t\tmethod: \"GET\",\n\t\t\t});\n\t\t\treturn true;\n\t\t} catch (e) {\n\t\t\tthis.log(e as unknown as string);\n\t\t\treturn false;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/lib/css/app.scss",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@import url(@fontsource/radio-canada-big/600.css);\n@import url(\"$lib/assets/style/host-grotesk.css\");\n\n:root {\n\t--font-body:\n\t\t\"Host Grotesk\", system-ui, -apple-system, BlinkMacSystemFont,\n\t\t\"Segoe UI\", Roboto, Oxygen, Ubuntu, Cantarell, \"Open Sans\",\n\t\t\"Helvetica Neue\", sans-serif;\n\t--font-display: \"Radio Canada Big\", var(--font-body);\n\t--transition: linear(\n\t\t0,\n\t\t0.006,\n\t\t0.025 2.8%,\n\t\t0.101 6.1%,\n\t\t0.539 18.9%,\n\t\t0.721 25.3%,\n\t\t0.849 31.5%,\n\t\t0.937 38.1%,\n\t\t0.968 41.8%,\n\t\t0.991 45.7%,\n\t\t1.006 50.1%,\n\t\t1.015 55%,\n\t\t1.017 63.9%,\n\t\t1.001\n\t);\n}\n\n@mixin light {\n\t// general\n\t--accent-pink: hsl(302, 100%, 76%);\n\t--accent-pink-alt: hsl(302, 100%, 50%);\n\t--accent-pink-muted: hsl(302, 98%, 42%);\n\t--accent-red: hsl(348, 100%, 80%);\n\t--accent-red-alt: hsl(348, 100%, 50%);\n\t--accent-purple: hsl(264, 100%, 81%);\n\t--accent-purple-alt: hsl(264, 100%, 50%);\n\t--accent-blue: hsl(220, 100%, 78%);\n\t--accent-blue-alt: hsl(220, 100%, 50%);\n\t--accent-green: hsl(140, 70%, 74%);\n\t--accent-green-alt: hsl(140, 66%, 55%);\n\t--accent: var(--accent-pink);\n\t--accent-alt: var(--accent-pink-alt);\n\t--accent-pink-transparent: hsla(303, 100%, 50%, 0);\n\t--accent-red-transparent: hsla(348, 100%, 50%, 0);\n\t--accent-purple-transparent: hsla(264, 100%, 50%, 0);\n\t--accent-blue-transparent: hsla(220, 100%, 50%, 0);\n\t--accent-green-transparent: hsla(140, 70%, 30%, 0);\n\n\t// foregrounds\n\t--fg: hsl(0, 0%, 0%);\n\t--fg-muted: hsla(0, 0%, 0%, 0.6);\n\t--fg-on-accent: hsl(0, 0%, 0%);\n\t--fg-on-badge: hsl(0, 0%, 0%);\n\t// readable version of the accent color\n\t--fg-accent: var(--accent-pink-muted);\n\t--fg-failure: var(--accent-red-alt);\n\n\t// backgrounds\n\t--bg: hsl(0, 0%, 95%);\n\n\t--bg-gradient-from: var(--accent-pink);\n\t--bg-gradient-to: hsla(303, 100%, 50%, 0);\n\t--bg-gradient-pink-from: var(--accent-pink);\n\t--bg-gradient-pink-to: hsla(303, 100%, 50%, 0);\n\t--bg-gradient-pink-alt-from: var(--accent-pink);\n\t--bg-gradient-pink-alt-to: hsl(303, 100%, 91%);\n\t--bg-gradient-red-from: var(--accent-red);\n\t--bg-gradient-red-to: hsla(348, 100%, 50%, 0);\n\t--bg-gradient-red-alt-from: var(--accent-red);\n\t--bg-gradient-red-alt-to: hsl(348, 100%, 91%);\n\t--bg-gradient-purple-from: var(--accent-purple);\n\t--bg-gradient-purple-to: hsla(264, 100%, 50%, 0);\n\t--bg-gradient-purple-alt-from: var(--accent-purple);\n\t--bg-gradient-purple-alt-to: hsl(264, 100%, 91%);\n\t--bg-gradient-blue-from: var(--accent-blue);\n\t--bg-gradient-blue-to: hsla(220, 100%, 50%, 0);\n\t--bg-gradient-blue-alt-from: var(--accent-blue);\n\t--bg-gradient-blue-alt-to: hsl(220, 100%, 91%);\n\t--bg-gradient-green-from: var(--accent-green);\n\t--bg-gradient-green-to: hsla(140, 70%, 30%, 0);\n\t--bg-gradient-green-alt-from: var(--accent-green-alt);\n\t--bg-gradient-green-alt-to: hsl(140, 70%, 91%);\n\t--bg-gradient-image-from: hsla(0, 0%, 95%, 0.5);\n\t--bg-gradient-image-to: hsla(0, 0%, 95%, 1);\n\n\t--bg-gradient: linear-gradient(\n\t\tto bottom,\n\t\tvar(--bg-gradient-from),\n\t\tvar(--bg-gradient-to) 100%\n\t);\n\t--bg-gradient-pink: linear-gradient(\n\t\tto bottom,\n\t\tvar(--bg-gradient-pink-from),\n\t\tvar(--bg-gradient-pink-to) 25%\n\t);\n\t--bg-gradient-pink-alt: linear-gradient(\n\t\tto top,\n\t\tvar(--bg-gradient-pink-alt-from),\n\t\tvar(--bg-gradient-pink-alt-to) 100%\n\t);\n\t--bg-gradient-red: linear-gradient(\n\t\tto bottom,\n\t\tvar(--bg-gradient-red-from),\n\t\tvar(--bg-gradient-red-to) 25%\n\t);\n\t--bg-gradient-red-alt: linear-gradient(\n\t\tto top,\n\t\tvar(--bg-gradient-red-alt-from),\n\t\tvar(--bg-gradient-red-alt-to) 100%\n\t);\n\t--bg-gradient-purple: linear-gradient(\n\t\tto bottom,\n\t\tvar(--bg-gradient-purple-from),\n\t\tvar(--bg-gradient-purple-to) 25%\n\t);\n\t--bg-gradient-purple-alt: linear-gradient(\n\t\tto top,\n\t\tvar(--bg-gradient-purple-alt-from),\n\t\tvar(--bg-gradient-purple-alt-to) 100%\n\t);\n\t--bg-gradient-blue: linear-gradient(\n\t\tto bottom,\n\t\tvar(--bg-gradient-blue-from),\n\t\tvar(--bg-gradient-blue-to) 25%\n\t);\n\t--bg-gradient-blue-alt: linear-gradient(\n\t\tto top,\n\t\tvar(--bg-gradient-blue-alt-from),\n\t\tvar(--bg-gradient-blue-alt-to) 100%\n\t);\n\t--bg-gradient-green: linear-gradient(\n\t\tto bottom,\n\t\tvar(--bg-gradient-green-from),\n\t\tvar(--bg-gradient-green-to) 25%\n\t);\n\t--bg-gradient-green-alt: linear-gradient(\n\t\tto top,\n\t\tvar(--bg-gradient-green-alt-from),\n\t\tvar(--bg-gradient-green-alt-to) 100%\n\t);\n\t--bg-gradient-image: linear-gradient(\n\t\tto bottom,\n\t\tvar(--bg-gradient-image-from),\n\t\tvar(--bg-gradient-image-to) 100%\n\t);\n\t--bg-panel: hsl(0, 0%, 100%);\n\t--bg-panel-highlight: hsl(0, 0%, 92%);\n\t--bg-separator: hsla(0, 0%, 0%, 0.2);\n\t--bg-button: var(--bg-panel-highlight);\n\t--bg-badge: var(--accent-pink);\n\t--bg-input: #e0e0e0;\n\n\t--shadow-panel: 0 2px 4px 0 hsla(0, 0%, 0%, 0.15);\n}\n\n@mixin dark {\n\t// general\n\t--accent-pink: hsl(302, 100%, 76%);\n\t--accent-pink-alt: hsl(302, 100%, 50%);\n\t--accent-red: hsl(348, 100%, 80%);\n\t--accent-red-alt: hsl(348, 100%, 50%);\n\t--accent-purple: hsl(264, 100%, 81%);\n\t--accent-purple-alt: hsl(264, 100%, 50%);\n\t--accent-blue: hsl(220, 100%, 78%);\n\t--accent-blue-alt: hsl(220, 100%, 50%);\n\t--accent: var(--accent-pink);\n\t--accent-alt: var(--accent-pink-alt);\n\t--accent-green: hsl(140, 70%, 74%);\n\t--accent-green-alt: hsl(140, 64%, 42%);\n\t--accent-pink-transparent: hsla(303, 100%, 50%, 0);\n\t--accent-red-transparent: hsla(348, 100%, 50%, 0);\n\t--accent-purple-transparent: hsla(264, 100%, 50%, 0);\n\t--accent-blue-transparent: hsla(220, 100%, 50%, 0);\n\t--accent-green-transparent: hsla(140, 70%, 30%, 0);\n\n\t// foregrounds\n\t--fg: hsl(0, 0%, 100%);\n\t--fg-muted: hsla(0, 0%, 100%, 0.65);\n\t--fg-on-accent: hsl(0, 0%, 0%);\n\t--fg-on-badge: hsl(0, 0%, 0%);\n\t--fg-accent: var(--accent);\n\t--fg-failure: var(--accent-red);\n\n\t// backgrounds\n\t--bg: hsl(220, 5%, 15%);\n\n\t--bg-gradient-from: hsla(303, 100%, 50%, 0.1);\n\t--bg-gradient-to: hsla(303, 100%, 50%, 0);\n\t--bg-gradient-pink-from: hsla(303, 100%, 50%, 0.1);\n\t--bg-gradient-pink-to: hsla(303, 100%, 50%, 0);\n\t--bg-gradient-pink-alt-from: var(--accent-pink);\n\t--bg-gradient-pink-alt-to: hsl(303, 100%, 91%);\n\t--bg-gradient-red-from: hsla(348, 100%, 50%, 0.1);\n\t--bg-gradient-red-to: hsla(348, 100%, 50%, 0);\n\t--bg-gradient-red-alt-from: var(--accent-red);\n\t--bg-gradient-red-alt-to: hsl(348, 100%, 91%);\n\t--bg-gradient-purple-from: hsla(264, 100%, 50%, 0.1);\n\t--bg-gradient-purple-to: hsla(264, 100%, 50%, 0);\n\t--bg-gradient-purple-alt-from: var(--accent-purple);\n\t--bg-gradient-purple-alt-to: hsl(264, 100%, 91%);\n\t--bg-gradient-blue-from: hsla(220, 100%, 50%, 0.1);\n\t--bg-gradient-blue-to: hsla(220, 100%, 50%, 0);\n\t--bg-gradient-blue-alt-from: var(--accent-blue);\n\t--bg-gradient-blue-alt-to: hsl(220, 100%, 91%);\n\t--bg-gradient-green-from: hsla(140, 70%, 30%, 0.1);\n\t--bg-gradient-green-to: hsla(140, 70%, 30%, 0);\n\t--bg-gradient-green-alt-from: var(--accent-green-alt);\n\t--bg-gradient-green-alt-to: hsl(140, 70%, 91%);\n\t--bg-gradient-image-from: hsla(220, 5%, 12%, 0.5);\n\t--bg-gradient-image-to: hsla(220, 5%, 12%, 1);\n\n\t--bg-gradient: linear-gradient(\n\t\tto bottom,\n\t\tvar(--bg-gradient-from),\n\t\tvar(--bg-gradient-to) 100%\n\t);\n\t--bg-gradient-pink: linear-gradient(\n\t\tto bottom,\n\t\tvar(--bg-gradient-pink-from),\n\t\tvar(--bg-gradient-pink-to) 25%\n\t);\n\t--bg-gradient-pink-alt: linear-gradient(\n\t\tto top,\n\t\tvar(--bg-gradient-pink-alt-from),\n\t\tvar(--bg-gradient-pink-alt-to) 100%\n\t);\n\t--bg-gradient-red: linear-gradient(\n\t\tto bottom,\n\t\tvar(--bg-gradient-red-from),\n\t\tvar(--bg-gradient-red-to) 25%\n\t);\n\t--bg-gradient-red-alt: linear-gradient(\n\t\tto top,\n\t\tvar(--bg-gradient-red-alt-from),\n\t\tvar(--bg-gradient-red-alt-to) 100%\n\t);\n\t--bg-gradient-purple: linear-gradient(\n\t\tto bottom,\n\t\tvar(--bg-gradient-purple-from),\n\t\tvar(--bg-gradient-purple-to) 25%\n\t);\n\t--bg-gradient-purple-alt: linear-gradient(\n\t\tto top,\n\t\tvar(--bg-gradient-purple-alt-from),\n\t\tvar(--bg-gradient-purple-alt-to) 100%\n\t);\n\t--bg-gradient-blue: linear-gradient(\n\t\tto bottom,\n\t\tvar(--bg-gradient-blue-from),\n\t\tvar(--bg-gradient-blue-to) 25%\n\t);\n\t--bg-gradient-blue-alt: linear-gradient(\n\t\tto top,\n\t\tvar(--bg-gradient-blue-alt-from),\n\t\tvar(--bg-gradient-blue-alt-to) 100%\n\t);\n\t--bg-gradient-green: linear-gradient(\n\t\tto bottom,\n\t\tvar(--bg-gradient-green-from),\n\t\tvar(--bg-gradient-green-to) 25%\n\t);\n\t--bg-gradient-green-alt: linear-gradient(\n\t\tto top,\n\t\tvar(--bg-gradient-green-alt-from),\n\t\tvar(--bg-gradient-green-alt-to) 100%\n\t);\n\t--bg-gradient-image: linear-gradient(\n\t\tto bottom,\n\t\tvar(--bg-gradient-image-from),\n\t\tvar(--bg-gradient-image-to) 100%\n\t);\n\t--bg-panel: hsl(220, 4%, 24%);\n\t--bg-panel-highlight: hsl(220, 2%, 32%);\n\t--bg-separator: hsl(220, 4%, 28%);\n\t--bg-button: hsl(220, 6%, 34%);\n\t--bg-badge: var(--accent-pink);\n\n\t--shadow-panel: 0 4px 6px 0 hsla(0, 0%, 0%, 0.15);\n\n\tcolor-scheme: dark;\n}\n\n@media (prefers-color-scheme: dark) {\n\t:root {\n\t\t@include dark;\n\t}\n}\n\n@media (prefers-color-scheme: light) {\n\t:root {\n\t\t@include light;\n\t}\n}\n\n:root.light {\n\t@include light;\n}\n\n:root.dark {\n\t@include dark;\n}\n\nbody {\n\t@apply text-foreground font-body font-semibold overflow-x-hidden;\n\twidth: 100vw;\n\tbackground-color: var(--bg);\n\tbackground-size: 100vw 100vh;\n}\n\n::selection,\n::-moz-selection {\n\t@apply bg-accent-blue text-on-accent;\n}\n\n.hoverable {\n\t@apply hover:scale-105 duration-200;\n\twill-change: transform;\n}\n\n.hoverable-md {\n\t@apply hover:scale-110 duration-200;\n\twill-change: transform;\n}\n\n.hoverable-lg {\n\t@apply hover:scale-[1.15] duration-200;\n\twill-change: transform;\n}\n\n.selected {\n\t@apply bg-accent-purple !text-black;\n}\n\n@layer components {\n\tselect {\n\t\t@apply appearance-none;\n\t}\n\n\t.btn {\n\t\t@apply bg-button flex items-center justify-center overflow-hidden relative cursor-pointer px-6 h-14 rounded-full font-medium focus:!outline-none hover:scale-105 duration-200 active:scale-95 disabled:opacity-50 disabled:pointer-events-none hoverable;\n\t\ttransition:\n\t\t\topacity 0.2s ease,\n\t\t\ttransform 0.2s ease,\n\t\t\tbackground-color 0.2s ease;\n\t}\n\n\t.btn.highlight {\n\t\t@apply bg-accent text-on-accent;\n\t}\n\n\th1,\n\th2,\n\th3,\n\th4,\n\th5,\n\th6 {\n\t\t@apply font-display font-semibold;\n\t}\n\n\tcode {\n\t\t@apply font-mono bg-gray-200 rounded-md px-1 dynadark:bg-panel-alt dynadark:text-white;\n\t}\n\n\tp a {\n\t\t@apply text-accent underline;\n\t}\n\n\tinput[type=\"text\"],\n\tselect.dropdown {\n\t\t@apply w-full p-3 rounded-lg bg-panel border-2 border-button pl-3 pr-[4rem];\n\t}\n\n\tinput[type=\"number\"]::-webkit-inner-spin-button,\n\tinput[type=\"number\"]::-webkit-outer-spin-button {\n\t\t-webkit-appearance: none;\n\t\tmargin: 0;\n\t}\n\n\tinput[type=\"number\"] {\n\t\t-moz-appearance: textfield;\n\t\tappearance: textfield;\n\t}\n\n\tinput[type=\"text\"]::placeholder {\n\t\t@apply text-muted font-normal;\n\t}\n\n\tinput[type=\"text\"]:focus {\n\t\t@apply outline outline-accent outline-2;\n\t}\n\n\tinput[type=\"range\"] {\n\t\t@apply appearance-none bg-panel h-2 rounded-lg;\n\t}\n\n\tinput[type=\"range\"]::-webkit-slider-thumb {\n\t\t@apply appearance-none w-4 h-4 bg-accent rounded-full cursor-pointer;\n\t}\n\n\tinput[type=\"range\"]::-moz-range-thumb {\n\t\t@apply w-4 h-4 bg-accent rounded-full cursor-pointer;\n\t}\n\n\thr {\n\t\t@apply border-separator;\n\t}\n}\n"
  },
  {
    "path": "src/lib/sections/about/Credits.svelte",
    "content": "<script lang=\"ts\">\n\timport Panel from \"$lib/components/visual/Panel.svelte\";\n\timport { HeartHandshakeIcon } from \"lucide-svelte\";\n\timport {\n\t\tDISABLE_ALL_EXTERNAL_REQUESTS,\n\t\tGITHUB_URL_VERT,\n\t} from \"$lib/util/consts\";\n\timport { m } from \"$lib/paraglide/messages\";\n\timport { link, sanitize } from \"$lib/store/index.svelte\";\n\n\tlet { mainContribs, notableContribs, ghContribs } = $props();\n</script>\n\n{#snippet contributor(\n\tname: string,\n\tgithub: string,\n\tavatar: string,\n\trole?: string,\n\tsmaller?: boolean,\n)}\n\t<div class=\"flex items-center gap-4\" class:gap-1={smaller}>\n\t\t<a\n\t\t\thref={github}\n\t\t\ttarget=\"_blank\"\n\t\t\trel=\"noopener noreferrer\"\n\t\t\tclass=\"flex-shrink-0\"\n\t\t>\n\t\t\t<img\n\t\t\t\tsrc={avatar}\n\t\t\t\talt={name}\n\t\t\t\ttitle={name}\n\t\t\t\tclass=\"{smaller\n\t\t\t\t\t? 'w-12 h-12 hoverable'\n\t\t\t\t\t: role\n\t\t\t\t\t\t? 'w-14 h-14 hoverable-md'\n\t\t\t\t\t\t: 'w-10 h-10 hoverable-lg'} rounded-full\"\n\t\t\t/>\n\t\t</a>\n\t\t{#if role}\n\t\t\t<div class=\"flex flex-col gap-1\">\n\t\t\t\t<p\n\t\t\t\t\tclass=\"font-semibold\"\n\t\t\t\t\tclass:text-xl={!smaller}\n\t\t\t\t\tclass:text-base={smaller}\n\t\t\t\t>\n\t\t\t\t\t{name}\n\t\t\t\t</p>\n\t\t\t\t<p class=\"text-sm font-normal text-muted\">{role}</p>\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n{/snippet}\n\n<Panel class=\"flex flex-col gap-8 p-6\">\n\t<h2 class=\"text-2xl font-bold flex items-center\">\n\t\t<div class=\"rounded-full bg-blue-300 p-2 inline-block mr-3 w-10 h-10\">\n\t\t\t<HeartHandshakeIcon color=\"black\" />\n\t\t</div>\n\t\t{m[\"about.credits.title\"]()}\n\t</h2>\n\n\t<p class=\"-mt-4 -mb-3 font-black text-lg\">\n\t\t{m[\"about.credits.contact_team\"]()}\n\t</p>\n\n\t<!-- Main contributors -->\n\t<div class=\"flex flex-col gap-4\">\n\t\t<div class=\"flex flex-col flex-wrap gap-2\">\n\t\t\t{#each mainContribs as contrib}\n\t\t\t\t{@const { name, github, avatar, role } = contrib}\n\t\t\t\t{@render contributor(name, github, avatar, role)}\n\t\t\t{/each}\n\t\t</div>\n\t</div>\n\n\t<!-- Notable contributors -->\n\t<div class=\"flex flex-col gap-4\">\n\t\t<div class=\"flex flex-col gap-1\">\n\t\t\t<h2 class=\"text-base font-bold\">\n\t\t\t\t{m[\"about.credits.notable_contributors\"]()}\n\t\t\t</h2>\n\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t<p class=\"text-base text-muted font-normal\">\n\t\t\t\t\t{m[\"about.credits.notable_description\"]()}\n\t\t\t\t</p>\n\t\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t\t{#each notableContribs as contrib}\n\t\t\t\t\t\t{@const { name, github, avatar, role } = contrib}\n\t\t\t\t\t\t{@render contributor(name, github, avatar, role, true)}\n\t\t\t\t\t{/each}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- GitHub contributors -->\n\t\t{#if !DISABLE_ALL_EXTERNAL_REQUESTS}\n\t\t\t<div class=\"flex flex-col gap-4\">\n\t\t\t\t<div class=\"flex flex-col gap-1\">\n\t\t\t\t\t<h2 class=\"text-base font-bold\">\n\t\t\t\t\t\t{m[\"about.credits.github_contributors\"]()}\n\t\t\t\t\t</h2>\n\t\t\t\t\t{#if ghContribs && ghContribs.length > 0}\n\t\t\t\t\t\t<p class=\"text-base text-muted font-normal\">\n\t\t\t\t\t\t\t{@html sanitize(\n\t\t\t\t\t\t\t\tlink(\n\t\t\t\t\t\t\t\t\t\"github_link\",\n\t\t\t\t\t\t\t\t\tm[\"about.credits.github_description\"](),\n\t\t\t\t\t\t\t\t\tGITHUB_URL_VERT,\n\t\t\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t{:else}\n\t\t\t\t\t\t<p class=\"text-base text-muted font-normal italic\">\n\t\t\t\t\t\t\t{@html sanitize(\n\t\t\t\t\t\t\t\tlink(\n\t\t\t\t\t\t\t\t\t\"contribute_link\",\n\t\t\t\t\t\t\t\t\tm[\"about.credits.no_contributors\"](),\n\t\t\t\t\t\t\t\t\tGITHUB_URL_VERT,\n\t\t\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t{/if}\n\t\t\t\t</div>\n\n\t\t\t\t{#if ghContribs && ghContribs.length > 0}\n\t\t\t\t\t<div class=\"flex flex-row flex-wrap gap-2\">\n\t\t\t\t\t\t{#each ghContribs as contrib}\n\t\t\t\t\t\t\t{@const { name, github, avatar } = contrib}\n\t\t\t\t\t\t\t{@render contributor(name, github, avatar)}\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</div>\n\t\t\t\t{/if}\n\t\t\t</div>\n\n\t\t\t<h2 class=\"mt-2 -mb-2\">{m[\"about.credits.libraries\"]()}</h2>\n\t\t\t<p class=\"font-normal\">\n\t\t\t\t{m[\"about.credits.libraries_description\"]()}\n\t\t\t</p>\n\t\t{/if}\n\t</div>\n</Panel>\n"
  },
  {
    "path": "src/lib/sections/about/Donate.svelte",
    "content": "<script lang=\"ts\" module>\n\texport interface Donor {\n\t\tname: string;\n\t\tamount: number;\n\t\tavatar: string;\n\t}\n</script>\n\n<script lang=\"ts\">\n\timport { goto } from \"$app/navigation\";\n\timport { page } from \"$app/state\";\n\timport { PUB_DONATION_URL, PUB_STRIPE_KEY } from \"$env/static/public\";\n\tconst OFFICIAL_DONATION_URL = \"https://donations.vert.sh\";\n\tconst OFFICIAL_STRIPE_KEY =\n\t\t\"pk_live_51RDVmAGSxPVad6bQwzVNnbc28nlmzA30krLWk1fefCMpUPiSRPkavMMbGqa8A3lUaOCMlsUEVy2CWDYg0ip3aPpL00ZJlsMkf2\";\n\tconst isOfficial =\n\t\tPUB_DONATION_URL === OFFICIAL_DONATION_URL &&\n\t\tPUB_STRIPE_KEY === OFFICIAL_STRIPE_KEY;\n\n\t// import { PUB_STRIPE_KEY, PUB_DONATION_API } from \"$env/static/public\";\n\timport { fade } from \"$lib/util/animation\";\n\timport FancyInput from \"$lib/components/functional/FancyInput.svelte\";\n\timport Panel from \"$lib/components/visual/Panel.svelte\";\n\timport { effects, link, sanitize } from \"$lib/store/index.svelte\";\n\timport { loadStripe } from \"@stripe/stripe-js/pure\";\n\timport { type Stripe, type StripeElements } from \"@stripe/stripe-js\";\n\timport clsx from \"clsx\";\n\timport {\n\t\tCalendarHeartIcon,\n\t\tHandCoinsIcon,\n\t\tHeartIcon,\n\t\tWalletIcon,\n\t} from \"lucide-svelte\";\n\timport { onMount } from \"svelte\";\n\timport { Elements, PaymentElement } from \"svelte-stripe\";\n\timport { quintOut } from \"svelte/easing\";\n\timport { m } from \"$lib/paraglide/messages\";\n\timport { ToastManager } from \"$lib/util/toast.svelte\";\n\timport { log } from \"$lib/util/logger\";\n\n\tlet amount = $state(1);\n\tlet customAmount = $state(\"\");\n\tlet type = $state(\"one-time\");\n\tlet stripe = $state<Stripe | null>(null);\n\n\tconst presetAmounts = [1, 10, 25];\n\n\tlet paymentState = $state<\"prepay\" | \"fetching\" | \"details\">(\"prepay\");\n\tlet enablePay = $state(false);\n\tlet clientSecret = $state<string | null>(null);\n\tlet elements: StripeElements | null = $state(null);\n\n\tconst amountClick = (preset: number) => {\n\t\tamount = preset;\n\t\tcustomAmount = \"\";\n\t};\n\n\tconst paymentClick = async () => {\n\t\tif (paymentState !== \"prepay\") return;\n\n\t\tif (!stripe) stripe = await loadStripe(PUB_STRIPE_KEY);\n\n\t\tpaymentState = \"fetching\";\n\t\tconst res = await fetch(`${PUB_DONATION_URL}/billing`, {\n\t\t\tmethod: \"POST\",\n\t\t\tbody: (amount * 100).toString(),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\tpaymentState = \"prepay\";\n\t\t\tToastManager.add({\n\t\t\t\ttype: \"error\",\n\t\t\t\tmessage: m[\"about.donate.payment_error\"](),\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tconst { data }: { data: string } = await res.json();\n\t\tclientSecret = data;\n\t\tpaymentState = \"details\";\n\t};\n\n\t$effect(() => {\n\t\tif (customAmount) {\n\t\t\tamount = parseFloat(customAmount);\n\t\t}\n\t});\n\n\tconst payDuration = 400;\n\tconst transition = \"cubic-bezier(0.23, 1, 0.320, 1)\";\n\n\tonMount(async () => {\n\t\tif (!isOfficial) {\n\t\t\tlog(\n\t\t\t\t[\"about\", \"donate\"],\n\t\t\t\t\"donations are being sent to an unofficial VERT instance - PUB_DONATION_URL and/or PUB_STRIPE_KEY have been changed.\",\n\t\t\t);\n\t\t} else {\n\t\t\tlog(\n\t\t\t\t[\"about\", \"donate\"],\n\t\t\t\t\"donations are being sent to the official VERT instance.\",\n\t\t\t);\n\t\t}\n\t});\n\n\tconst donate = async () => {\n\t\tif (!stripe || !clientSecret || !elements) return;\n\n\t\tenablePay = false;\n\n\t\tconst submitResult = await elements.submit();\n\t\tif (submitResult.error) {\n\t\t\tconst period = submitResult.error.message?.endsWith(\".\") ? \"\" : \".\";\n\t\t\tToastManager.add({\n\t\t\t\ttype: \"error\",\n\t\t\t\tmessage: m[\"about.donate.payment_failed\"]({\n\t\t\t\t\tmessage: submitResult.error.message || \"\",\n\t\t\t\t\tperiod,\n\t\t\t\t}),\n\t\t\t});\n\t\t\tenablePay = true;\n\t\t\treturn;\n\t\t}\n\n\t\tconst res = await stripe.confirmPayment({\n\t\t\telements,\n\t\t\tclientSecret,\n\t\t\tredirect: \"if_required\",\n\t\t\tconfirmParams: {\n\t\t\t\treturn_url: page.url.toString(),\n\t\t\t},\n\t\t});\n\n\t\tif (res.error) {\n\t\t\tconst period = res.error.message?.endsWith(\".\") ? \"\" : \".\";\n\t\t\tToastManager.add({\n\t\t\t\ttype: \"error\",\n\t\t\t\tmessage: m[\"about.donate.payment_failed\"]({\n\t\t\t\t\tmessage: res.error.message || \"\",\n\t\t\t\t\tperiod,\n\t\t\t\t}),\n\t\t\t});\n\t\t} else {\n\t\t\tToastManager.add({\n\t\t\t\ttype: \"info\",\n\t\t\t\tmessage: m[\"about.donate.thank_you\"](),\n\t\t\t});\n\t\t}\n\n\t\tpaymentState = \"prepay\";\n\t\tclientSecret = null;\n\t\telements = null;\n\t\tamount = 1;\n\t\tcustomAmount = \"\";\n\t\ttype = \"one-time\";\n\t\tenablePay = false;\n\n\t\tstripe = await loadStripe(PUB_STRIPE_KEY);\n\t};\n\n\tonMount(() => {\n\t\tconst status = page.url.searchParams.get(\"redirect_status\");\n\t\tif (status) {\n\t\t\tswitch (status) {\n\t\t\t\tcase \"succeeded\":\n\t\t\t\t\tToastManager.add({\n\t\t\t\t\t\ttype: \"success\",\n\t\t\t\t\t\tmessage: m[\"about.donate.thank_you\"](),\n\t\t\t\t\t});\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tToastManager.add({\n\t\t\t\t\t\ttype: \"error\",\n\t\t\t\t\t\tmessage: m[\"about.donate.donation_error\"](),\n\t\t\t\t\t});\n\t\t\t}\n\n\t\t\tgoto(\"/about\");\n\t\t}\n\t});\n</script>\n\n<Panel class=\"flex flex-col gap-8 p-6\">\n\t<div class=\"flex flex-col gap-3\">\n\t\t<h2 class=\"text-2xl font-bold flex items-center\">\n\t\t\t<div\n\t\t\t\tclass=\"rounded-full bg-accent-red p-2 inline-block mr-3 w-10 h-10\"\n\t\t\t>\n\t\t\t\t<HeartIcon color=\"black\" />\n\t\t\t</div>\n\t\t\t{m[\"about.donate.title\"]()}\n\t\t</h2>\n\t\t<p class=\"text-base font-normal\">\n\t\t\t{m[\"about.donate.description\"]()}\n\t\t</p>\n\t</div>\n\n\t<div\n\t\tclass=\"flex flex-col gap-3 w-full overflow-visible\"\n\t\tstyle=\"height: {paymentState !== 'prepay' ? 0 : 124}px;\n\t\ttransform: scaleY({paymentState !== 'prepay' ? 0 : 1});\n\t\topacity: {paymentState !== 'prepay' ? 0 : 1};\n\t\tfilter: blur({paymentState !== 'prepay' ? 4 : 0}px);\n\t\ttransition: height {payDuration}ms {transition}, \n\t\t\t\t\topacity {payDuration - 200}ms {transition}, \n\t\t\t\t\ttransform {payDuration}ms {transition},\n\t\t\t\t\tfilter {payDuration}ms {transition};\"\n\t>\n\t\t<div class=\"flex gap-3 w-full\">\n\t\t\t<button\n\t\t\t\tonclick={() => (type = \"one-time\")}\n\t\t\t\tclass={clsx(\n\t\t\t\t\t\"btn flex-1 p-4 rounded-lg flex items-center justify-center\",\n\t\t\t\t\t{\n\t\t\t\t\t\t\"!scale-100\": !$effects,\n\t\t\t\t\t\t\"bg-accent-red text-black\": type === \"one-time\",\n\t\t\t\t\t},\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t<HandCoinsIcon size=\"24\" class=\"inline-block mr-2\" />\n\t\t\t\t{m[\"about.donate.one_time\"]()}\n\t\t\t</button>\n\n\t\t\t<button\n\t\t\t\tdisabled\n\t\t\t\tonclick={() => (type = \"monthly\")}\n\t\t\t\tclass={clsx(\n\t\t\t\t\t\"btn flex-1 p-4 rounded-lg flex items-center justify-center\",\n\t\t\t\t\t{\n\t\t\t\t\t\t\"!scale-100\": !$effects,\n\t\t\t\t\t\t\"bg-accent-red text-black\": type === \"monthly\",\n\t\t\t\t\t},\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t<CalendarHeartIcon size=\"24\" class=\"inline-block mr-2\" />\n\t\t\t\t{m[\"about.donate.monthly\"]()}\n\t\t\t</button>\n\t\t</div>\n\t\t<div class=\"grid grid-cols-4 gap-3 w-full\">\n\t\t\t{#each presetAmounts as preset, i}\n\t\t\t\t<button\n\t\t\t\t\tonclick={() => amountClick(preset)}\n\t\t\t\t\tclass={clsx(\n\t\t\t\t\t\t\"btn p-4 rounded-lg flex items-center justify-center\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"!scale-100\": !$effects,\n\t\t\t\t\t\t\t\"bg-accent-red text-black\": amount === preset,\n\t\t\t\t\t\t},\n\t\t\t\t\t)}\n\t\t\t\t\tstyle={i === 2 ? \"grid-column: 3;\" : \"\"}\n\t\t\t\t>\n\t\t\t\t\t${preset} USD\n\t\t\t\t</button>\n\t\t\t{/each}\n\t\t\t<div class=\"flex items-center justify-center\">\n\t\t\t\t<FancyInput\n\t\t\t\t\tbind:value={customAmount}\n\t\t\t\t\tplaceholder={m[\"about.donate.custom\"]()}\n\t\t\t\t\tprefix=\"$\"\n\t\t\t\t\ttype=\"number\"\n\t\t\t\t\tclass=\"h-full\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<div class=\"flex flex-row justify-center w-full\">\n\t\t<div\n\t\t\trole=\"button\"\n\t\t\ttabindex=\"0\"\n\t\t\tonkeydown={(e) => {\n\t\t\t\tif (e.key === \"Enter\") {\n\t\t\t\t\tpaymentClick();\n\t\t\t\t}\n\t\t\t}}\n\t\t\tonclick={paymentClick}\n\t\t\tclass={clsx(\n\t\t\t\t\"btn flex-1 p-3 relative rounded-3xl bg-accent-red border-2 border-accent-red h-14 text-black\",\n\t\t\t\t{\n\t\t\t\t\t\"h-[450px] rounded-2xl bg-transparent cursor-auto !scale-100 -mt-10 -mb-2\":\n\t\t\t\t\t\tpaymentState !== \"prepay\",\n\t\t\t\t\t\"!scale-100\": !$effects,\n\t\t\t\t},\n\t\t\t)}\n\t\t\tstyle=\"transition: height {payDuration}ms {transition}, border-radius {payDuration}ms {transition}, background-color {payDuration}ms {transition}, transform {payDuration}ms {transition}, margin {payDuration}ms {transition}; will-change: height, border-radius, background-color, transform, margin;\"\n\t\t>\n\t\t\t<div class=\"grid grid-cols-1 grid-rows-1 w-full h-full\">\n\t\t\t\t{#if paymentState !== \"prepay\"}\n\t\t\t\t\t<div\n\t\t\t\t\t\ttransition:fade={{\n\t\t\t\t\t\t\tduration: payDuration,\n\t\t\t\t\t\t\teasing: quintOut,\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tclass=\"row-start-1 col-start-1 flex w-full h-full flex-col gap-4\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclass=\"flex-grow max-h-full overflow-y-auto overflow-x-hidden\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{#if stripe && clientSecret}\n\t\t\t\t\t\t\t\t<Elements {stripe} {clientSecret} bind:elements>\n\t\t\t\t\t\t\t\t\t<PaymentElement\n\t\t\t\t\t\t\t\t\t\ton:change={(e) => {\n\t\t\t\t\t\t\t\t\t\t\tenablePay = e.detail.complete;\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</Elements>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div class=\"flex-shrink-0\">\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tdisabled={!stripe ||\n\t\t\t\t\t\t\t\t\t!clientSecret ||\n\t\t\t\t\t\t\t\t\t!enablePay}\n\t\t\t\t\t\t\t\tclass=\"btn w-full h-12 bg-accent-red text-black rounded-full mt-4\"\n\t\t\t\t\t\t\t\tonclick={donate}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{m[\"about.donate.donate_amount\"]({\n\t\t\t\t\t\t\t\t\tamount: amount.toFixed(2),\n\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t{:else}\n\t\t\t\t\t<!-- svelte-ignore a11y_click_events_have_key_events -->\n\t\t\t\t\t<!-- svelte-ignore a11y_no_static_element_interactions -->\n\t\t\t\t\t<div\n\t\t\t\t\t\ttransition:fade={{\n\t\t\t\t\t\t\tduration: payDuration,\n\t\t\t\t\t\t\teasing: quintOut,\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tonclick={paymentClick}\n\t\t\t\t\t\tclass=\"row-start-1 col-start-1 flex justify-center items-center\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<WalletIcon size=\"24\" class=\"inline-block mr-2\" />\n\t\t\t\t\t\t{m[\"about.donate.pay_now\"]()}\n\t\t\t\t\t</div>\n\t\t\t\t{/if}\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<p class=\"text-sm font-normal text-muted\">\n\t\t{#if isOfficial}\n\t\t\t{m[\"about.donate.donation_notice_official\"]()}\n\t\t{:else}\n\t\t\t{@html sanitize(\n\t\t\t\tlink(\n\t\t\t\t\t\"official_link\",\n\t\t\t\t\tm[\"about.donate.donation_notice_unofficial\"](),\n\t\t\t\t\t\"https://vert.sh\",\n\t\t\t\t\ttrue,\n\t\t\t\t\t\"\",\n\t\t\t\t),\n\t\t\t)}\n\t\t{/if}\n\t</p>\n</Panel>\n"
  },
  {
    "path": "src/lib/sections/about/Resources.svelte",
    "content": "<script lang=\"ts\">\n\timport Panel from \"$lib/components/visual/Panel.svelte\";\n\timport { CONTACT_EMAIL, DISCORD_URL, GITHUB_URL_VERT } from \"$lib/util/consts\";\n\timport { effects } from \"$lib/store/index.svelte\";\n\timport {\n\t\tGithubIcon,\n\t\tLinkIcon,\n\t\tMailIcon,\n\t\tMessageCircleMoreIcon,\n\t} from \"lucide-svelte\";\n\timport { m } from \"$lib/paraglide/messages\";\n</script>\n\n<Panel class=\"flex flex-col gap-4 p-6\">\n\t<h2 class=\"text-2xl font-bold flex items-center\">\n\t\t<div\n\t\t\tclass=\"rounded-full bg-accent-purple p-2 inline-block mr-3 w-10 h-10\"\n\t\t>\n\t\t\t<LinkIcon color=\"black\" />\n\t\t</div>\n\t\t{m[\"about.resources.title\"]()}\n\t</h2>\n\t<div class=\"flex gap-3\">\n\t\t<a\n\t\t\thref={DISCORD_URL}\n\t\t\ttarget=\"_blank\"\n\t\t\trel=\"noopener noreferrer\"\n\t\t\tclass=\"btn {$effects\n\t\t\t\t? ''\n\t\t\t\t: '!scale-100'} flex-1 gap-2 p-4 rounded-full bg-button text-black dynadark:text-white flex items-center justify-center\"\n\t\t>\n\t\t\t<MessageCircleMoreIcon size=\"24\" class=\"inline-block mr-2\" />\n\t\t\t{m[\"about.resources.discord\"]()}\n\t\t</a>\n\t\t<a\n\t\t\thref={GITHUB_URL_VERT}\n\t\t\ttarget=\"_blank\"\n\t\t\trel=\"noopener noreferrer\"\n\t\t\tclass=\"btn {$effects\n\t\t\t\t? ''\n\t\t\t\t: '!scale-100'} flex-1 gap-2 p-4 rounded-full bg-button text-black dynadark:text-white flex items-center justify-center\"\n\t\t>\n\t\t\t<GithubIcon size=\"24\" class=\"inline-block mr-2\" />\n\t\t\t{m[\"about.resources.source\"]()}\n\t\t</a>\n\t\t<a\n\t\t\thref=\"mailto:{CONTACT_EMAIL}\"\n\t\t\ttarget=\"_blank\"\n\t\t\trel=\"noopener noreferrer\"\n\t\t\tclass=\"btn {$effects\n\t\t\t\t? ''\n\t\t\t\t: '!scale-100'} flex-1 gap-2 p-4 rounded-full bg-button text-black dynadark:text-white flex items-center justify-center\"\n\t\t>\n\t\t\t<MailIcon size=\"24\" class=\"inline-block mr-2\" />\n\t\t\t{m[\"about.resources.email\"]()}\n\t\t</a>\n\t</div>\n</Panel>\n"
  },
  {
    "path": "src/lib/sections/about/Sponsors.svelte",
    "content": "<script lang=\"ts\">\n\timport Panel from \"$lib/components/visual/Panel.svelte\";\n\timport { PiggyBankIcon, CopyIcon, CheckIcon } from \"lucide-svelte\";\n\timport HotMilk from \"$lib/assets/hotmilk.svg?component\";\n\timport { DISCORD_URL } from \"$lib/util/consts\";\n\timport { error } from \"$lib/util/logger\";\n\timport { m } from \"$lib/paraglide/messages\";\n\timport { link, sanitize } from \"$lib/store/index.svelte\";\n\timport { ToastManager } from \"$lib/util/toast.svelte\";\n\n\tlet copied = false;\n\tlet timeoutId: NodeJS.Timeout | null = null;\n\n\tfunction copyToClipboard() {\n\t\ttry {\n\t\t\tnavigator.clipboard.writeText(\"hello@vert.sh\");\n\t\t\tcopied = true;\n\t\t\tToastManager.add({\n\t\t\t\ttype: \"success\",\n\t\t\t\tmessage: m[\"about.sponsors.email_copied\"](),\n\t\t\t});\n\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\ttimeoutId = setTimeout(() => (copied = false), 2000);\n\t\t} catch (err) {\n\t\t\terror(`Failed to copy email: ${err}`);\n\t\t}\n\t}\n</script>\n\n<Panel class=\"flex flex-col gap-3 p-6 min-h-[280px]\">\n\t<h2 class=\"text-2xl font-bold flex items-center\">\n\t\t<div\n\t\t\tclass=\"rounded-full bg-accent-pink p-2 inline-block mr-3 w-10 h-10\"\n\t\t>\n\t\t\t<PiggyBankIcon color=\"black\" />\n\t\t</div>\n\t\t{m[\"about.sponsors.title\"]()}\n\t</h2>\n\t<div class=\"mt-2 [&>*]:font-normal h-full flex justify-between flex-col\">\n\t\t<div class=\"flex gap-3 justify-center text-lg\">\n\t\t\t<a\n\t\t\t\thref=\"https://hotmilk.studio\"\n\t\t\t\ttarget=\"_blank\"\n\t\t\t\tclass=\"w-fit h-fit rounded-2xl py-4 btn gap-2 flex flex-col justify-center items-center\"\n\t\t\t>\n\t\t\t\t<HotMilk class=\"w-full h-16\" />\n\t\t\t</a>\n\t\t</div>\n\t\t<p class=\"text-muted\">\n\t\t\t{@html sanitize(link(\n\t\t\t\t\"discord_link\",\n\t\t\t\tm[\"about.sponsors.description\"](),\n\t\t\t\tDISCORD_URL,\n\t\t\t\ttrue\n\t\t\t))}\n\t\t\t<span class=\"inline-block mx-[2px] relative top-[2px]\">\n\t\t\t\t<button\n\t\t\t\t\tid=\"email\"\n\t\t\t\t\tclass=\"flex items-center gap-[6px] cursor-pointer\"\n\t\t\t\t\tonclick={copyToClipboard}\n\t\t\t\t\taria-label=\"Copy email to clipboard\"\n\t\t\t\t>\n\t\t\t\t\t{#if copied}\n\t\t\t\t\t\t<CheckIcon size=\"14\"></CheckIcon>\n\t\t\t\t\t{:else}\n\t\t\t\t\t\t<CopyIcon size=\"14\"></CopyIcon>\n\t\t\t\t\t{/if}\n\t\t\t\t\thello@vert.sh\n\t\t\t\t</button>\n\t\t\t</span>!\n\t\t</p>\n\t</div>\n</Panel>\n\n<style lang=\"postcss\">\n\t#email {\n\t\t@apply font-mono bg-gray-200 rounded-md px-1 text-inherit no-underline dynadark:bg-panel-alt dynadark:text-white;\n\t}\n\n\t#email:hover {\n\t\t@apply font-mono !bg-accent !text-black rounded-md px-1 duration-200;\n\t}\n</style>\n"
  },
  {
    "path": "src/lib/sections/about/Why.svelte",
    "content": "<script lang=\"ts\">\n\timport Panel from \"$lib/components/visual/Panel.svelte\";\n\timport { MessageCircleQuestionIcon } from \"lucide-svelte\";\n\timport { m } from \"$lib/paraglide/messages\";\n\timport { sanitize } from \"$lib/store/index.svelte\";\n</script>\n\n<Panel class=\"flex flex-col gap-3 p-6\">\n\t<h2 class=\"text-2xl font-bold flex items-center\">\n\t\t<div\n\t\t\tclass=\"rounded-full bg-accent-pink p-2 inline-block mr-3 w-10 h-10\"\n\t\t>\n\t\t\t<MessageCircleQuestionIcon color=\"black\" />\n\t\t</div>\n\t\t{m[\"about.why.title\"]()}\n\t</h2>\n\t<p class=\"text-lg font-normal\">\n\t\t{@html sanitize(m[\"about.why.description\"]())}\n\t</p>\n</Panel>\n"
  },
  {
    "path": "src/lib/sections/about/index.ts",
    "content": "export { default as Credits } from \"./Credits.svelte\";\nexport { default as Donate } from \"./Donate.svelte\";\nexport { default as Resources } from \"./Resources.svelte\";\nexport { default as Why } from \"./Why.svelte\";\nexport { default as Sponsors } from \"./Sponsors.svelte\";\n"
  },
  {
    "path": "src/lib/sections/settings/Appearance.svelte",
    "content": "<script lang=\"ts\">\n\timport Panel from \"$lib/components/visual/Panel.svelte\";\n\timport {\n\t\ttheme,\n\t\teffects,\n\t\tsetEffects,\n\t\tsetTheme,\n\t\tupdateLocale,\n\t\tavailableLocales,\n\t} from \"$lib/store/index.svelte\";\n\timport {\n\t\tMoonIcon,\n\t\tPaletteIcon,\n\t\tPauseIcon,\n\t\tPlayIcon,\n\t\tSunIcon,\n\t} from \"lucide-svelte\";\n\timport { onMount, onDestroy } from \"svelte\";\n\timport { m } from \"$lib/paraglide/messages\";\n\timport { getLocale } from \"$lib/paraglide/runtime\";\n\timport Dropdown from \"$lib/components/functional/Dropdown.svelte\";\n\n\tlet currentLocale = $state(\"en\");\n\n\tconst getLanguageDisplayName = (locale: string) => {\n\t\ttry {\n\t\t\treturn availableLocales[locale as keyof typeof availableLocales];\n\t\t} catch {\n\t\t\treturn locale.toUpperCase();\n\t\t}\n\t};\n\n\tconst languageOptions = Object.keys(availableLocales).map((locale) =>\n\t\tgetLanguageDisplayName(locale),\n\t);\n\n\tlet lightElement: HTMLButtonElement;\n\tlet darkElement: HTMLButtonElement;\n\tlet enableEffectsElement: HTMLButtonElement;\n\tlet disableEffectsElement: HTMLButtonElement;\n\n\tlet effectsUnsubscribe: () => void;\n\tlet themeUnsubscribe: () => void;\n\n\tconst updateEffectsClasses = (value: boolean) => {\n\t\tif (value) {\n\t\t\tenableEffectsElement.classList.add(\"selected\");\n\t\t\tdisableEffectsElement.classList.remove(\"selected\");\n\t\t} else {\n\t\t\tdisableEffectsElement.classList.add(\"selected\");\n\t\t\tenableEffectsElement.classList.remove(\"selected\");\n\t\t}\n\t};\n\n\tconst updateThemeClasses = (value: string) => {\n\t\tdocument.documentElement.classList.remove(\"light\", \"dark\");\n\t\tdocument.documentElement.classList.add(value);\n\n\t\tif (value === \"dark\") {\n\t\t\tdarkElement.classList.add(\"selected\");\n\t\t\tlightElement.classList.remove(\"selected\");\n\t\t} else {\n\t\t\tlightElement.classList.add(\"selected\");\n\t\t\tdarkElement.classList.remove(\"selected\");\n\t\t}\n\t};\n\n\tonMount(() => {\n\t\teffectsUnsubscribe = effects.subscribe(updateEffectsClasses);\n\t\tthemeUnsubscribe = theme.subscribe(updateThemeClasses);\n\n\t\tcurrentLocale = localStorage.getItem(\"locale\") || getLocale();\n\t});\n\n\tonDestroy(() => {\n\t\tif (effectsUnsubscribe) effectsUnsubscribe();\n\t\tif (themeUnsubscribe) themeUnsubscribe();\n\t});\n\n\t$effect(() => {\n\t\tupdateEffectsClasses($effects);\n\t\tupdateThemeClasses($theme);\n\t});\n\n\tfunction handleLanguageChange(selectedLanguage: string) {\n\t\tconst selectedLocale = Object.keys(availableLocales).find(\n\t\t\t(locale) => getLanguageDisplayName(locale) === selectedLanguage,\n\t\t);\n\n\t\tif (selectedLocale && selectedLocale !== currentLocale) {\n\t\t\tcurrentLocale = selectedLocale;\n\t\t\tupdateLocale(selectedLocale);\n\t\t}\n\t}\n</script>\n\n<Panel class=\"flex flex-col gap-8 p-6\">\n\t<div class=\"flex flex-col gap-3\">\n\t\t<h2 class=\"text-2xl font-bold\">\n\t\t\t<PaletteIcon\n\t\t\t\tsize=\"40\"\n\t\t\t\tclass=\"inline-block -mt-1 mr-2 bg-accent-purple p-2 rounded-full\"\n\t\t\t\tcolor=\"black\"\n\t\t\t/>\n\t\t\t{m[\"settings.appearance.title\"]()}\n\t\t</h2>\n\t\t<div class=\"flex flex-col gap-8\">\n\t\t\t<div class=\"flex flex-col gap-4\">\n\t\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t\t<p class=\"text-base font-bold\">\n\t\t\t\t\t\t{m[\"settings.appearance.brightness_theme\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t\t<p class=\"text-sm text-muted font-normal italic\">\n\t\t\t\t\t\t{m[\"settings.appearance.brightness_description\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"flex flex-col gap-3 w-full\">\n\t\t\t\t\t<div class=\"flex gap-3 w-full\">\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tbind:this={lightElement}\n\t\t\t\t\t\t\tonclick={() => setTheme(\"light\")}\n\t\t\t\t\t\t\tclass=\"btn {$effects\n\t\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t\t: '!scale-100'} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<SunIcon size=\"24\" class=\"inline-block mr-2\" />\n\t\t\t\t\t\t\t{m[\"settings.appearance.light\"]()}\n\t\t\t\t\t\t</button>\n\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tbind:this={darkElement}\n\t\t\t\t\t\t\tonclick={() => setTheme(\"dark\")}\n\t\t\t\t\t\t\tclass=\"btn {$effects\n\t\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t\t: '!scale-100'} flex-1 p-4 rounded-lg text-black flex items-center justify-center\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<MoonIcon size=\"24\" class=\"inline-block mr-2\" />\n\t\t\t\t\t\t\t{m[\"settings.appearance.dark\"]()}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"flex flex-col gap-4\">\n\t\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t\t<p class=\"text-base font-bold\">\n\t\t\t\t\t\t{m[\"settings.appearance.effect_settings\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t\t<p class=\"text-sm text-muted font-normal italic\">\n\t\t\t\t\t\t{m[\"settings.appearance.effect_description\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"flex flex-col gap-3 w-full\">\n\t\t\t\t\t<div class=\"flex gap-3 w-full\">\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tbind:this={enableEffectsElement}\n\t\t\t\t\t\t\tonclick={() => setEffects(true)}\n\t\t\t\t\t\t\tclass=\"btn {$effects\n\t\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t\t: '!scale-100'} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<PlayIcon size=\"24\" class=\"inline-block mr-2\" />\n\t\t\t\t\t\t\t{m[\"settings.appearance.enable\"]()}\n\t\t\t\t\t\t</button>\n\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tbind:this={disableEffectsElement}\n\t\t\t\t\t\t\tonclick={() => setEffects(false)}\n\t\t\t\t\t\t\tclass=\"btn {$effects\n\t\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t\t: '!scale-100'} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<PauseIcon size=\"24\" class=\"inline-block mr-2\" />\n\t\t\t\t\t\t\t{m[\"settings.appearance.disable\"]()}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"flex flex-col gap-4\">\n\t\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t\t<p class=\"text-base font-bold\">\n\t\t\t\t\t\t{m[\"settings.language.title\"]()}\n\t\t\t\t\t\t{#if currentLocale !== \"en\"} (Language){/if}\n\t\t\t\t\t</p>\n\t\t\t\t\t<p class=\"text-sm text-muted font-normal italic\">\n\t\t\t\t\t\t{m[\"settings.language.description\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"flex flex-col gap-3 w-full\">\n\t\t\t\t\t<Dropdown\n\t\t\t\t\t\toptions={languageOptions}\n\t\t\t\t\t\tsettingsStyle\n\t\t\t\t\t\tselected={getLanguageDisplayName(currentLocale)}\n\t\t\t\t\t\tonselect={handleLanguageChange}\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</Panel>\n"
  },
  {
    "path": "src/lib/sections/settings/Conversion.svelte",
    "content": "<script lang=\"ts\">\n\timport FancyTextInput from \"$lib/components/functional/FancyInput.svelte\";\n\timport Panel from \"$lib/components/visual/Panel.svelte\";\n\timport {\n\t\tPauseIcon,\n\t\tPlayIcon,\n\t\tRefreshCwIcon,\n\t\tChevronDownIcon,\n\t} from \"lucide-svelte\";\n\timport type { ISettings } from \"./index.svelte\";\n\timport {\n\t\tCONVERSION_BITRATES,\n\t\ttype ConversionBitrate,\n\t\tSAMPLE_RATES,\n\t\ttype SampleRate,\n\t} from \"$lib/converters/ffmpeg.svelte\";\n\timport { m } from \"$lib/paraglide/messages\";\n\timport Dropdown from \"$lib/components/functional/Dropdown.svelte\";\n\timport FancyInput from \"$lib/components/functional/FancyInput.svelte\";\n\timport { effects, sanitize } from \"$lib/store/index.svelte\";\n\timport FormatDropdown from \"$lib/components/functional/FormatDropdown.svelte\";\n\timport { categories } from \"$lib/converters\";\n\timport clsx from \"clsx\";\n\n\tconst { settings = $bindable() }: { settings: ISettings } = $props();\n\tlet showAdvanced = $state(false);\n</script>\n\n<Panel class=\"flex flex-col gap-8 p-6\">\n\t<div class=\"flex flex-col gap-3\">\n\t\t<h2 class=\"text-2xl font-bold\">\n\t\t\t<RefreshCwIcon\n\t\t\t\tsize=\"40\"\n\t\t\t\tclass=\"inline-block -mt-1 mr-2 bg-accent p-2 rounded-full\"\n\t\t\t\tcolor=\"black\"\n\t\t\t/>\n\t\t\t{m[\"settings.conversion.title\"]()}\n\t\t</h2>\n\t\t<div class=\"flex flex-col gap-4\">\n\t\t\t<div class=\"flex flex-col gap-4\">\n\t\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t\t<p class=\"text-base font-bold\">\n\t\t\t\t\t\t{m[\"settings.conversion.filename_format\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t\t<p class=\"text-sm text-muted font-normal\">\n\t\t\t\t\t\t{@html sanitize(m[\"settings.conversion.filename_description\"]())}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t<FancyTextInput\n\t\t\t\t\tplaceholder=\"VERT_%name%\"\n\t\t\t\t\tbind:value={settings.filenameFormat}\n\t\t\t\t\textension={\".ext\"}\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t\t<div class=\"flex flex-col gap-4\">\n\t\t\t\t<button\n\t\t\t\t\tonclick={() => (showAdvanced = !showAdvanced)}\n\t\t\t\t\tclass=\"bg-button flex items-center justify-between p-4 rounded-lg text-black dynadark:text-white w-full\"\n\t\t\t\t>\n\t\t\t\t\t<span class=\"text-base font-bold\"\n\t\t\t\t\t\t>{m[\"settings.conversion.advanced_settings\"]()}</span\n\t\t\t\t\t>\n\t\t\t\t\t<ChevronDownIcon\n\t\t\t\t\t\tsize=\"20\"\n\t\t\t\t\t\tclass={clsx(\"transition-transform duration-300\", {\n\t\t\t\t\t\t\t\"rotate-180\": showAdvanced,\n\t\t\t\t\t\t})}\n\t\t\t\t\t/>\n\t\t\t\t</button>\n\t\t\t\t<div\n\t\t\t\t\tclass={clsx(\n\t\t\t\t\t\t\"flex flex-col gap-8 transition-all duration-300 ease-in-out\",\n\t\t\t\t\t\t{\"max-h-[2000px] opacity-100 overflow-visible\": showAdvanced},\n\t\t\t\t\t\t{\"max-h-0 opacity-0 overflow-hidden -mb-4\": !showAdvanced},\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t<div class=\"flex flex-col gap-8\">\n\t\t\t\t\t\t<div class=\"flex flex-col gap-4\">\n\t\t\t\t\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t\t\t\t\t<p class=\"text-base font-bold\">\n\t\t\t\t\t\t\t\t\t{m[\"settings.conversion.default_format\"]()}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t<p class=\"text-sm text-muted font-normal\">\n\t\t\t\t\t\t\t\t\t{m[\n\t\t\t\t\t\t\t\t\t\t\"settings.conversion.default_format_description\"\n\t\t\t\t\t\t\t\t\t]()}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"flex flex-col gap-3 w-full\">\n\t\t\t\t\t\t\t\t<div class=\"flex gap-3 w-full\">\n\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\tonclick={() =>\n\t\t\t\t\t\t\t\t\t\t\t(settings.useDefaultFormat = true)}\n\t\t\t\t\t\t\t\t\t\tclass=\"btn {$effects\n\t\t\t\t\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t\t\t\t\t: '!scale-100'} {settings.useDefaultFormat\n\t\t\t\t\t\t\t\t\t\t\t? 'selected'\n\t\t\t\t\t\t\t\t\t\t\t: ''} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<PlayIcon\n\t\t\t\t\t\t\t\t\t\t\tsize=\"24\"\n\t\t\t\t\t\t\t\t\t\t\tclass=\"inline-block mr-2\"\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t{m[\"settings.conversion.default_format_enable\"]()}\n\t\t\t\t\t\t\t\t\t</button>\n\n\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\tonclick={() =>\n\t\t\t\t\t\t\t\t\t\t\t(settings.useDefaultFormat = false)}\n\t\t\t\t\t\t\t\t\t\tclass=\"btn {$effects\n\t\t\t\t\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t\t\t\t\t: '!scale-100'} {settings.useDefaultFormat\n\t\t\t\t\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t\t\t\t\t: 'selected'} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<PauseIcon\n\t\t\t\t\t\t\t\t\t\t\tsize=\"24\"\n\t\t\t\t\t\t\t\t\t\t\tclass=\"inline-block mr-2\"\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t{m[\"settings.conversion.default_format_disable\"]()}\n\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tclass=\"grid gap-3 grid-cols-2 md:grid-cols-4\"\n\t\t\t\t\t\t\t\tclass:opacity-50={!settings.useDefaultFormat}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t\t\t\t\t\t<p class=\"text-sm font-bold\">\n\t\t\t\t\t\t\t\t\t\t{m[\n\t\t\t\t\t\t\t\t\t\t\t\"settings.conversion.default_format_image\"\n\t\t\t\t\t\t\t\t\t\t]()}\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t<FormatDropdown\n\t\t\t\t\t\t\t\t\t\tcategories={{ image: categories.image }}\n\t\t\t\t\t\t\t\t\t\tfrom={\".png\"}\n\t\t\t\t\t\t\t\t\t\tbind:selected={\n\t\t\t\t\t\t\t\t\t\t\tsettings.defaultFormat.image\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdisabled={!settings.useDefaultFormat}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t\t\t\t\t\t<p class=\"text-sm font-bold\">\n\t\t\t\t\t\t\t\t\t\t{m[\n\t\t\t\t\t\t\t\t\t\t\t\"settings.conversion.default_format_audio\"\n\t\t\t\t\t\t\t\t\t\t]()}\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t<FormatDropdown\n\t\t\t\t\t\t\t\t\t\tcategories={{ audio: categories.audio }}\n\t\t\t\t\t\t\t\t\t\tfrom={\".mp3\"}\n\t\t\t\t\t\t\t\t\t\tbind:selected={\n\t\t\t\t\t\t\t\t\t\t\tsettings.defaultFormat.audio\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdisabled={!settings.useDefaultFormat}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t\t\t\t\t\t<p class=\"text-sm font-bold\">\n\t\t\t\t\t\t\t\t\t\t{m[\n\t\t\t\t\t\t\t\t\t\t\t\"settings.conversion.default_format_video\"\n\t\t\t\t\t\t\t\t\t\t]()}\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t<FormatDropdown\n\t\t\t\t\t\t\t\t\t\tcategories={{ video: categories.video }}\n\t\t\t\t\t\t\t\t\t\tfrom={\".mp4\"}\n\t\t\t\t\t\t\t\t\t\tbind:selected={\n\t\t\t\t\t\t\t\t\t\t\tsettings.defaultFormat.video\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdisabled={!settings.useDefaultFormat}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t\t\t\t\t\t<p class=\"text-sm font-bold\">\n\t\t\t\t\t\t\t\t\t\t{m[\n\t\t\t\t\t\t\t\t\t\t\t\"settings.conversion.default_format_document\"\n\t\t\t\t\t\t\t\t\t\t]()}\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t<FormatDropdown\n\t\t\t\t\t\t\t\t\t\tcategories={{ doc: categories.doc }}\n\t\t\t\t\t\t\t\t\t\tfrom={\".docx\"}\n\t\t\t\t\t\t\t\t\t\tbind:selected={\n\t\t\t\t\t\t\t\t\t\t\tsettings.defaultFormat.document\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\tdisabled={!settings.useDefaultFormat}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"flex flex-col gap-4\">\n\t\t\t\t\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t\t\t\t\t<p class=\"text-base font-bold\">\n\t\t\t\t\t\t\t\t\t{m[\"settings.conversion.metadata\"]()}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t<p\n\t\t\t\t\t\t\t\t\tclass=\"text-sm text-muted font-normal\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{m[\n\t\t\t\t\t\t\t\t\t\t\"settings.conversion.metadata_description\"\n\t\t\t\t\t\t\t\t\t]()}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"flex flex-col gap-3 w-full\">\n\t\t\t\t\t\t\t\t<div class=\"flex gap-3 w-full\">\n\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\tonclick={() =>\n\t\t\t\t\t\t\t\t\t\t\t(settings.metadata = true)}\n\t\t\t\t\t\t\t\t\t\tclass=\"btn {$effects\n\t\t\t\t\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t\t\t\t\t: '!scale-100'} {settings.metadata\n\t\t\t\t\t\t\t\t\t\t\t? 'selected'\n\t\t\t\t\t\t\t\t\t\t\t: ''} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<PlayIcon\n\t\t\t\t\t\t\t\t\t\t\tsize=\"24\"\n\t\t\t\t\t\t\t\t\t\t\tclass=\"inline-block mr-2\"\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t{m[\"settings.conversion.keep\"]()}\n\t\t\t\t\t\t\t\t\t</button>\n\n\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\tonclick={() =>\n\t\t\t\t\t\t\t\t\t\t\t(settings.metadata = false)}\n\t\t\t\t\t\t\t\t\t\tclass=\"btn {$effects\n\t\t\t\t\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t\t\t\t\t: '!scale-100'} {settings.metadata\n\t\t\t\t\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t\t\t\t\t: 'selected'} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<PauseIcon\n\t\t\t\t\t\t\t\t\t\t\tsize=\"24\"\n\t\t\t\t\t\t\t\t\t\t\tclass=\"inline-block mr-2\"\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t{m[\"settings.conversion.remove\"]()}\n\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"flex flex-col gap-4\">\n\t\t\t\t\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t\t\t\t\t<p class=\"text-base font-bold\">\n\t\t\t\t\t\t\t\t\t{m[\"settings.conversion.quality\"]()}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t<p class=\"text-sm text-muted font-normal\">\n\t\t\t\t\t\t\t\t\t{m[\n\t\t\t\t\t\t\t\t\t\t\"settings.conversion.quality_description\"\n\t\t\t\t\t\t\t\t\t]()}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"grid grid-cols-2 gap-3\">\n\t\t\t\t\t\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t\t\t\t\t\t<p class=\"text-sm font-bold\">\n\t\t\t\t\t\t\t\t\t\t{m[\n\t\t\t\t\t\t\t\t\t\t\t\"settings.conversion.quality_images\"\n\t\t\t\t\t\t\t\t\t\t]()}\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t<FancyInput\n\t\t\t\t\t\t\t\t\t\tbind:value={\n\t\t\t\t\t\t\t\t\t\t\tsettings.magickQuality as unknown as string\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\t\t\tmin={1}\n\t\t\t\t\t\t\t\t\t\tmax={100}\n\t\t\t\t\t\t\t\t\t\tplaceholder={\"100\"}\n\t\t\t\t\t\t\t\t\t\textension={\"%\"}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t\t\t\t\t\t<p class=\"text-sm font-bold\">\n\t\t\t\t\t\t\t\t\t\t{m[\n\t\t\t\t\t\t\t\t\t\t\t\"settings.conversion.quality_audio\"\n\t\t\t\t\t\t\t\t\t\t]()}\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t<Dropdown\n\t\t\t\t\t\t\t\t\t\toptions={CONVERSION_BITRATES.map((b) =>\n\t\t\t\t\t\t\t\t\t\t\tb.toString(),\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\tselected={settings.ffmpegQuality.toString()}\n\t\t\t\t\t\t\t\t\t\tonselect={(option: string) =>\n\t\t\t\t\t\t\t\t\t\t\t(settings.ffmpegQuality =\n\t\t\t\t\t\t\t\t\t\t\t\toption as ConversionBitrate)}\n\t\t\t\t\t\t\t\t\t\tsettingsStyle\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div class=\"grid grid-cols-2 gap-3\">\n\t\t\t\t\t\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t\t\t\t\t\t<p class=\"text-sm font-bold\">\n\t\t\t\t\t\t\t\t\t\t{m[\"settings.conversion.rate\"]()}\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t<Dropdown\n\t\t\t\t\t\t\t\t\t\toptions={SAMPLE_RATES.map((r) =>\n\t\t\t\t\t\t\t\t\t\t\tr.toString(),\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\tselected={settings.ffmpegSampleRate.toString()}\n\t\t\t\t\t\t\t\t\t\tonselect={(option: string) => {\n\t\t\t\t\t\t\t\t\t\t\tsettings.ffmpegSampleRate =\n\t\t\t\t\t\t\t\t\t\t\t\toption as SampleRate;\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\tsettingsStyle\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t\t\t\t\t\t<p class=\"text-sm font-bold select-none\">\n\t\t\t\t\t\t\t\t\t\t&nbsp;&nbsp;\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t<FancyInput\n\t\t\t\t\t\t\t\t\t\tbind:value={\n\t\t\t\t\t\t\t\t\t\t\tsettings.ffmpegCustomSampleRate as unknown as string\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\t\t\tmin={1}\n\t\t\t\t\t\t\t\t\t\tplaceholder={\"44100\"}\n\t\t\t\t\t\t\t\t\t\textension={\"Hz\"}\n\t\t\t\t\t\t\t\t\t\tdisabled={settings.ffmpegSampleRate !==\n\t\t\t\t\t\t\t\t\t\t\t\"custom\"}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div></Panel\n>\n"
  },
  {
    "path": "src/lib/sections/settings/Privacy.svelte",
    "content": "<script lang=\"ts\">\n\timport Panel from \"$lib/components/visual/Panel.svelte\";\n\timport {\n\t\tChartColumnIcon,\n\t\tPauseIcon,\n\t\tPlayIcon,\n\t\tRefreshCwIcon,\n\t\tTrash2Icon,\n\t} from \"lucide-svelte\";\n\timport type { ISettings } from \"./index.svelte\";\n\timport { effects } from \"$lib/store/index.svelte\";\n\timport { m } from \"$lib/paraglide/messages\";\n\timport { link, sanitize } from \"$lib/store/index.svelte\";\n\timport { swManager, type CacheInfo } from \"$lib/util/sw\";\n\timport { onMount } from \"svelte\";\n\timport { error } from \"$lib/util/logger\";\n\timport { ToastManager } from \"$lib/util/toast.svelte\";\n\timport { DISABLE_ALL_EXTERNAL_REQUESTS } from \"$lib/util/consts\";\n\timport { addDialog } from \"$lib/store/DialogProvider\";\n\n\tconst { settings = $bindable() }: { settings: ISettings } = $props();\n\n\tlet cacheInfo = $state<CacheInfo | null>(null);\n\tlet isLoadingCache = $state(false);\n\n\tasync function loadCacheInfo() {\n\t\tif (isLoadingCache) return;\n\t\tisLoadingCache = true;\n\t\ttry {\n\t\t\tawait swManager.init();\n\n\t\t\tif (\"serviceWorker\" in navigator) {\n\t\t\t\tawait navigator.serviceWorker.ready;\n\t\t\t}\n\n\t\t\tif (!navigator.serviceWorker.controller) {\n\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, 500));\n\t\t\t}\n\n\t\t\tcacheInfo = await swManager.getCacheInfo();\n\t\t} catch (err) {\n\t\t\terror([\"privacy\", \"cache\"], \"Failed to load cache info:\", err);\n\t\t} finally {\n\t\t\tisLoadingCache = false;\n\t\t}\n\t}\n\n\tasync function clearCache() {\n\t\tif (isLoadingCache) return;\n\t\tisLoadingCache = true;\n\t\ttry {\n\t\t\tawait swManager.clearCache();\n\t\t\tcacheInfo = null;\n\t\t\tawait loadCacheInfo();\n\t\t\tToastManager.add({\n\t\t\t\ttype: \"success\",\n\t\t\t\tmessage: m[\"settings.privacy.cache_cleared\"](),\n\t\t\t});\n\t\t} catch (err) {\n\t\t\terror([\"privacy\", \"cache\"], \"Failed to clear cache:\", err);\n\t\t\tToastManager.add({\n\t\t\t\ttype: \"error\",\n\t\t\t\tmessage: m[\"settings.privacy.cache_clear_error\"](),\n\t\t\t});\n\t\t} finally {\n\t\t\tisLoadingCache = false;\n\t\t}\n\t}\n\n\tasync function clearAllData() {\n\t\tif (isLoadingCache) return;\n\n\t\taddDialog(\n\t\t\tm[\"settings.privacy.clear_all_data_confirm_title\"](),\n\t\t\tm[\"settings.privacy.clear_all_data_confirm\"](),\n\t\t\t[\n\t\t\t\t{\n\t\t\t\t\ttext: m[\"settings.privacy.clear_all_data_cancel\"](),\n\t\t\t\t\taction: () => {},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttext: m[\"settings.privacy.clear_all_data\"](),\n\t\t\t\t\taction: async () => {\n\t\t\t\t\t\tisLoadingCache = true;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait swManager.clearCache();\n\t\t\t\t\t\t\tlocalStorage.clear();\n\t\t\t\t\t\t\tsessionStorage.clear();\n\n\t\t\t\t\t\t\tToastManager.add({\n\t\t\t\t\t\t\t\ttype: \"success\",\n\t\t\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\t\t\tm[\"settings.privacy.all_data_cleared\"](),\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\t\t\twindow.location.href = \"/\";\n\t\t\t\t\t\t\t}, 1500);\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\terror(\n\t\t\t\t\t\t\t\t[\"privacy\", \"data\"],\n\t\t\t\t\t\t\t\t`Failed to clear all data: ${err}`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tToastManager.add({\n\t\t\t\t\t\t\t\ttype: \"error\",\n\t\t\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\t\t\tm[\n\t\t\t\t\t\t\t\t\t\t\"settings.privacy.all_data_clear_error\"\n\t\t\t\t\t\t\t\t\t](),\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} finally {\n\t\t\t\t\t\t\tisLoadingCache = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t],\n\t\t\t\"warning\",\n\t\t);\n\t}\n\n\tonMount(() => {\n\t\tloadCacheInfo();\n\t});\n</script>\n\n<Panel class=\"flex flex-col gap-8 p-6\">\n\t<div class=\"flex flex-col gap-3\">\n\t\t<h2 class=\"text-2xl font-bold\">\n\t\t\t<ChartColumnIcon\n\t\t\t\tsize=\"40\"\n\t\t\t\tclass=\"inline-block -mt-1 mr-2 bg-accent-blue p-2 rounded-full\"\n\t\t\t\tcolor=\"black\"\n\t\t\t/>\n\t\t\t{m[\"settings.privacy.title\"]()}\n\t\t</h2>\n\t\t<div class=\"flex flex-col gap-8\">\n\t\t\t{#if !DISABLE_ALL_EXTERNAL_REQUESTS}\n\t\t\t\t<div class=\"flex flex-col gap-4\">\n\t\t\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t\t\t<p class=\"text-base font-bold\">\n\t\t\t\t\t\t\t{m[\"settings.privacy.plausible_title\"]()}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<p class=\"text-sm text-muted font-normal\">\n\t\t\t\t\t\t\t{@html link(\n\t\t\t\t\t\t\t\t[\"plausible_link\", \"analytics_link\"],\n\t\t\t\t\t\t\t\tm[\"settings.privacy.plausible_description\"](),\n\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\"https://plausible.io/privacy-focused-web-analytics\",\n\t\t\t\t\t\t\t\t\t\"https://ats.vert.sh/vert.sh\",\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"flex flex-col gap-3 w-full\">\n\t\t\t\t\t\t<div class=\"flex gap-3 w-full\">\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tonclick={() => (settings.plausible = true)}\n\t\t\t\t\t\t\t\tclass=\"btn {$effects\n\t\t\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t\t\t: '!scale-100'} {settings.plausible\n\t\t\t\t\t\t\t\t\t? 'selected'\n\t\t\t\t\t\t\t\t\t: ''} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<PlayIcon size=\"24\" class=\"inline-block mr-2\" />\n\t\t\t\t\t\t\t\t{m[\"settings.privacy.opt_in\"]()}\n\t\t\t\t\t\t\t</button>\n\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tonclick={() => (settings.plausible = false)}\n\t\t\t\t\t\t\t\tclass=\"btn {$effects\n\t\t\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t\t\t: '!scale-100'} {settings.plausible\n\t\t\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t\t\t: 'selected'} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<PauseIcon\n\t\t\t\t\t\t\t\t\tsize=\"24\"\n\t\t\t\t\t\t\t\t\tclass=\"inline-block mr-2\"\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t{m[\"settings.privacy.opt_out\"]()}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t{/if}\n\t\t\t<div class=\"flex flex-col gap-4\">\n\t\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t\t<p class=\"text-base font-bold\">\n\t\t\t\t\t\t{m[\"settings.privacy.cache_title\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t\t<p class=\"text-sm text-muted font-normal\">\n\t\t\t\t\t\t{m[\"settings.privacy.cache_description\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"grid grid-cols-2 gap-4\">\n\t\t\t\t\t<div class=\"bg-button p-4 rounded-lg\">\n\t\t\t\t\t\t<div class=\"text-sm text-muted\">\n\t\t\t\t\t\t\t{m[\"settings.privacy.total_size\"]()}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"text-lg font-bold flex items-center gap-2\">\n\t\t\t\t\t\t\t{#if isLoadingCache}\n\t\t\t\t\t\t\t\t<RefreshCwIcon size=\"16\" class=\"animate-spin\" />\n\t\t\t\t\t\t\t\t{m[\"settings.privacy.loading_cache\"]()}\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t{cacheInfo\n\t\t\t\t\t\t\t\t\t? swManager.formatSize(cacheInfo.totalSize)\n\t\t\t\t\t\t\t\t\t: \"0 B\"}\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"bg-button p-4 rounded-lg\">\n\t\t\t\t\t\t<div class=\"text-sm text-muted\">\n\t\t\t\t\t\t\t{m[\"settings.privacy.files_cached_label\"]()}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"text-lg font-bold flex items-center gap-2\">\n\t\t\t\t\t\t\t{#if isLoadingCache}\n\t\t\t\t\t\t\t\t<RefreshCwIcon size=\"16\" class=\"animate-spin\" />\n\t\t\t\t\t\t\t\t{m[\"settings.privacy.loading_cache\"]()}\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t{cacheInfo?.fileCount ?? 0}\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"flex gap-3 w-full\">\n\t\t\t\t\t<button\n\t\t\t\t\t\tonclick={loadCacheInfo}\n\t\t\t\t\t\tclass=\"btn {$effects\n\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t: '!scale-100'} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center\"\n\t\t\t\t\t\tdisabled={isLoadingCache}\n\t\t\t\t\t>\n\t\t\t\t\t\t<RefreshCwIcon size=\"24\" class=\"inline-block mr-2\" />\n\t\t\t\t\t\t{m[\"settings.privacy.refresh_cache\"]()}\n\t\t\t\t\t</button>\n\t\t\t\t\t<button\n\t\t\t\t\t\tonclick={clearCache}\n\t\t\t\t\t\tclass=\"btn {$effects\n\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t: '!scale-100'} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center\"\n\t\t\t\t\t\tdisabled={isLoadingCache}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Trash2Icon size=\"24\" class=\"inline-block mr-2\" />\n\t\t\t\t\t\t{m[\"settings.privacy.clear_cache\"]()}\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<div class=\"flex flex-col gap-4\">\n\t\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t\t<p class=\"text-base font-bold\">\n\t\t\t\t\t\t{m[\"settings.privacy.site_data_title\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t\t<p class=\"text-sm text-muted font-normal\">\n\t\t\t\t\t\t{m[\"settings.privacy.site_data_description\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\n\t\t\t\t<button\n\t\t\t\t\tonclick={clearAllData}\n\t\t\t\t\tclass=\"btn {$effects\n\t\t\t\t\t\t? ''\n\t\t\t\t\t\t: '!scale-100'} w-full p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center\"\n\t\t\t\t\tdisabled={isLoadingCache}\n\t\t\t\t>\n\t\t\t\t\t<Trash2Icon size=\"24\" class=\"inline-block mr-2\" />\n\t\t\t\t\t{m[\"settings.privacy.clear_all_data\"]()}\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t</div>\n\t</div></Panel\n>\n"
  },
  {
    "path": "src/lib/sections/settings/Vertd.svelte",
    "content": "<script lang=\"ts\">\n\timport Panel from \"$lib/components/visual/Panel.svelte\";\n\timport { GITHUB_URL_VERTD } from \"$lib/util/consts\";\n\timport { ServerIcon } from \"lucide-svelte\";\n\timport type { ISettings } from \"./index.svelte\";\n\timport clsx from \"clsx\";\n\timport Dropdown from \"$lib/components/functional/Dropdown.svelte\";\n\timport { vertdLoaded } from \"$lib/store/index.svelte\";\n\timport { m } from \"$lib/paraglide/messages\";\n\timport { link, sanitize } from \"$lib/store/index.svelte\";\n\timport { VertdInstance, type VertdInner } from \"./vertdSettings.svelte\";\n\n\tlet vertdCommit = $state<string | null>(null);\n\tlet abortController: AbortController | null = null;\n\n\tconst { settings = $bindable() }: { settings: ISettings } = $props();\n\n\t$effect(() => {\n\t\tif (abortController) abortController.abort();\n\t\tabortController = new AbortController();\n\t\tconst { signal } = abortController;\n\n\t\tvertdCommit = \"loading\";\n\t\tVertdInstance.instance\n\t\t\t.url()\n\t\t\t.then((u) => fetch(`${u}/api/version`, { signal }))\n\t\t\t.then((res) => {\n\t\t\t\tif (!res.ok) throw new Error(\"bad response\");\n\t\t\t\tvertdLoaded.set(false);\n\t\t\t\treturn res.json();\n\t\t\t})\n\t\t\t.then((data) => {\n\t\t\t\tvertdCommit = data.data;\n\t\t\t\tvertdLoaded.set(true);\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\tif (err.name !== \"AbortError\") {\n\t\t\t\t\tvertdCommit = null;\n\t\t\t\t\tvertdLoaded.set(false);\n\t\t\t\t}\n\t\t\t});\n\n\t\treturn () => {\n\t\t\tif (abortController) abortController.abort();\n\t\t};\n\t});\n</script>\n\n<Panel class=\"flex flex-col gap-8 p-6\">\n\t<div class=\"flex flex-col gap-3\">\n\t\t<h2 class=\"text-2xl font-bold\">\n\t\t\t<ServerIcon\n\t\t\t\tsize=\"40\"\n\t\t\t\tclass=\"inline-block -mt-1 mr-2 bg-accent-red p-2 rounded-full overflow-visible\"\n\t\t\t\tcolor=\"black\"\n\t\t\t/>\n\t\t\t{m[\"settings.vertd.title\"]()}\n\t\t</h2>\n\t\t<p\n\t\t\tclass={clsx(\"text-sm font-normal\", {\n\t\t\t\t\"text-failure\": vertdCommit === null,\n\t\t\t\t\"text-green-700 dynadark:text-green-300\": vertdCommit !== null,\n\t\t\t\t\"!text-muted\": vertdCommit === \"loading\",\n\t\t\t})}\n\t\t>\n\t\t\t{m[\"settings.vertd.status\"]()}\n\t\t\t{vertdCommit\n\t\t\t\t? vertdCommit === \"loading\"\n\t\t\t\t\t? m[\"settings.vertd.loading\"]()\n\t\t\t\t\t: m[\"settings.vertd.available\"]({ commitId: vertdCommit })\n\t\t\t\t: m[\"settings.vertd.unavailable\"]()}\n\t\t</p>\n\t\t<div class=\"flex flex-col gap-8\">\n\t\t\t<div class=\"flex flex-col gap-4\">\n\t\t\t\t<p class=\"text-sm text-muted font-normal\">\n\t\t\t\t\t{@html sanitize(m[\"settings.vertd.description\"]())}\n\t\t\t\t</p>\n\t\t\t\t<p class=\"text-sm text-muted font-normal\">\n\t\t\t\t\t{@html sanitize(link(\n\t\t\t\t\t\t\"vertd_link\",\n\t\t\t\t\t\tm[\"settings.vertd.hosting_info\"](),\n\t\t\t\t\t\tGITHUB_URL_VERTD,\n\t\t\t\t\t))}\n\t\t\t\t</p>\n\t\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t\t<p class=\"text-base font-bold\">\n\t\t\t\t\t\t{m[\"settings.vertd.instance\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t\t<Dropdown\n\t\t\t\t\t\toptions={[\n\t\t\t\t\t\t\tm[\"settings.vertd.auto_instance\"](),\n\t\t\t\t\t\t\tm[\"settings.vertd.eu_instance\"](),\n\t\t\t\t\t\t\tm[\"settings.vertd.us_instance\"](),\n\t\t\t\t\t\t\tm[\"settings.vertd.custom_instance\"](),\n\t\t\t\t\t\t]}\n\t\t\t\t\t\tonselect={(selected) => {\n\t\t\t\t\t\t\tlet inner: VertdInner;\n\t\t\t\t\t\t\tswitch (selected) {\n\t\t\t\t\t\t\t\tcase m[\"settings.vertd.auto_instance\"]():\n\t\t\t\t\t\t\t\t\tinner = { type: \"auto\" };\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\tcase m[\"settings.vertd.eu_instance\"]():\n\t\t\t\t\t\t\t\t\tinner = { type: \"eu\" };\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\tcase m[\"settings.vertd.us_instance\"]():\n\t\t\t\t\t\t\t\t\tinner = { type: \"us\" };\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\tcase m[\"settings.vertd.custom_instance\"]():\n\t\t\t\t\t\t\t\t\tinner = {\n\t\t\t\t\t\t\t\t\t\ttype: \"custom\",\n\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\t\tinner = { type: \"auto\" };\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tVertdInstance.instance.set(inner);\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tselected={(() => {\n\t\t\t\t\t\t\tswitch (VertdInstance.instance.innerData().type) {\n\t\t\t\t\t\t\t\tcase \"auto\":\n\t\t\t\t\t\t\t\t\treturn m[\"settings.vertd.auto_instance\"]();\n\t\t\t\t\t\t\t\tcase \"eu\":\n\t\t\t\t\t\t\t\t\treturn m[\"settings.vertd.eu_instance\"]();\n\t\t\t\t\t\t\t\tcase \"us\":\n\t\t\t\t\t\t\t\t\treturn m[\"settings.vertd.us_instance\"]();\n\t\t\t\t\t\t\t\tcase \"custom\":\n\t\t\t\t\t\t\t\t\treturn m[\n\t\t\t\t\t\t\t\t\t\t\"settings.vertd.custom_instance\"\n\t\t\t\t\t\t\t\t\t]();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t})()}\n\t\t\t\t\t\tsettingsStyle\n\t\t\t\t\t/>\n\t\t\t\t\t{#if VertdInstance.instance.innerData().type === \"custom\"}\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\tplaceholder={m[\"settings.vertd.url_placeholder\"]()}\n\t\t\t\t\t\t\tbind:value={settings.vertdURL}\n\t\t\t\t\t\t/>\n\t\t\t\t\t{/if}\n\t\t\t\t</div>\n\t\t\t\t<div class=\"flex flex-col gap-4\">\n\t\t\t\t\t<div class=\"flex flex-col gap-2\">\n\t\t\t\t\t\t<p class=\"text-base font-bold\">\n\t\t\t\t\t\t\t{m[\"settings.vertd.conversion_speed\"]()}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<p class=\"text-sm text-muted font-normal\">\n\t\t\t\t\t\t\t{m[\"settings.vertd.speed_description\"]()}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\t\t\t\t\t<Dropdown\n\t\t\t\t\t\toptions={[\n\t\t\t\t\t\t\tm[\"settings.vertd.speeds.very_slow\"](),\n\t\t\t\t\t\t\tm[\"settings.vertd.speeds.slower\"](),\n\t\t\t\t\t\t\tm[\"settings.vertd.speeds.slow\"](),\n\t\t\t\t\t\t\tm[\"settings.vertd.speeds.medium\"](),\n\t\t\t\t\t\t\tm[\"settings.vertd.speeds.fast\"](),\n\t\t\t\t\t\t\tm[\"settings.vertd.speeds.ultra_fast\"](),\n\t\t\t\t\t\t]}\n\t\t\t\t\t\tsettingsStyle\n\t\t\t\t\t\tselected={(() => {\n\t\t\t\t\t\t\tswitch (settings.vertdSpeed) {\n\t\t\t\t\t\t\t\tcase \"verySlow\":\n\t\t\t\t\t\t\t\t\treturn m[\n\t\t\t\t\t\t\t\t\t\t\"settings.vertd.speeds.very_slow\"\n\t\t\t\t\t\t\t\t\t]();\n\t\t\t\t\t\t\t\tcase \"slower\":\n\t\t\t\t\t\t\t\t\treturn m[\"settings.vertd.speeds.slower\"]();\n\t\t\t\t\t\t\t\tcase \"slow\":\n\t\t\t\t\t\t\t\t\treturn m[\"settings.vertd.speeds.slow\"]();\n\t\t\t\t\t\t\t\tcase \"medium\":\n\t\t\t\t\t\t\t\t\treturn m[\"settings.vertd.speeds.medium\"]();\n\t\t\t\t\t\t\t\tcase \"fast\":\n\t\t\t\t\t\t\t\t\treturn m[\"settings.vertd.speeds.fast\"]();\n\t\t\t\t\t\t\t\tcase \"ultraFast\":\n\t\t\t\t\t\t\t\t\treturn m[\n\t\t\t\t\t\t\t\t\t\t\"settings.vertd.speeds.ultra_fast\"\n\t\t\t\t\t\t\t\t\t]();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t})()}\n\t\t\t\t\t\tonselect={(selected) => {\n\t\t\t\t\t\t\tswitch (selected) {\n\t\t\t\t\t\t\t\tcase m[\"settings.vertd.speeds.very_slow\"]():\n\t\t\t\t\t\t\t\t\tsettings.vertdSpeed = \"verySlow\";\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\tcase m[\"settings.vertd.speeds.slower\"]():\n\t\t\t\t\t\t\t\t\tsettings.vertdSpeed = \"slower\";\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\tcase m[\"settings.vertd.speeds.slow\"]():\n\t\t\t\t\t\t\t\t\tsettings.vertdSpeed = \"slow\";\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\tcase m[\"settings.vertd.speeds.medium\"]():\n\t\t\t\t\t\t\t\t\tsettings.vertdSpeed = \"medium\";\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\tcase m[\"settings.vertd.speeds.fast\"]():\n\t\t\t\t\t\t\t\t\tsettings.vertdSpeed = \"fast\";\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\tcase m[\"settings.vertd.speeds.ultra_fast\"]():\n\t\t\t\t\t\t\t\t\tsettings.vertdSpeed = \"ultraFast\";\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</Panel>\n"
  },
  {
    "path": "src/lib/sections/settings/index.svelte.ts",
    "content": "import { PUB_VERTD_URL } from \"$env/static/public\";\nimport type { ConversionBitrate } from \"$lib/converters/ffmpeg.svelte\";\nimport type { ConversionSpeed } from \"$lib/converters/vertd.svelte\";\nimport { VertdInstance } from \"./vertdSettings.svelte\";\n\nexport { default as Appearance } from \"./Appearance.svelte\";\nexport { default as Conversion } from \"./Conversion.svelte\";\nexport { default as Vertd } from \"./Vertd.svelte\";\nexport { default as Privacy } from \"./Privacy.svelte\";\n\n// TODO: clean up settings & button code (componetize)\n\nexport interface DefaultFormats {\n\timage: string;\n\tvideo: string;\n\taudio: string;\n\tdocument: string;\n}\nexport interface ISettings {\n\tfilenameFormat: string;\n\tdefaultFormat: DefaultFormats;\n\tuseDefaultFormat: boolean;\n\tmetadata: boolean;\n\tplausible: boolean;\n\tvertdURL: string;\n\tvertdSpeed: ConversionSpeed; // videos\n\tmagickQuality: number; // images\n\tffmpegQuality: ConversionBitrate; // audio (or audio <-> video)\n\tffmpegSampleRate: string; // audio (or audio <-> video)\n\tffmpegCustomSampleRate: number; // audio (or audio <-> video) - only used when ffmpegSampleRate is \"custom\"\n\tvertdBlockedHashes: Map<string, Date[]>; // hashes of files blocked from vertd conversion\n}\n\nexport class Settings {\n\tpublic static instance = new Settings();\n\n\tpublic settings: ISettings = $state({\n\t\tfilenameFormat: \"VERT_%name%\",\n\t\tdefaultFormat: {\n\t\t\timage: \".png\",\n\t\t\tvideo: \".mp4\",\n\t\t\taudio: \".mp3\",\n\t\t\tdocument: \".docx\",\n\t\t},\n\t\tuseDefaultFormat: false,\n\t\tmetadata: true,\n\t\tplausible: true,\n\t\tvertdURL: PUB_VERTD_URL,\n\t\tvertdSpeed: \"slow\",\n\t\tmagickQuality: 100,\n\t\tffmpegQuality: \"auto\",\n\t\tffmpegSampleRate: \"auto\",\n\t\tffmpegCustomSampleRate: 44100,\n\t\tvertdBlockedHashes: new Map<string, Date[]>(),\n\t});\n\n\tpublic save() {\n\t\tlocalStorage.setItem(\"settings\", JSON.stringify(this.settings));\n\t\tVertdInstance.instance.save();\n\t}\n\n\tpublic load() {\n\t\ttry {\n\t\t\tVertdInstance.instance.load();\n\t\t\tconst ls = localStorage.getItem(\"settings\");\n\t\t\tif (!ls) return;\n\t\t\tconst settings: ISettings = JSON.parse(ls);\n\t\t\tconst vertdBlockedHashes = new Map<string, Date[]>(\n\t\t\t\tObject.entries(\n\t\t\t\t\tsettings.vertdBlockedHashes ||\n\t\t\t\t\t\tthis.settings.vertdBlockedHashes,\n\t\t\t\t),\n\t\t\t);\n\n\t\t\tsettings.vertdBlockedHashes = vertdBlockedHashes;\n\n\t\t\tthis.settings = {\n\t\t\t\t...this.settings,\n\t\t\t\t...settings,\n\t\t\t};\n\t\t} catch {\n\t\t\t// ignore errors, use default settings\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/lib/sections/settings/vertdSettings.svelte.ts",
    "content": "import { ip, type IpInfo } from \"$lib/util/ip\";\nimport { Settings } from \"./index.svelte\";\nimport { PUB_VERTD_URL } from \"$env/static/public\";\n\nconst LOCATIONS = [\n\t{\n\t\tlatitude: 49.0976,\n\t\tlongitude: 12.4869,\n\t\turl: \"https://eu.vertd.vert.sh\",\n\t},\n\t{\n\t\tlatitude: 47.6587,\n\t\tlongitude: -117.426,\n\t\turl: \"https://usa.vertd.vert.sh\",\n\t},\n];\n\nconst toRad = (value: number) => (value * Math.PI) / 180;\nconst haversine = (lat1: number, lon1: number, lat2: number, lon2: number) => {\n\tconst R = 6371; // km\n\tconst dLat = toRad(lat2 - lat1);\n\tconst dLon = toRad(lon2 - lon1);\n\tconst a =\n\t\tMath.sin(dLat / 2) * Math.sin(dLat / 2) +\n\t\tMath.cos(toRad(lat1)) *\n\t\t\tMath.cos(toRad(lat2)) *\n\t\t\tMath.sin(dLon / 2) *\n\t\t\tMath.sin(dLon / 2);\n\tconst c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n\tconst d = R * c;\n\treturn d;\n};\n\nexport type VertdInner =\n\t| { type: \"auto\" }\n\t| { type: \"eu\" }\n\t| { type: \"us\" }\n\t| { type: \"custom\" };\n\nexport class VertdInstance {\n\tpublic static instance = new VertdInstance();\n\n\tprivate cachedIp = $state<IpInfo | null>(null);\n\n\tprivate inner = $state<VertdInner>({\n\t\ttype: \"auto\",\n\t});\n\n\tpublic save() {\n\t\tlocalStorage.setItem(\"vertdInstance\", JSON.stringify(this.inner));\n\t}\n\n\tpublic load() {\n\t\tconst ls = localStorage.getItem(\"vertdInstance\");\n\n\t\t// if custom vertd url and no saved setting, default to the custom url\n\t\tif (!ls) {\n\t\t\tconst isCustomUrl =\n\t\t\t\tPUB_VERTD_URL && PUB_VERTD_URL !== \"https://vertd.vert.sh\";\n\t\t\tif (isCustomUrl) {\n\t\t\t\tthis.inner = { type: \"custom\" };\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tif (!ls) return;\n\t\tconst inner: VertdInner = JSON.parse(ls);\n\t\tthis.inner = {\n\t\t\t...this.inner,\n\t\t\t...inner,\n\t\t};\n\t}\n\n\tpublic innerData() {\n\t\treturn this.inner;\n\t}\n\n\tpublic set(inner: VertdInner) {\n\t\tthis.inner = inner;\n\t\tthis.save();\n\t}\n\n\tpublic async url() {\n\t\tconst reachable = async (url: string) => {\n\t\t\ttry {\n\t\t\t\tconst res = await fetch(url + \"/api/version\", {\n\t\t\t\t\tmethod: \"GET\",\n\t\t\t\t\tcache: \"no-store\",\n\t\t\t\t});\n\t\t\t\treturn res.ok;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t};\n\n\t\tswitch (this.inner.type) {\n\t\t\tcase \"auto\": {\n\t\t\t\tif (!this.cachedIp) this.cachedIp = await ip();\n\t\t\t\tconst ipInfo = this.cachedIp;\n\t\t\t\tconst primary = this.geographicallyOptimalInstance(ipInfo);\n\n\t\t\t\t// try primary (closest) first\n\t\t\t\tif (await reachable(primary)) return primary;\n\n\t\t\t\t// fall back to other locations\n\t\t\t\tfor (const location of LOCATIONS) {\n\t\t\t\t\tif (location.url === primary) continue;\n\t\t\t\t\tif (await reachable(location.url)) return location.url;\n\t\t\t\t}\n\n\t\t\t\t// if none are reachable, fall back to custom\n\t\t\t\treturn Settings.instance.settings.vertdURL;\n\t\t\t}\n\n\t\t\tcase \"eu\": {\n\t\t\t\treturn \"https://eu.vertd.vert.sh\";\n\t\t\t}\n\n\t\t\tcase \"us\": {\n\t\t\t\treturn \"https://usa.vertd.vert.sh\";\n\t\t\t}\n\n\t\t\tcase \"custom\": {\n\t\t\t\treturn Settings.instance.settings.vertdURL;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate geographicallyOptimalInstance(ip: IpInfo) {\n\t\tlet bestLocation = LOCATIONS[0];\n\t\tlet bestDistance = haversine(\n\t\t\tip.latitude,\n\t\t\tip.longitude,\n\t\t\tbestLocation.latitude,\n\t\t\tbestLocation.longitude,\n\t\t);\n\n\t\tfor (let i = 1; i < LOCATIONS.length; i++) {\n\t\t\tconst location = LOCATIONS[i];\n\t\t\tconst distance = haversine(\n\t\t\t\tip.latitude,\n\t\t\t\tip.longitude,\n\t\t\t\tlocation.latitude,\n\t\t\t\tlocation.longitude,\n\t\t\t);\n\t\t\tif (distance < bestDistance) {\n\t\t\t\tbestDistance = distance;\n\t\t\t\tbestLocation = location;\n\t\t\t}\n\t\t}\n\n\t\treturn bestLocation.url;\n\t}\n}\n"
  },
  {
    "path": "src/lib/store/DialogProvider.ts",
    "content": "import type { Component } from \"svelte\";\nimport { writable } from \"svelte/store\";\n\ntype DialogType = \"success\" | \"error\" | \"info\" | \"warning\";\n\ntype BaseDialog = {\n\tid: number;\n\ttitle: string;\n\tbuttons: {\n\t\ttext: string;\n\t\taction: () => void;\n\t}[];\n\ttype: DialogType;\n};\n\nexport type StringDialog = BaseDialog & {\n\tmessage: string;\n};\n\nexport type ComponentDialog<T = unknown> = BaseDialog & {\n\tmessage: Component<DialogProps<T>>;\n\tadditional: T;\n};\n\nexport type Dialog<T = unknown> = StringDialog | ComponentDialog<T>;\n\nexport type DialogProps<T = unknown> = {\n\tid: number;\n\ttitle: string;\n\ttype: DialogType;\n\tbuttons: {\n\t\ttext: string;\n\t\taction: () => void;\n\t}[];\n\tadditional: T;\n};\n\nconst dialogs = writable<Dialog[]>([]);\n\nlet dialogId = 0;\n\nfunction addDialog(\n\ttitle: string,\n\tmessage: string | Component<DialogProps>,\n\tbuttons: BaseDialog[\"buttons\"],\n\ttype: DialogType,\n\tadditional?: unknown,\n): number {\n\tconst id = dialogId++;\n\n\tif (typeof message === \"string\") {\n\t\tconst newDialog: StringDialog = {\n\t\t\tid,\n\t\t\ttitle,\n\t\t\tmessage,\n\t\t\tbuttons,\n\t\t\ttype,\n\t\t};\n\t\tdialogs.update((currentDialogs) => [...currentDialogs, newDialog]);\n\t} else {\n\t\tconst newDialog: ComponentDialog = {\n\t\t\tid,\n\t\t\ttitle,\n\t\t\tmessage,\n\t\t\tbuttons,\n\t\t\ttype,\n\t\t\tadditional,\n\t\t};\n\t\tdialogs.update((currentDialogs) => [...currentDialogs, newDialog]);\n\t}\n\n\treturn id;\n}\n\nfunction removeDialog(id: number) {\n\tdialogs.update((currentDialogs) =>\n\t\tcurrentDialogs.filter((dialog) => dialog.id !== id),\n\t);\n}\n\nexport { dialogs, addDialog, removeDialog };\n"
  },
  {
    "path": "src/lib/store/index.svelte.ts",
    "content": "import { browser } from \"$app/environment\";\nimport { byNative, converters } from \"$lib/converters\";\nimport { error, log } from \"$lib/util/logger\";\nimport { VertFile } from \"$lib/types\";\nimport { parseBlob, selectCover } from \"music-metadata\";\nimport { writable } from \"svelte/store\";\nimport { addDialog } from \"./DialogProvider\";\nimport PQueue from \"p-queue\";\nimport { getLocale, setLocale } from \"$lib/paraglide/runtime\";\nimport { m } from \"$lib/paraglide/messages\";\nimport sanitizeHtml from \"sanitize-html\";\nimport { ToastManager } from \"$lib/util/toast.svelte\";\nimport { GB } from \"$lib/util/consts\";\n\nclass Files {\n\tpublic files = $state<VertFile[]>([]);\n\n\tpublic requiredConverters = $derived(\n\t\tArray.from(new Set(files.files.map((f) => f.converters).flat())),\n\t);\n\n\tpublic ready = $derived(\n\t\tthis.files.length === 0\n\t\t\t? false\n\t\t\t: this.requiredConverters.every((f) => f?.status === \"ready\") &&\n\t\t\t\t\tthis.files.every((f) => !f.processing),\n\t);\n\tpublic results = $derived(\n\t\tthis.files.length === 0 ? false : this.files.every((f) => f.result),\n\t);\n\n\tprivate thumbnailQueue = new PQueue({\n\t\tconcurrency: browser ? navigator.hardwareConcurrency || 4 : 4,\n\t});\n\n\tprivate _addThumbnail = async (file: VertFile) => {\n\t\tthis.thumbnailQueue.add(async () => {\n\t\t\tconst isAudio = converters\n\t\t\t\t.find((c) => c.name === \"ffmpeg\")\n\t\t\t\t?.supportedFormats.filter((f) => f.isNative)\n\t\t\t\t.map((f) => f.name)\n\t\t\t\t?.includes(file.from.toLowerCase());\n\t\t\tconst isVideo = converters\n\t\t\t\t.find((c) => c.name === \"vertd\")\n\t\t\t\t?.supportedFormats.filter((f) => f.isNative)\n\t\t\t\t.map((f) => f.name)\n\t\t\t\t?.includes(file.from.toLowerCase());\n\n\t\t\ttry {\n\t\t\t\tif (isAudio) {\n\t\t\t\t\t// try to get the thumbnail from the audio via music-metadata\n\t\t\t\t\tconst { common } = await parseBlob(file.file, {\n\t\t\t\t\t\tskipPostHeaders: true,\n\t\t\t\t\t});\n\t\t\t\t\tconst cover = selectCover(common.picture);\n\t\t\t\t\tif (cover) {\n\t\t\t\t\t\tconst arrayBuffer =\n\t\t\t\t\t\t\tcover.data.buffer instanceof ArrayBuffer\n\t\t\t\t\t\t\t\t? cover.data.buffer\n\t\t\t\t\t\t\t\t: new Uint8Array(cover.data).buffer;\n\t\t\t\t\t\tconst blob = new Blob([new Uint8Array(arrayBuffer)], {\n\t\t\t\t\t\t\ttype: cover.format,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tfile.blobUrl = URL.createObjectURL(blob);\n\t\t\t\t\t}\n\t\t\t\t} else if (isVideo) {\n\t\t\t\t\t// video\n\t\t\t\t\tfile.blobUrl = await this._generateThumbnailFromMedia(\n\t\t\t\t\t\tfile.file,\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\t// image\n\t\t\t\t\tfile.blobUrl = await this._generateThumbnailFromMedia(\n\t\t\t\t\t\tfile.file,\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\terror([\"files\"], e);\n\t\t\t}\n\t\t});\n\t};\n\n\tprivate async _generateThumbnailFromMedia(\n\t\tfile: File,\n\t\tisVideo: boolean,\n\t): Promise<string | undefined> {\n\t\tconst maxSize = 180;\n\t\tconst mediaElement = isVideo\n\t\t\t? document.createElement(\"video\")\n\t\t\t: new Image();\n\t\tmediaElement.src = URL.createObjectURL(file);\n\n\t\tawait new Promise((resolve, reject) => {\n\t\t\tif (isVideo) {\n\t\t\t\tconst video = mediaElement as HTMLVideoElement;\n\t\t\t\t// seek to 10% of video time or 2 seconds in\n\t\t\t\tvideo.onloadeddata = () => {\n\t\t\t\t\tconst seekTime = Math.min(video.duration * 0.1, 2);\n\t\t\t\t\tvideo.currentTime = seekTime;\n\t\t\t\t};\n\t\t\t\tvideo.onseeked = resolve;\n\t\t\t\tvideo.onerror = reject;\n\t\t\t} else {\n\t\t\t\t(mediaElement as HTMLImageElement).onload = resolve;\n\t\t\t\t(mediaElement as HTMLImageElement).onerror = reject;\n\t\t\t}\n\t\t});\n\n\t\tconst canvas = document.createElement(\"canvas\");\n\t\tconst ctx = canvas.getContext(\"2d\");\n\t\tif (!ctx) return undefined;\n\n\t\tconst width = isVideo\n\t\t\t? (mediaElement as HTMLVideoElement).videoWidth\n\t\t\t: (mediaElement as HTMLImageElement).width;\n\t\tconst height = isVideo\n\t\t\t? (mediaElement as HTMLVideoElement).videoHeight\n\t\t\t: (mediaElement as HTMLImageElement).height;\n\n\t\tconst scale = Math.max(maxSize / width, maxSize / height);\n\t\tcanvas.width = width * scale;\n\t\tcanvas.height = height * scale;\n\t\tctx.drawImage(mediaElement, 0, 0, canvas.width, canvas.height);\n\n\t\t// check if completely transparent\n\t\tconst imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);\n\t\tconst isTransparent = Array.from(imageData.data).every((value, index) => {\n\t\t\treturn (index + 1) % 4 !== 0 || value === 0;\n\t\t});\n\t\tif (isTransparent) {\n\t\t\tcanvas.remove();\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst url = canvas.toDataURL();\n\t\tcanvas.remove();\n\t\treturn url;\n\t}\n\n\tprivate async _handleZipFile(file: File): Promise<void> {\n\t\ttry {\n\t\t\tlog([\"files\"], `extracting zip file: ${file.name}`);\n\t\t\tToastManager.add({\n\t\t\t\ttype: \"info\",\n\t\t\t\tmessage: m[\"convert.archive_file.extracting\"]({\n\t\t\t\t\tfilename: file.name,\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\tconst { extractZip } = await import(\"$lib/util/zip\");\n\t\t\tconst entries = await extractZip(file);\n\n\t\t\tconst totalEntries = entries.length;\n\t\t\tlog([\"files\"], `extracted ${totalEntries} files from zip`);\n\n\t\t\t// check if all files in zip use the same converter and are compatible\n\t\t\tconst convertersUsed = new Set<string>();\n\t\t\tlet incompatibleFiles = false;\n\n\t\t\tfor (const { filename } of entries) {\n\t\t\t\tconst format = \".\" + filename.split(\".\").pop()?.toLowerCase();\n\t\t\t\tif (!format || format === \".zip\") {\n\t\t\t\t\tincompatibleFiles = true;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst converter = converters\n\t\t\t\t\t.sort(byNative(format))\n\t\t\t\t\t.find((c) => c.formatStrings().includes(format));\n\n\t\t\t\tif (converter) convertersUsed.add(converter.name);\n\t\t\t\telse incompatibleFiles = true;\n\t\t\t}\n\n\t\t\tconst converterCount = convertersUsed.size;\n\t\t\tconst canConvertAsOne = converterCount === 1 && !incompatibleFiles;\n\n\t\t\tlog(\n\t\t\t\t[\"files\"],\n\t\t\t\t`extracted ${entries.length} files from zip (converters: ${converterCount}, compatible: ${canConvertAsOne})`,\n\t\t\t);\n\n\t\t\tif (canConvertAsOne) {\n\t\t\t\t// all files use same converter - add zip as a single VertFile file\n\t\t\t\tconst vf = new VertFile(file, \".zip\");\n\t\t\t\tvf.converters = converters.filter(\n\t\t\t\t\t(c) => c.name === Array.from(convertersUsed)[0],\n\t\t\t\t);\n\n\t\t\t\tconst converterName = vf.converters[0].name;\n\t\t\t\tconst type =\n\t\t\t\t\tconverterName === \"imagemagick\"\n\t\t\t\t\t\t? \"image\"\n\t\t\t\t\t\t: converterName === \"ffmpeg\"\n\t\t\t\t\t\t\t? \"audio\"\n\t\t\t\t\t\t\t: converterName === \"pandoc\"\n\t\t\t\t\t\t\t\t? \"doc\"\n\t\t\t\t\t\t\t\t: \"video\";\n\n\t\t\t\tthis.files.push(vf);\n\t\t\t\tthis._addThumbnail(vf);\n\n\t\t\t\tToastManager.add({\n\t\t\t\t\ttype: \"success\",\n\t\t\t\t\tmessage: m[\"convert.archive_file.detected\"]({\n\t\t\t\t\t\ttype: m[`convert.archive_file.${type}`](),\n\t\t\t\t\t\tfilename: file.name,\n\t\t\t\t\t}),\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\t// mixed converters/incompatible files - extract all individually\n\t\t\t\tfor (const { filename, data } of entries) {\n\t\t\t\t\tthis._add(\n\t\t\t\t\t\tnew File([new Uint8Array(data)], filename, {\n\t\t\t\t\t\t\ttype: \"application/octet-stream\",\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tToastManager.add({\n\t\t\t\t\ttype: \"success\",\n\t\t\t\t\tmessage: m[\"convert.archive_file.extracted\"]({\n\t\t\t\t\t\tfilename: file.name,\n\t\t\t\t\t\textract_count: entries.length,\n\t\t\t\t\t\tignore_count: 0,\n\t\t\t\t\t}),\n\t\t\t\t});\n\t\t\t}\n\t\t} catch (e) {\n\t\t\terror([\"files\"], `error processing zip file: ${e}`);\n\t\t\tthrow e;\n\t\t}\n\t}\n\n\tprivate _warningShown = false;\n\tprivate async _add(file: VertFile | File) {\n\t\tif (file instanceof VertFile) {\n\t\t\tthis.files.push(file);\n\t\t\tthis._addThumbnail(file);\n\t\t} else {\n\t\t\t// if zip, extract and add contents\n\t\t\tconst isZip =\n\t\t\t\tfile.name.toLowerCase().endsWith(\".zip\") ||\n\t\t\t\tfile.type === \"application/zip\" ||\n\t\t\t\tfile.type === \"application/x-zip-compressed\";\n\n\t\t\tif (isZip) {\n\t\t\t\ttry {\n\t\t\t\t\tawait this._handleZipFile(file);\n\t\t\t\t\treturn;\n\t\t\t\t} catch (err) {\n\t\t\t\t\terror([\"files\"], `error extracting zip file: ${err}`);\n\t\t\t\t\tToastManager.add({\n\t\t\t\t\t\ttype: \"error\",\n\t\t\t\t\t\tmessage: m[\"convert.archive_file.extract_error\"]({\n\t\t\t\t\t\t\tfilename: file.name,\n\t\t\t\t\t\t\terror: String(err),\n\t\t\t\t\t\t}),\n\t\t\t\t\t});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// regular files\n\t\t\tconst format = \".\" + file.name.split(\".\").pop()?.toLowerCase();\n\t\t\tif (!format) {\n\t\t\t\tlog([\"files\"], `no extension found for ${file.name}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst converter = converters\n\t\t\t\t.sort(byNative(format))\n\t\t\t\t.find((converter) => converter.formatStrings().includes(format));\n\t\t\tif (!converter) {\n\t\t\t\tlog([\"files\"], `no converter found for ${file.name}`);\n\t\t\t\tthis.files.push(new VertFile(file, format));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst to = converter.formatStrings().find((f) => f !== format);\n\t\t\tif (!to) {\n\t\t\t\tlog([\"files\"], `no output format found for ${file.name}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst vf = new VertFile(file, to);\n\t\t\tthis.files.push(vf);\n\t\t\tthis._addThumbnail(vf);\n\n\t\t\tconst convName = converter.name;\n\t\t\tif (file.size > MAX_ARRAY_BUFFER_SIZE && convName === \"vertd\") {\n\t\t\t\tToastManager.add({\n\t\t\t\t\ttype: \"warning\",\n\t\t\t\t\tmessage: m[\"convert.large_file_warning\"]({\n\t\t\t\t\t\tlimit: (MAX_ARRAY_BUFFER_SIZE / GB).toFixed(2),\n\t\t\t\t\t}),\n\t\t\t\t\tdurations: {\n\t\t\t\t\t\tstay: 10000,\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst isVideo = convName === \"vertd\";\n\t\t\tconst acceptedExternalWarning =\n\t\t\t\tlocalStorage.getItem(\"acceptedExternalWarning\") === \"true\";\n\t\t\tif (isVideo && !acceptedExternalWarning && !this._warningShown) {\n\t\t\t\tthis._warningShown = true;\n\t\t\t\tconst title = m[\"convert.external_warning.title\"]();\n\t\t\t\tconst message = m[\"convert.external_warning.text\"]();\n\t\t\t\tconst buttons = [\n\t\t\t\t\t{\n\t\t\t\t\t\ttext: m[\"convert.external_warning.no\"](),\n\t\t\t\t\t\taction: () => {\n\t\t\t\t\t\t\tthis.files = [\n\t\t\t\t\t\t\t\t...this.files.filter(\n\t\t\t\t\t\t\t\t\t(f) => !f.converters.map((c) => c.name).includes(\"vertd\"),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\tthis._warningShown = false;\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\ttext: m[\"convert.external_warning.yes\"](),\n\t\t\t\t\t\taction: () => {\n\t\t\t\t\t\t\tlocalStorage.setItem(\"acceptedExternalWarning\", \"true\");\n\t\t\t\t\t\t\tthis._warningShown = false;\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t];\n\t\t\t\taddDialog(title, message, buttons, \"warning\");\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic add(file: VertFile | null | undefined): void;\n\tpublic add(file: File | null | undefined): void;\n\tpublic add(file: File[] | null | undefined): void;\n\tpublic add(file: VertFile[] | null | undefined): void;\n\tpublic add(file: FileList | null | undefined): void;\n\tpublic add(\n\t\tfile: VertFile | File | VertFile[] | File[] | FileList | null | undefined,\n\t) {\n\t\tif (!file) return;\n\t\tif (Array.isArray(file) || file instanceof FileList) {\n\t\t\tfor (const f of file) {\n\t\t\t\tthis._add(f);\n\t\t\t}\n\t\t} else {\n\t\t\tthis._add(file);\n\t\t}\n\t}\n\n\tpublic async convertAll() {\n\t\tconst promiseFns = this.files.map((f) => () => f.convert());\n\t\tconst coreCount = navigator.hardwareConcurrency || 4;\n\t\tconst queue = new PQueue({ concurrency: coreCount });\n\t\tawait Promise.all(promiseFns.map((fn) => queue.add(fn)));\n\t}\n\n\tpublic async downloadAll() {\n\t\tif (files.files.length === 0) return;\n\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\t\tconst dlFiles: any[] = [];\n\t\tfor (let i = 0; i < files.files.length; i++) {\n\t\t\tconst file = files.files[i];\n\t\t\tconst result = file.result;\n\n\t\t\tif (!result) {\n\t\t\t\terror([\"files\"], \"No result found\");\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet to = result.to;\n\t\t\tif (!to.startsWith(\".\")) to = `.${to}`;\n\n\t\t\tdlFiles.push({\n\t\t\t\tname: file.file.name.replace(/\\.[^/.]+$/, \"\") + to,\n\t\t\t\tlastModified: Date.now(),\n\t\t\t\tinput: await result.file.arrayBuffer(),\n\t\t\t});\n\t\t}\n\t\tconst { downloadZip } = await import(\"client-zip\");\n\t\tconst blob = await downloadZip(dlFiles, \"converted.zip\").blob();\n\t\tconst url = URL.createObjectURL(blob);\n\n\t\tconst settings = JSON.parse(localStorage.getItem(\"settings\") ?? \"{}\");\n\t\tconst filenameFormat = settings.filenameFormat || \"VERT_%name%\";\n\n\t\tconst format = (name: string) => {\n\t\t\tconst date = new Date().toISOString();\n\t\t\treturn name\n\t\t\t\t.replace(/%date%/g, date)\n\t\t\t\t.replace(/%name%/g, \"Multi\")\n\t\t\t\t.replace(/%extension%/g, \"\");\n\t\t};\n\n\t\tconst a = document.createElement(\"a\");\n\t\ta.href = url;\n\t\ta.download = `${format(filenameFormat)}.zip`;\n\t\ta.click();\n\t\tURL.revokeObjectURL(url);\n\t\ta.remove();\n\t}\n}\n\nexport function setTheme(themeTo: \"light\" | \"dark\") {\n\tdocument.documentElement.classList.remove(\"light\", \"dark\");\n\tdocument.documentElement.classList.add(themeTo);\n\tlocalStorage.setItem(\"theme\", themeTo);\n\tlog([\"theme\"], `set to ${themeTo}`);\n\ttheme.set(themeTo);\n\n\t// Lock dark reader if it's set to dark mode\n\tif (themeTo === \"dark\") {\n\t\tconst lock = document.createElement(\"meta\");\n\t\tlock.name = \"darkreader-lock\";\n\t\tdocument.head.appendChild(lock);\n\t} else {\n\t\tconst lock = document.querySelector('meta[name=\"darkreader-lock\"]');\n\t\tif (lock) lock.remove();\n\t}\n}\n\nexport function setEffects(effectsEnabled: boolean) {\n\tlocalStorage.setItem(\"effects\", effectsEnabled.toString());\n\tlog([\"effects\"], `set to ${effectsEnabled}`);\n\teffects.set(effectsEnabled);\n}\n\nexport const files = new Files();\nexport const showGradient = writable(true);\nexport const gradientColor = writable(\"\");\nexport const goingLeft = writable(false);\nexport const dropping = writable(false);\nexport const vertdLoaded = writable(false);\nexport const dropdownStates = writable<Record<string, string>>({});\n\nexport const isMobile = writable(false);\nexport const effects = writable(true);\nexport const theme = writable<\"light\" | \"dark\">(\"light\");\nexport const locale = writable(getLocale());\nexport const availableLocales = {\n\ten: \"English\",\n\tes: \"Español\",\n\tfr: \"Français\",\n\tde: \"Deutsch\",\n\tit: \"Italiano\",\n\tba: \"Bosanski\",\n\thr: \"Hrvatski\",\n\tid: \"Bahasa Indonesia\",\n\ttr: \"Türkçe\",\n\tja: \"日本語\",\n\tko: \"한국어\",\n\tel: \"Ελληνικά\",\n\t\"zh-Hans\": \"简体中文\",\n\t\"zh-Hant\": \"繁體中文\",\n\t\"pt-BR\": \"Português (Brasil)\",\n};\n\nexport function updateLocale(newLocale: string) {\n\tif (!Object.keys(availableLocales).includes(newLocale)) newLocale = \"en\";\n\n\tlog([\"locale\"], `set to ${newLocale}`);\n\tlocalStorage.setItem(\"locale\", newLocale);\n\t// @ts-expect-error shush\n\tsetLocale(newLocale, { reload: false });\n\t// @ts-expect-error shush\n\tlocale.set(newLocale);\n}\n\nexport function link(\n\ttag: string | string[],\n\ttext: string,\n\tlinks: string | string[],\n\tnewTab?: boolean | boolean[],\n\tclassName?: string | string[],\n) {\n\tif (!text) return \"\";\n\n\tconst tags = Array.isArray(tag) ? tag : [tag];\n\tconst linksArr = Array.isArray(links) ? links : [links];\n\tconst newTabArr = Array.isArray(newTab) ? newTab : [newTab];\n\tconst classArr = Array.isArray(className) ? className : [className];\n\n\tlet result = text;\n\n\ttags.forEach((t, i) => {\n\t\tconst link = linksArr[i] ?? \"#\";\n\t\tconst target = newTabArr[i]\n\t\t\t? 'target=\"_blank\" rel=\"noopener noreferrer\"'\n\t\t\t: \"\";\n\t\tconst cls = classArr[i] ? `class=\"${classArr[i]}\"` : \"\";\n\n\t\tconst regex = new RegExp(`\\\\[${t}\\\\](.*?)\\\\[\\\\/${t}\\\\]`, \"g\");\n\t\tresult = result.replace(\n\t\t\tregex,\n\t\t\t(_, inner) => `<a href=\"${link}\" ${target} ${cls} >${inner}</a>`,\n\t\t);\n\t});\n\n\treturn result;\n}\n\nexport function sanitize(\n\thtml: string,\n\tallowedTags: string[] = [\"a\", \"b\", \"code\", \"br\"],\n): string {\n\treturn sanitizeHtml(html, {\n\t\tallowedTags: allowedTags,\n\t\tallowedAttributes: {\n\t\t\ta: [\"href\", \"target\", \"rel\", \"class\"],\n\t\t\t\"*\": [\"class\"],\n\t\t},\n\t\tallowedSchemes: [\"http\", \"https\", \"mailto\", \"blob\"],\n\t});\n}\n\n/**\n * Binary search for a max value without knowing the exact value, only that it\n * can be under or over It dose not test every number but instead looks for\n * 1,2,4,8,16,32,64,128,96,95 to figure out that you thought about #96 from\n * 0-infinity\n *\n * @example findFirstPositive(x => matchMedia(`(max-resolution: ${x}dpi)`).matches)\n * @author Jimmy Wärting\n * @see {@link https://stackoverflow.com/a/72124984/1008999}\n * @param {function} f The function to run the test on (should return truthy or falsy values)\n * @param {bigint} [b=1] Where to start looking from\n * @param {function} d privately used to calculate the next value to test\n * @returns {bigint} Integer\n */\nfunction findFirstPositive(\n\tf: (x: bigint) => number,\n\tb = 1n,\n\td = (e: bigint, g: bigint, c?: bigint): bigint =>\n\t\tg < e\n\t\t\t? -1n\n\t\t\t: 0 < f((c = (e + g) >> 1n))\n\t\t\t\t? c == e || 0 >= f(c - 1n)\n\t\t\t\t\t? c\n\t\t\t\t\t: d(e, c - 1n)\n\t\t\t\t: d(c + 1n, g),\n): bigint {\n\tfor (; 0 >= f(b); b <<= 1n);\n\treturn d(b >> 1n, b) - 1n;\n}\n\nexport const getMaxArrayBufferSize = (): number => {\n\tif (typeof window === \"undefined\") return 2 * GB; // default for SSR\n\n\t// check cache first\n\tconst cached = localStorage.getItem(\"maxArrayBufferSize\");\n\tif (cached) {\n\t\tconst parsed = Number(cached);\n\t\tlog([\"converters\"], `using cached max ArrayBuffer size: ${parsed} bytes`);\n\t\tif (!isNaN(parsed) && parsed > 0) return parsed;\n\t}\n\n\t// detect max size using binary search\n\tconst maxSize = findFirstPositive((x) => {\n\t\ttry {\n\t\t\tnew ArrayBuffer(Number(x));\n\t\t\treturn 0; // false = can allocate\n\t\t} catch {\n\t\t\treturn 1; // true = cannot allocate\n\t\t}\n\t});\n\n\tconst result = Number(maxSize);\n\tlocalStorage.setItem(\"maxArrayBufferSize\", result.toString());\n\tlog([\"converters\"], `detected max ArrayBuffer size: ${result} bytes`);\n\n\treturn result;\n};\n\nexport const MAX_ARRAY_BUFFER_SIZE = getMaxArrayBufferSize();\n"
  },
  {
    "path": "src/lib/types/conversion-worker.ts",
    "content": "import { VertFile } from \"./file.svelte\";\r\n\r\ninterface ConvertMessage {\r\n\ttype: \"convert\";\r\n\tinput: {\r\n\t\tfile: File;\r\n\t\tname: string;\r\n\t\tfrom: string;\r\n\t\tto: string;\r\n\t} | VertFile;\r\n\tto: string;\r\n\tcompression: number | null;\r\n\tkeepMetadata?: boolean;\r\n}\r\n\r\ninterface FinishedMessage {\r\n\ttype: \"finished\";\r\n\toutput: ArrayBufferLike | Uint8Array;\r\n\tzip?: boolean;\r\n}\r\n\r\ninterface LoadMessage {\r\n\ttype: \"load\";\r\n\twasm: ArrayBuffer;\r\n}\r\n\r\ninterface LoadedMessage {\r\n\ttype: \"loaded\";\r\n}\r\n\r\ninterface ReadyMessage {\r\n\ttype: \"ready\";\r\n}\r\n\r\ninterface ErrorMessage {\r\n\ttype: \"error\";\r\n\terror: string;\r\n}\r\n\r\nexport type WorkerMessage = (\r\n\t| ConvertMessage\r\n\t| FinishedMessage\r\n\t| LoadMessage\r\n\t| LoadedMessage\r\n\t| ReadyMessage\r\n\t| ErrorMessage\r\n) & {\r\n\tid: string; // unused? rn just using file id, probably meant to be incrementing w/ every message posted?\r\n};\r\n"
  },
  {
    "path": "src/lib/types/file.svelte.ts",
    "content": "import { byNative, converters } from \"$lib/converters\";\nimport type { Converter } from \"$lib/converters/converter.svelte\";\nimport { m } from \"$lib/paraglide/messages\";\nimport { ToastManager } from \"$lib/util/toast.svelte\";\nimport type { Component } from \"svelte\";\nimport { MAX_ARRAY_BUFFER_SIZE } from \"$lib/store/index.svelte\";\n\nexport class VertFile {\n\tpublic id: string = Math.random().toString(36).slice(2, 8);\n\tpublic readonly file: File;\n\n\tpublic get from() {\n\t\treturn (\".\" + this.file.name.split(\".\").pop() || \"\").toLowerCase();\n\t}\n\n\tpublic get name() {\n\t\treturn this.file.name;\n\t}\n\n\tpublic progress = $state(0);\n\tpublic result = $state<VertFile | null>(null);\n\n\tpublic to = $state(\"\");\n\n\tpublic blobUrl = $state<string>();\n\n\tpublic processing = $state(false);\n\n\tpublic cancelled = $state(false);\n\n\tpublic converters: Converter[] = [];\n\n\tpublic isZip = $state(() => this.from === \".zip\");\n\n\tpublic findConverters(supportedFormats: string[] = [this.from]) {\n\t\tconst converter = this.converters\n\t\t\t.filter((converter) =>\n\t\t\t\tconverter\n\t\t\t\t\t.formatStrings()\n\t\t\t\t\t.map((f) => supportedFormats.includes(f)),\n\t\t\t)\n\t\t\t.sort(byNative(this.from));\n\t\treturn converter;\n\t}\n\n\tpublic findConverter() {\n\t\t// zip will always only be added if there's one converter that supports all files - handled in store's _handleZipFile()\n\t\tif (this.isZip()) return this.converters[0];\n\n\t\tconst converter = this.converters.find((converter) => {\n\t\t\tif (\n\t\t\t\t!converter.formatStrings().includes(this.from) ||\n\t\t\t\t!converter.formatStrings().includes(this.to)\n\t\t\t) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst theirFrom = converter.supportedFormats.find(\n\t\t\t\t(f) => f.name === this.from,\n\t\t\t);\n\t\t\tconst theirTo = converter.supportedFormats.find(\n\t\t\t\t(f) => f.name === this.to,\n\t\t\t);\n\t\t\tif (!theirFrom || !theirTo) return false;\n\t\t\tif (!theirFrom.isNative && !theirTo.isNative) return false;\n\t\t\treturn true;\n\t\t});\n\t\treturn converter;\n\t}\n\n\tpublic isLarge(): boolean {\n\t\treturn this.file.size > MAX_ARRAY_BUFFER_SIZE;\n\t}\n\n\tpublic supportsStreaming(): boolean {\n\t\t// only vertd (video/gif -> video/gif) supports streaming\n\t\t// rest of converters need entire file in memory, limited by ArrayBuffer limits\n\t\tconst converter = this.findConverter();\n\t\treturn converter?.name === \"vertd\";\n\t}\n\n\tconstructor(file: File, to: string, blobUrl?: string) {\n\t\tconst ext = file.name.split(\".\").pop();\n\t\tconst newFile = new File(\n\t\t\t[file.slice(0, file.size, file.type)],\n\t\t\t`${file.name.split(\".\").slice(0, -1).join(\".\")}.${ext?.toLowerCase()}`,\n\t\t);\n\t\tthis.file = newFile;\n\t\tthis.to = to.startsWith(\".\") ? to : `.${to}`;\n\t\tthis.converters = converters.filter((c) =>\n\t\t\tc.formatStrings().includes(this.from),\n\t\t);\n\t\tthis.convert = this.convert.bind(this);\n\t\tthis.download = this.download.bind(this);\n\t\tthis.blobUrl = blobUrl;\n\t}\n\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tpublic async convert(...args: any[]) {\n\t\tif (!this.converters.length) throw new Error(\"No converters found\");\n\t\tconst converter = this.findConverter();\n\t\tif (!converter) throw new Error(\"No converter found\");\n\t\tthis.result = null;\n\t\tthis.progress = 0;\n\t\tthis.processing = true;\n\t\tthis.cancelled = false;\n\t\tlet res;\n\t\ttry {\n\t\t\t// for zips: extract > convert each > re-zip\n\t\t\t// else convert normally\n\t\t\tres = this.isZip()\n\t\t\t\t? await this.convertZip(converter)\n\t\t\t\t: await converter.convert(this, this.to, ...args);\n\t\t\tthis.result = res;\n\t\t} catch (err) {\n\t\t\tif (!this.cancelled) this.toastErr(err);\n\t\t\tthis.result = null;\n\t\t}\n\t\tthis.processing = false;\n\t\treturn res;\n\t}\n\n\tprivate async convertZip(converter: Converter): Promise<VertFile> {\n\t\tconst { extractZip, createZip } = await import(\"$lib/util/zip\");\n\t\tconst { default: PQueue } = await import(\"p-queue\");\n\n\t\tconst entries = await extractZip(this.file);\n\t\tconst totalFiles = entries.length;\n\t\tconst fileProgress: number[] = new Array(totalFiles).fill(0);\n\t\tconst convertedFiles: File[] = [];\n\n\t\tconst queue = new PQueue({\n\t\t\tconcurrency: navigator.hardwareConcurrency || 4,\n\t\t});\n\n\t\tconst updateProgress = () => {\n\t\t\tconst totalProgress = fileProgress.reduce((sum, p) => sum + p, 0);\n\t\t\tthis.progress = Math.round(totalProgress / totalFiles);\n\t\t};\n\n\t\t// convert all files in the zip\n\t\tawait queue.addAll(\n\t\t\tentries.map(({ filename, data }, index) => async () => {\n\t\t\t\tif (this.cancelled) {\n\t\t\t\t\tthrow new Error(\"Conversion cancelled\");\n\t\t\t\t}\n\n\t\t\t\tconst file = new File([new Uint8Array(data)], filename, {\n\t\t\t\t\ttype: \"application/octet-stream\",\n\t\t\t\t});\n\t\t\t\tconst tempVFile = new VertFile(file, this.to);\n\t\t\t\ttempVFile.converters = [converter];\n\n\t\t\t\tif (converter.reportsProgress) {\n\t\t\t\t\t// track progress of individual files\n\t\t\t\t\tconst progressInterval = setInterval(() => {\n\t\t\t\t\t\tfileProgress[index] = tempVFile.progress;\n\t\t\t\t\t\tupdateProgress();\n\t\t\t\t\t}, 100);\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst converted = await converter.convert(\n\t\t\t\t\t\t\ttempVFile,\n\t\t\t\t\t\t\tthis.to,\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tlet outputExt = this.to;\n\t\t\t\t\t\tif (!outputExt.startsWith(\".\"))\n\t\t\t\t\t\t\toutputExt = `.${outputExt}`;\n\n\t\t\t\t\t\tconvertedFiles[index] = new File(\n\t\t\t\t\t\t\t[await converted.file.arrayBuffer()],\n\t\t\t\t\t\t\tconverted.name,\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tfileProgress[index] = 100;\n\t\t\t\t\t\tupdateProgress();\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tclearInterval(progressInterval);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// else track progress via completions only\n\t\t\t\t\tconst converted = await converter.convert(\n\t\t\t\t\t\ttempVFile,\n\t\t\t\t\t\tthis.to,\n\t\t\t\t\t);\n\n\t\t\t\t\tlet outputExt = this.to;\n\t\t\t\t\tif (!outputExt.startsWith(\".\")) outputExt = `.${outputExt}`;\n\n\t\t\t\t\tconvertedFiles[index] = new File(\n\t\t\t\t\t\t[await converted.file.arrayBuffer()],\n\t\t\t\t\t\tconverted.name,\n\t\t\t\t\t);\n\n\t\t\t\t\tfileProgress[index] = 100;\n\t\t\t\t\tupdateProgress();\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\n\t\t// return zip of converted files\n\t\tconst resultArray = await createZip(convertedFiles);\n\t\tconst outputFilename = this.file.name.replace(/\\.[^/.]+$/, \".zip\");\n\t\tconst resultFile = new File(\n\t\t\t[new Uint8Array(resultArray)],\n\t\t\toutputFilename,\n\t\t);\n\t\treturn new VertFile(resultFile, \".zip\");\n\t}\n\n\tpublic async cancel() {\n\t\tif (!this.processing) return;\n\t\tconst converter = this.findConverter();\n\t\tif (!converter) throw new Error(\"No converter found\");\n\t\tthis.cancelled = true;\n\t\ttry {\n\t\t\tawait converter.cancel(this);\n\t\t\tthis.processing = false;\n\t\t\tthis.result = null;\n\t\t} catch (err) {\n\t\t\tthis.toastErr(err);\n\t\t}\n\t}\n\n\tprivate toastErr(err: unknown) {\n\t\ttype ToastMsg = {\n\t\t\tcomponent: Component;\n\t\t\tadditional: unknown;\n\t\t};\n\n\t\tconst castedErr = err as Error | string | ToastMsg;\n\t\tlet toastMsg: string | ToastMsg = \"\";\n\t\tif (typeof castedErr === \"string\") {\n\t\t\ttoastMsg = castedErr;\n\t\t} else if (castedErr instanceof Error) {\n\t\t\ttoastMsg = castedErr.message;\n\t\t} else {\n\t\t\ttoastMsg = castedErr;\n\t\t}\n\n\t\t// ToastManager.add({\n\t\t// \ttype: \"error\",\n\t\t// \tmessage:\n\t\t// \t\ttypeof toastMsg === \"string\"\n\t\t// \t\t\t? m[\"workers.errors.general\"]({\n\t\t// \t\t\t\t\tfile: this.file.name,\n\t\t// \t\t\t\t\tmessage: toastMsg,\n\t\t// \t\t\t\t})\n\t\t// \t\t\t: toastMsg,\n\t\t// });\n\n\t\tif (typeof toastMsg === \"string\") {\n\t\t\tToastManager.add({\n\t\t\t\ttype: \"error\",\n\t\t\t\tmessage: m[\"workers.errors.general\"]({\n\t\t\t\t\tfile: this.file.name,\n\t\t\t\t\tmessage: toastMsg,\n\t\t\t\t}),\n\t\t\t});\n\t\t} else {\n\t\t\tToastManager.add({\n\t\t\t\ttype: \"error\",\n\t\t\t\tmessage: toastMsg.component,\n\t\t\t\tadditional: toastMsg.additional,\n\t\t\t});\n\t\t}\n\t}\n\n\tpublic async download() {\n\t\tif (!this.result) throw new Error(\"No result found\");\n\n\t\t// give the freedom to the converter to set the extension (ie. pandoc uses this to output zips)\n\t\tlet to = this.result.to;\n\t\tif (!to.startsWith(\".\")) to = `.${to}`;\n\n\t\tconst settings = JSON.parse(localStorage.getItem(\"settings\") ?? \"{}\");\n\t\tconst filenameFormat = settings.filenameFormat || \"VERT_%name%\";\n\n\t\tconst format = (name: string) => {\n\t\t\tconst date = new Date().toISOString();\n\t\t\tconst baseName = this.file.name.replace(/\\.[^/.]+$/, \"\");\n\t\t\tconst originalExtension = this.file.name.split(\".\").pop()!;\n\t\t\treturn name\n\t\t\t\t.replace(/%date%/g, date)\n\t\t\t\t.replace(/%name%/g, baseName)\n\t\t\t\t.replace(/%extension%/g, originalExtension);\n\t\t};\n\n\t\tconst blob = URL.createObjectURL(\n\t\t\tnew Blob([await this.result.file.arrayBuffer()], {\n\t\t\t\t// type: to.slice(1),\n\t\t\t\ttype: \"application/octet-stream\", // use generic type to prevent browsers changing extension\n\t\t\t}),\n\t\t);\n\t\tconst a = document.createElement(\"a\");\n\t\ta.href = blob;\n\t\ta.download = `${format(filenameFormat)}${to}`;\n\t\t// force it to not open in a new tab\n\t\ta.target = \"_blank\";\n\t\ta.style.display = \"none\";\n\t\ta.click();\n\t\tURL.revokeObjectURL(blob);\n\t\ta.remove();\n\t}\n\n\tpublic hash(): Promise<string> {\n\t\tconst stream = this.file.stream();\n\t\tconst hashes = new Set<string>();\n\t\tconst reader = stream.getReader();\n\t\treturn new Promise<string>((resolve, reject) => {\n\t\t\tfunction processChunk() {\n\t\t\t\treader.read().then(({ done, value }) => {\n\t\t\t\t\tif (done) {\n\t\t\t\t\t\tconst combinedHash = Array.from(hashes).sort().join(\"\");\n\t\t\t\t\t\tresolve(combinedHash);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tcrypto.subtle\n\t\t\t\t\t\t.digest(\"SHA-256\", value)\n\t\t\t\t\t\t.then((hashBuffer) => {\n\t\t\t\t\t\t\tconst hashArray = Array.from(\n\t\t\t\t\t\t\t\tnew Uint8Array(hashBuffer),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tconst hashHex = hashArray\n\t\t\t\t\t\t\t\t.map((b) => b.toString(16).padStart(2, \"0\"))\n\t\t\t\t\t\t\t\t.join(\"\");\n\t\t\t\t\t\t\thashes.add(hashHex);\n\t\t\t\t\t\t\tprocessChunk();\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.catch((err) => {\n\t\t\t\t\t\t\treject(err);\n\t\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t}\n\t\t\tprocessChunk();\n\t\t});\n\t}\n}\n\nexport interface Categories {\n\t[key: string]: {\n\t\tformats: string[];\n\t\tcanConvertTo?: string[];\n\t};\n}\n"
  },
  {
    "path": "src/lib/types/index.ts",
    "content": "export * from \"./file.svelte\";\nexport * from \"./util\";\nexport * from \"./conversion-worker\";\n"
  },
  {
    "path": "src/lib/types/util.ts",
    "content": "export type OmitBetterStrict<T, K extends keyof T> = T extends unknown\n\t? Pick<T, Exclude<keyof T, K>>\n\t: never;\n"
  },
  {
    "path": "src/lib/util/animation.ts",
    "content": "import { isMobile, effects } from \"$lib/store/index.svelte\";\nimport type { AnimationConfig, FlipParams } from \"svelte/animate\";\nimport { cubicOut } from \"svelte/easing\";\nimport {\n\tfade as svelteFade,\n\tfly as svelteFly,\n\ttype FadeParams,\n\ttype FlyParams,\n} from \"svelte/transition\";\n\n// Subscribe to stores\nlet effectsEnabled = true;\nlet isMobileDevice = false;\n\nexport function initStores() {\n\teffects.subscribe((value) => {\n\t\teffectsEnabled = value;\n\t});\n\tisMobile.subscribe((value) => {\n\t\tisMobileDevice = value;\n\t});\n}\n\nexport const transition =\n\t\"linear(0,0.006,0.025 2.8%,0.101 6.1%,0.539 18.9%,0.721 25.3%,0.849 31.5%,0.937 38.1%,0.968 41.8%,0.991 45.7%,1.006 50.1%,1.015 55%,1.017 63.9%,1.001)\";\n\nexport const duration = 500;\n\nexport function fade(node: HTMLElement, options: FadeParams) {\n\tif (!effectsEnabled) return {};\n\tconst animation = svelteFade(node, options);\n\treturn animation;\n}\n\nexport function fly(node: HTMLElement, options: FlyParams) {\n\tif (!effectsEnabled || isMobileDevice) return {};\n\tconst animation = svelteFly(node, options);\n\treturn animation;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\nexport function is_function(thing: unknown): thing is Function {\n\treturn typeof thing === \"function\";\n}\n\ntype Params = FlipParams & {};\n\n/**\n * The flip function calculates the start and end position of an element and animates between them, translating the x and y values.\n * `flip` stands for [First, Last, Invert, Play](https://aerotwist.com/blog/flip-your-animations/).\n *\n * https://svelte.dev/docs/svelte-animate#flip\n */\nexport function flip(\n\tnode: HTMLElement,\n\t{ from, to }: { from: DOMRect; to: DOMRect },\n\tparams: Params = {},\n): AnimationConfig {\n\tconst style = getComputedStyle(node);\n\tconst transform = style.transform === \"none\" ? \"\" : style.transform;\n\tconst [ox, oy] = style.transformOrigin.split(\" \").map(parseFloat);\n\tconst dx = from.left + (from.width * ox) / to.width - (to.left + ox);\n\tconst dy = from.top + (from.height * oy) / to.height - (to.top + oy);\n\tconst {\n\t\tdelay = 0,\n\t\tduration = (d) => Math.sqrt(d) * 120,\n\t\teasing = cubicOut,\n\t} = params;\n\treturn {\n\t\tdelay,\n\t\tduration: is_function(duration)\n\t\t\t? duration(Math.sqrt(dx * dx + dy * dy))\n\t\t\t: duration,\n\t\teasing,\n\t\tcss: (_t, u) => {\n\t\t\tconst x = u * dx;\n\t\t\tconst y = u * dy;\n\t\t\t// const sx = scale ? t + (u * from.width) / to.width : 1;\n\t\t\t// const sy = scale ? t + (u * from.height) / to.height : 1;\n\t\t\treturn `transform: ${transform} translate(${x}px, ${y}px);`;\n\t\t},\n\t};\n}\n"
  },
  {
    "path": "src/lib/util/consts.ts",
    "content": "import { PUB_DISABLE_ALL_EXTERNAL_REQUESTS, PUB_ENV } from \"$env/static/public\";\n\nexport const GITHUB_URL_VERT = \"https://github.com/VERT-sh/VERT\";\nexport const GITHUB_URL_VERTD = \"https://github.com/VERT-sh/vertd\";\nexport const GITHUB_API_URL = \"https://api.github.com/repos/VERT-sh/VERT\";\nexport const DISCORD_URL = \"https://discord.gg/kqevGxYPak\";\nexport const VERT_NAME =\n\tPUB_ENV === \"development\"\n\t\t? \"VERT Local\"\n\t\t: PUB_ENV === \"nightly\"\n\t\t\t? \"VERT Nightly\"\n\t\t\t: \"VERT.sh\";\nexport const CONTACT_EMAIL = \"hello@vert.sh\";\n\n// i'm not entirely sure this should be in consts.ts, but it is technically a constant as .env is static for VERT\nexport const DISABLE_ALL_EXTERNAL_REQUESTS =\n\tPUB_DISABLE_ALL_EXTERNAL_REQUESTS === \"true\";\n\nexport const GB = 1024 * 1024 * 1024;\n"
  },
  {
    "path": "src/lib/util/ip.ts",
    "content": "import { browser } from \"$app/environment\";\n\nexport interface IpInfo {\n\tip: string;\n\tnetwork: string;\n\tversion: string;\n\tcity: string;\n\tregion: string;\n\tregion_code: string;\n\tcountry: string;\n\tcountry_name: string;\n\tcountry_code: string;\n\tcountry_code_iso3: string;\n\tcountry_capital: string;\n\tcountry_tld: string;\n\tcontinent_code: string;\n\tin_eu: boolean;\n\tpostal: string;\n\tlatitude: number;\n\tlongitude: number;\n\ttimezone: string;\n\tutc_offset: string;\n\tcountry_calling_code: string;\n\tcurrency: string;\n\tcurrency_name: string;\n\tlanguages: string;\n\tcountry_area: number;\n\tcountry_population: number;\n\tasn: string;\n\torg: string;\n}\n\nexport const ip = async (): Promise<IpInfo> => {\n\ttry {\n\t\tif (browser) {\n\t\t\tconst item = localStorage.getItem(\"ipinfo\");\n\t\t\tif (item) {\n\t\t\t\treturn JSON.parse(item);\n\t\t\t}\n\t\t}\n\n\t\tconst res = await fetch(\"https://ipapi.co/json/\").then((r) => r.json());\n\t\tif (browser) {\n\t\t\tlocalStorage.setItem(\"ipinfo\", JSON.stringify(res));\n\t\t}\n\n\t\treturn res;\n\t\t// eslint-disable-next-line @typescript-eslint/no-unused-vars\n\t} catch (_) {\n\t\treturn {\n\t\t\tip: \"127.0.0.1\",\n\t\t\tasn: \"AS0\",\n\t\t\tcity: \"Localhost\",\n\t\t\tcontinent_code: \"NA\",\n\t\t\tcountry: \"US\",\n\t\t\tcountry_calling_code: \"+1\",\n\t\t\tcountry_capital: \"Washington\",\n\t\t\tcountry_code: \"US\",\n\t\t\tcountry_code_iso3: \"USA\",\n\t\t\tcountry_name: \"United States\",\n\t\t\tcountry_population: 0,\n\t\t\tcurrency: \"USD\",\n\t\t\tcurrency_name: \"Dollar\",\n\t\t\tlanguages: \"en-US,es-US,haw,fr\",\n\t\t\tlatitude: 0,\n\t\t\tlongitude: 0,\n\t\t\tnetwork: \"Unknown\",\n\t\t\tpostal: \"00000\",\n\t\t\tregion: \"Local\",\n\t\t\tregion_code: \"LOC\",\n\t\t\tcountry_area: 0,\n\t\t\ttimezone: \"America/New_York\",\n\t\t\tutc_offset: \"-0500\",\n\t\t\tversion: \"IPv4\",\n\t\t\tin_eu: false,\n\t\t\torg: \"Localhost\",\n\t\t\tcountry_tld: \".us\",\n\t\t};\n\t}\n};\n"
  },
  {
    "path": "src/lib/util/logger.ts",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { browser } from \"$app/environment\";\n\nconst randomColorFromStr = (str: string) => {\n\t// generate a pleasant color from a string, using HSL\n\tlet hash = 0;\n\tfor (let i = 0; i < str.length; i++) {\n\t\thash = str.charCodeAt(i) + ((hash << 5) - hash);\n\t}\n\tconst h = hash % 360;\n\treturn `hsl(${h}, 75%, 71%)`;\n};\n\nconst whiteOrBlack = (hsl: string) => {\n\t// determine if the text should be white or black based on the background color\n\tconst [, , l] = hsl\n\t\t.replace(\"hsl(\", \"\")\n\t\t.replace(\")\", \"\")\n\t\t.split(\",\")\n\t\t.map((v) => parseInt(v));\n\treturn l > 70 ? \"black\" : \"white\";\n};\n\nexport const log = (prefix: string | string[], ...args: any[]) => {\n\tconst prefixes = Array.isArray(prefix) ? prefix : [prefix];\n\tif (!browser)\n\t\treturn console.log(prefixes.map((p) => `[${p}]`).join(\" \"), ...args);\n\tconst prefixesWithMeta = prefixes.map((p) => ({\n\t\tprefix: p,\n\t\tbgColor: randomColorFromStr(p),\n\t\ttextColor: whiteOrBlack(randomColorFromStr(p)),\n\t}));\n\n\tconsole.log(\n\t\t`%c${prefixesWithMeta.map(({ prefix }) => prefix).join(\" %c\")}`,\n\t\t...prefixesWithMeta.map(\n\t\t\t({ bgColor, textColor }, i) =>\n\t\t\t\t`color: ${textColor}; background-color: ${bgColor}; margin-left: ${i === 0 ? 0 : -6}px; padding: 0px 4px 0 4px; border-radius: 0px 9999px 9999px 0px;`,\n\t\t),\n\t\t...args,\n\t);\n};\n\nexport const error = (prefix: string | string[], ...args: any[]) => {\n\tconst prefixes = Array.isArray(prefix) ? prefix : [prefix];\n\tif (!browser)\n\t\treturn console.error(prefixes.map((p) => `[${p}]`).join(\" \"), ...args);\n\tconst prefixesWithMeta = prefixes.map((p) => ({\n\t\tprefix: p,\n\t\tbgColor: randomColorFromStr(p),\n\t\ttextColor: whiteOrBlack(randomColorFromStr(p)),\n\t}));\n\n\tconsole.error(\n\t\t`%c${prefixesWithMeta.map(({ prefix }) => prefix).join(\" %c\")}`,\n\t\t...prefixesWithMeta.map(\n\t\t\t({ bgColor, textColor }, i) =>\n\t\t\t\t`color: ${textColor}; background-color: ${bgColor}; margin-left: ${i === 0 ? 0 : -6}px; padding: 0px 4px 0 4px; border-radius: 0px 9999px 9999px 0px;`,\n\t\t),\n\t\t...args,\n\t);\n};\n"
  },
  {
    "path": "src/lib/util/parse/ani.ts",
    "content": "// THIS CODE IS FROM https://github.com/captbaritone/webamp/blob/15b0312cb794973a0e615d894df942452e920c36/packages/ani-cursor/src/parser.ts\r\n// LICENSED UNDER MIT. (c) Jordan Eldredge and Webamp contributors\r\n\r\n// this code is ripped from their project because i didn't want to\r\n// re-invent the wheel, BUT the library they provide (ani-cursor)\r\n// doesn't expose the internals.\r\n\r\nimport { RIFFFile } from \"riff-file\";\r\nimport { unpackArray, unpackString } from \"byte-data\";\r\n\r\ntype Chunk = {\r\n\tformat: string;\r\n\tchunkId: string;\r\n\tchunkData: {\r\n\t\tstart: number;\r\n\t\tend: number;\r\n\t};\r\n\tsubChunks: Chunk[];\r\n};\r\n\r\n// https://www.informit.com/articles/article.aspx?p=1189080&seqNum=3\r\ntype AniMetadata = {\r\n\tcbSize: number; // Data structure size (in bytes)\r\n\tnFrames: number; // Number of images (also known as frames) stored in the file\r\n\tnSteps: number; // Number of frames to be displayed before the animation repeats\r\n\tiWidth: number; // Width of frame (in pixels)\r\n\tiHeight: number; // Height of frame (in pixels)\r\n\tiBitCount: number; // Number of bits per pixel\r\n\tnPlanes: number; // Number of color planes\r\n\tiDispRate: number; // Default frame display rate (measured in 1/60th-of-a-second units)\r\n\tbfAttributes: number; // ANI attribute bit flags\r\n};\r\n\r\ntype ParsedAni = {\r\n\trate: number[] | null;\r\n\tseq: number[] | null;\r\n\timages: Uint8Array[];\r\n\tmetadata: AniMetadata;\r\n\tartist: string | null;\r\n\ttitle: string | null;\r\n};\r\n\r\nconst DWORD = { bits: 32, be: false, signed: false, fp: false };\r\n\r\nexport function parseAni(arr: Uint8Array): ParsedAni {\r\n\tconst riff = new RIFFFile();\r\n\r\n\triff.setSignature(arr);\r\n\r\n\tconst signature = riff.signature as Chunk;\r\n\tif (signature.format !== \"ACON\") {\r\n\t\tthrow new Error(\r\n\t\t\t`Expected format. Expected \"ACON\", got \"${signature.format}\"`,\r\n\t\t);\r\n\t}\r\n\r\n\t// Helper function to get a chunk by chunkId and transform it if it's non-null.\r\n\tfunction mapChunk<T>(\r\n\t\tchunkId: string,\r\n\t\tmapper: (chunk: Chunk) => T,\r\n\t): T | null {\r\n\t\tconst chunk = riff.findChunk(chunkId) as Chunk | null;\r\n\t\treturn chunk == null ? null : mapper(chunk);\r\n\t}\r\n\r\n\tfunction readImages(chunk: Chunk, frameCount: number): Uint8Array[] {\r\n\t\treturn chunk.subChunks.slice(0, frameCount).map((c) => {\r\n\t\t\tif (c.chunkId !== \"icon\") {\r\n\t\t\t\tthrow new Error(`Unexpected chunk type in fram: ${c.chunkId}`);\r\n\t\t\t}\r\n\t\t\treturn arr.slice(c.chunkData.start, c.chunkData.end);\r\n\t\t});\r\n\t}\r\n\r\n\tconst metadata = mapChunk(\"anih\", (c) => {\r\n\t\tconst words = unpackArray(\r\n\t\t\tarr,\r\n\t\t\tDWORD,\r\n\t\t\tc.chunkData.start,\r\n\t\t\tc.chunkData.end,\r\n\t\t);\r\n\t\treturn {\r\n\t\t\tcbSize: words[0],\r\n\t\t\tnFrames: words[1],\r\n\t\t\tnSteps: words[2],\r\n\t\t\tiWidth: words[3],\r\n\t\t\tiHeight: words[4],\r\n\t\t\tiBitCount: words[5],\r\n\t\t\tnPlanes: words[6],\r\n\t\t\tiDispRate: words[7],\r\n\t\t\tbfAttributes: words[8],\r\n\t\t};\r\n\t});\r\n\r\n\tif (metadata == null) {\r\n\t\tthrow new Error(\"Did not find anih\");\r\n\t}\r\n\r\n\tconst rate = mapChunk(\"rate\", (c) => {\r\n\t\treturn unpackArray(arr, DWORD, c.chunkData.start, c.chunkData.end);\r\n\t});\r\n\t// chunkIds are always four chars, hence the trailing space.\r\n\tconst seq = mapChunk(\"seq \", (c) => {\r\n\t\treturn unpackArray(arr, DWORD, c.chunkData.start, c.chunkData.end);\r\n\t});\r\n\r\n\tconst lists = riff.findChunk(\"LIST\", true) as Chunk[] | null;\r\n\tconst imageChunk = lists?.find((c) => c.format === \"fram\");\r\n\tif (imageChunk == null) {\r\n\t\tthrow new Error(\"Did not find fram LIST\");\r\n\t}\r\n\r\n\tlet images = readImages(imageChunk, metadata.nFrames);\r\n\r\n\tlet title = null;\r\n\tlet artist = null;\r\n\r\n\tconst infoChunk = lists?.find((c) => c.format === \"INFO\");\r\n\tif (infoChunk != null) {\r\n\t\tinfoChunk.subChunks.forEach((c) => {\r\n\t\t\tswitch (c.chunkId) {\r\n\t\t\t\tcase \"INAM\":\r\n\t\t\t\t\ttitle = unpackString(\r\n\t\t\t\t\t\tarr,\r\n\t\t\t\t\t\tc.chunkData.start,\r\n\t\t\t\t\t\tc.chunkData.end,\r\n\t\t\t\t\t);\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tcase \"IART\":\r\n\t\t\t\t\tartist = unpackString(\r\n\t\t\t\t\t\tarr,\r\n\t\t\t\t\t\tc.chunkData.start,\r\n\t\t\t\t\t\tc.chunkData.end,\r\n\t\t\t\t\t);\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tcase \"LIST\":\r\n\t\t\t\t\t// Some cursors with an artist of \"Created with Take ONE 3.5 (unregisterred version)\" seem to have their frames here for some reason?\r\n\t\t\t\t\tif (c.format === \"fram\") {\r\n\t\t\t\t\t\timages = readImages(c, metadata.nFrames);\r\n\t\t\t\t\t}\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tdefault:\r\n\t\t\t\t// Unexpected subchunk\r\n\t\t\t}\r\n\t\t});\r\n\t}\r\n\r\n\treturn { images, rate, seq, metadata, artist, title };\r\n}\r\n"
  },
  {
    "path": "src/lib/util/parse/icns/index.ts",
    "content": ""
  },
  {
    "path": "src/lib/util/sw.ts",
    "content": "import { browser } from \"$app/environment\";\n\nexport interface CacheInfo {\n\ttotalSize: number;\n\tfileCount: number;\n\tfiles: Array<{\n\t\turl: string;\n\t\tsize: number;\n\t\ttype: string;\n\t}>;\n}\n\nclass ServiceWorkerManager {\n\tprivate registration: ServiceWorkerRegistration | null = null;\n\tprivate initialized = false;\n\n\tasync init(): Promise<void> {\n\t\tif (!browser || !(\"serviceWorker\" in navigator) || this.initialized) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tthis.registration = await navigator.serviceWorker.register(\n\t\t\t\t\"/sw.js\",\n\t\t\t\t{\n\t\t\t\t\tscope: \"/\",\n\t\t\t\t},\n\t\t\t);\n\n\t\t\tthis.initialized = true;\n\t\t} catch (error) {\n\t\t\tconsole.error(\n\t\t\t\t\"[SW Manager] service worker registration failed:\",\n\t\t\t\terror,\n\t\t\t);\n\t\t}\n\t}\n\n\tasync getCacheInfo(): Promise<CacheInfo> {\n\t\tif (!this.registration || !navigator.serviceWorker.controller) {\n\t\t\tconsole.warn(\n\t\t\t\t\"[SW Manager] no service worker available for cache info\",\n\t\t\t);\n\t\t\treturn { totalSize: 0, fileCount: 0, files: [] };\n\t\t}\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst messageChannel = new MessageChannel();\n\n\t\t\tmessageChannel.port1.onmessage = (event) => {\n\t\t\t\tresolve(event.data);\n\t\t\t};\n\n\t\t\tsetTimeout(() => {\n\t\t\t\treject(new Error(\"Timeout waiting for cache info\"));\n\t\t\t}, 5000);\n\n\t\t\tnavigator.serviceWorker?.controller?.postMessage(\n\t\t\t\t{ type: \"GET_CACHE_INFO\" },\n\t\t\t\t[messageChannel.port2],\n\t\t\t);\n\t\t});\n\t}\n\n\tasync clearCache(): Promise<void> {\n\t\tif (!this.registration || !navigator.serviceWorker.controller) {\n\t\t\tthrow new Error(\"No service worker available for cache clearing\");\n\t\t}\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst messageChannel = new MessageChannel();\n\n\t\t\tmessageChannel.port1.onmessage = (event) => {\n\t\t\t\tif (event.data.success) {\n\t\t\t\t\tresolve();\n\t\t\t\t} else {\n\t\t\t\t\treject(\n\t\t\t\t\t\tnew Error(event.data.error || \"Failed to clear cache\"),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tsetTimeout(() => {\n\t\t\t\treject(new Error(\"Timeout waiting for cache clear\"));\n\t\t\t}, 10000);\n\n\t\t\tnavigator.serviceWorker?.controller?.postMessage(\n\t\t\t\t{ type: \"CLEAR_CACHE\" },\n\t\t\t\t[messageChannel.port2],\n\t\t\t);\n\t\t});\n\t}\n\n\tformatSize(bytes: number): string {\n\t\tif (bytes === 0) return \"0 B\";\n\t\tconst k = 1024;\n\t\tconst sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n\t\tconst i = Math.floor(Math.log(bytes) / Math.log(k));\n\t\treturn parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + \" \" + sizes[i];\n\t}\n}\n\nexport const swManager = new ServiceWorkerManager();\n\n// Auto-initialize when imported\nif (browser) {\n\tswManager.init();\n}\n"
  },
  {
    "path": "src/lib/util/toast.svelte.ts",
    "content": "import type { Component } from \"svelte\";\n\nexport type ToastType = \"success\" | \"error\" | \"info\" | \"warning\";\n\n// export interface Toast<\n// \tT = unknown,\n// \tU extends string | ToastComponent<T> = string | ToastComponent<T>,\n// > {\n// \tid: number;\n// \ttype: ToastType;\n// \tmessage: U;\n// \tdisappearing: boolean;\n// \tdurations: {\n// \t\tenter: number;\n// \t\tstay: number;\n// \t\texit: number;\n// \t};\n// \tadditional: U extends string ? undefined : T;\n// }\n\ntype BaseToast = {\n\tid: number;\n\ttype: ToastType;\n\tdisappearing: boolean;\n\tdurations: {\n\t\tenter: number;\n\t\tstay: number;\n\t\texit: number;\n\t};\n};\n\nexport type StringToast = BaseToast & {\n\tmessage: string;\n};\n\nexport type ComponentToast<T> = BaseToast & {\n\tmessage: ToastComponent<T>;\n\tadditional: T;\n};\n\nexport type Toast<T = unknown> = StringToast | ComponentToast<T>;\n\nexport type ToastProps<T = unknown> = Omit<ComponentToast<T>, \"disappearing\">;\n\nexport type ToastExports = {\n\ttitle?: string;\n};\n\nexport type ToastComponent<T> = Component<ToastProps<T>, ToastExports>;\n\n// export interface ToastOptions<T = unknown> {\n// \ttype?: ToastType;\n// \tmessage: string | ToastComponent<T>;\n// \tdisappearing?: boolean;\n// \tdurations?: {\n// \t\tenter?: number;\n// \t\tstay?: number;\n// \t\texit?: number;\n// \t};\n// \tadditional?: T;\n// }\n\ntype RecursivePartial<T> = {\n\t[P in keyof T]?: T[P] extends (infer U)[]\n\t\t? RecursivePartial<U>[]\n\t\t: T[P] extends object | undefined\n\t\t\t? RecursivePartial<T[P]>\n\t\t\t: T[P];\n};\n\ntype BaseToastOptions = Omit<RecursivePartial<BaseToast>, \"id\"> & {\n\tdisappearing?: boolean;\n};\n\nexport type StringToastOptions = BaseToastOptions & {\n\tmessage: string;\n};\n\nexport type ComponentToastOptions<T> = BaseToastOptions & {\n\tmessage: ToastComponent<T>;\n\tadditional: T;\n};\n\nexport type ToastOptions<T = unknown> =\n\t| StringToastOptions\n\t| ComponentToastOptions<T>;\n\n// const toasts = writable<Toast[]>([]);\n\n// let toastId = 0;\n\n// function addToast(\n// \ttype: ToastType,\n// \tmessage: string | Component,\n// \tdisappearing?: boolean,\n// \tdurations?: { enter: number; stay: number; exit: number },\n// ) {\n// \tconst id = toastId++;\n\n// \tdurations = durations ?? {\n// \t\tenter: 300,\n// \t\tstay: disappearing || disappearing === undefined ? 5000 : 86400000, // 24h cause why not\n// \t\texit: 500,\n// \t};\n\n// \tconst newToast: Toast = {\n// \t\tid,\n// \t\ttype,\n// \t\tmessage,\n// \t\tdisappearing: disappearing ?? true,\n// \t\tdurations,\n// \t};\n// \ttoasts.update((currentToasts) => [...currentToasts, newToast]);\n\n// \tsetTimeout(\n// \t\t() => {\n// \t\t\tremoveToast(id);\n// \t\t},\n// \t\tdurations.enter + durations.stay + durations.exit,\n// \t);\n\n// \treturn id;\n// }\n\n// function removeToast(id: number) {\n// \ttoasts.update((currentToasts) =>\n// \t\tcurrentToasts.filter((toast) => toast.id !== id),\n// \t);\n// }\n\n// export { toasts, addToast, removeToast };\n\n// const DURATION_DEFAULTS = {\n// \tenter: 300,\n// \tstay: 5000,\n// \texit: 500,\n// };\n\nconst durationDefault = (disappearing: boolean) => ({\n\tenter: 300,\n\tstay: disappearing ? 5000 : 86400000, // 24h cause why not\n\texit: 500,\n});\n\n// const toastState = {\n// \ttoasts: $state<Toast[]>([]),\n// };\n\nclass ToastState {\n\tprivate pId = $state(0);\n\tprivate pToasts = $state<Toast<unknown>[]>([]);\n\n\tpublic add<T>(toast: Toast<T>) {\n\t\tthis.pToasts.push(toast as Toast<unknown>);\n\t}\n\n\tpublic remove(id: number) {\n\t\tthis.pToasts = this.pToasts.filter((toast) => toast.id !== id);\n\t}\n\n\tpublic id(): number {\n\t\treturn this.pId++;\n\t}\n\n\tpublic get toasts() {\n\t\treturn this.pToasts;\n\t}\n}\n\nexport class ToastManager {\n\tstatic pToasts = new ToastState();\n\n\tpublic static add<T = unknown>(toastOptions: ToastOptions<T>): number {\n\t\tconst id = this.pToasts.id();\n\t\tconst {\n\t\t\ttype = \"info\",\n\t\t\tdisappearing = true,\n\t\t\tdurations: d = durationDefault(toastOptions.disappearing ?? true),\n\t\t} = toastOptions;\n\t\tconst durations = {\n\t\t\t...durationDefault(disappearing),\n\t\t\t...d,\n\t\t};\n\n\t\tif (typeof toastOptions.message === \"string\") {\n\t\t\tconst newToast: StringToast = {\n\t\t\t\tid,\n\t\t\t\ttype,\n\t\t\t\tmessage: toastOptions.message,\n\t\t\t\tdisappearing,\n\t\t\t\tdurations,\n\t\t\t};\n\n\t\t\tthis.pToasts.add(newToast);\n\t\t} else {\n\t\t\tconst newToast: ComponentToast<T> = {\n\t\t\t\tid,\n\t\t\t\ttype,\n\t\t\t\tmessage: toastOptions.message,\n\t\t\t\tdisappearing,\n\t\t\t\tdurations,\n\t\t\t\tadditional: (toastOptions as ComponentToastOptions<T>)\n\t\t\t\t\t.additional,\n\t\t\t};\n\n\t\t\tthis.pToasts.add(newToast);\n\t\t}\n\n\t\tsetTimeout(\n\t\t\t() => {\n\t\t\t\tthis.remove(id);\n\t\t\t},\n\t\t\tdurations.enter + durations.stay + durations.exit,\n\t\t);\n\t\treturn id;\n\t}\n\n\tpublic static remove(id: number) {\n\t\tthis.pToasts.remove(id);\n\t}\n\n\tpublic static get toasts() {\n\t\treturn this.pToasts.toasts;\n\t}\n}\n"
  },
  {
    "path": "src/lib/util/zip.ts",
    "content": "import { error, log } from \"$lib/util/logger\";\nimport { unzip } from \"fflate\";\nimport { downloadZip } from \"client-zip\";\n\nexport interface ZipEntry {\n\tfilename: string;\n\tdata: Uint8Array;\n}\n\nexport async function extractZip(file: File): Promise<ZipEntry[]> {\n\tlog([\"zip\"], `extracting zip: ${file.name}`);\n\n\tconst arrayBuffer = await file.arrayBuffer();\n\tconst uint8Array = new Uint8Array(arrayBuffer);\n\n\treturn new Promise((resolve, reject) => {\n\t\tunzip(uint8Array, (err, unzipped) => {\n\t\t\tif (err) {\n\t\t\t\terror([\"zip\"], `failed to extract zip: ${err.message}`);\n\t\t\t\treject(new Error(`Failed to extract zip: ${err.message}`));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst entries = Object.entries(unzipped)\n\t\t\t\t.filter(([filename]) => !ignoreEntry(filename))\n\t\t\t\t.map(([filename, data]) => ({\n\t\t\t\t\tfilename,\n\t\t\t\t\tdata: new Uint8Array(data),\n\t\t\t\t}));\n\n\t\t\tlog([\"zip\"], `extracted ${entries.length} entries from ${file.name}`);\n\t\t\tresolve(entries);\n\t\t});\n\t});\n}\n\nexport async function createZip(files: File[]): Promise<Uint8Array> {\n\tlog([\"zip\"], `creating zip with ${files.length} files`);\n\tconst zipBlob = await downloadZip(files).blob();\n\treturn new Uint8Array(await zipBlob.arrayBuffer());\n}\n\nexport function ignoreEntry(filename: string): boolean {\n\treturn (\n\t\tfilename.startsWith(\".\") ||\n\t\tfilename.includes(\"/__MACOSX/\") ||\n\t\tfilename.endsWith(\"/\")\n\t);\n}\n"
  },
  {
    "path": "src/lib/workers/magick.ts",
    "content": "import {\n\tinitializeImageMagick,\n\tMagickFormat,\n\tMagickImage,\n\tMagickImageCollection,\n\tMagickReadSettings,\n\ttype IMagickImage,\n} from \"@imagemagick/magick-wasm\";\nimport { makeZip } from \"client-zip\";\nimport { parseAni } from \"$lib/util/parse/ani\";\nimport { parseIcns } from \"vert-wasm\";\nimport type { WorkerMessage } from \"$lib/types\";\n\nlet magickInitialized = false;\n\nself.postMessage({ type: \"ready\", id: \"0\" });\n\nconst handleMessage = async (\n\tmessage: WorkerMessage,\n): Promise<Partial<WorkerMessage>> => {\n\tswitch (message.type) {\n\t\tcase \"load\": {\n\t\t\ttry {\n\t\t\t\tif (!message.wasm || !(message.wasm instanceof ArrayBuffer)) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Invalid WASM data: ${typeof message.wasm}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tconst wasmBytes = new Uint8Array(message.wasm);\n\n\t\t\t\tawait initializeImageMagick(wasmBytes);\n\t\t\t\tmagickInitialized = true;\n\t\t\t\treturn { type: \"loaded\" };\n\t\t\t} catch (error) {\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t\terror: `error loading magick-wasm: ${(error as Error).message}`,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\tcase \"convert\": {\n\t\t\tif (!magickInitialized) {\n\t\t\t\treturn { type: \"error\", error: \"magick-wasm not initialized\" };\n\t\t\t}\n\n\t\t\tconst compression: number | undefined =\n\t\t\t\tmessage.compression ?? undefined;\n\t\t\tconst keepMetadata: boolean = message.keepMetadata ?? true;\n\t\t\tif (!message.to.startsWith(\".\")) message.to = `.${message.to}`;\n\t\t\tmessage.to = message.to.toLowerCase();\n\t\t\tif (message.to === \".jfif\") message.to = \".jpeg\";\n\n\t\t\tlet from = message.input.from;\n\t\t\tif (from === \".jfif\") from = \".jpeg\";\n\t\t\tif (from === \".fit\") from = \".fits\";\n\n\t\t\tconst buffer = await message.input.file.arrayBuffer();\n\n\t\t\t// special ico handling to split them all into separate images\n\t\t\tif (from === \".ico\") {\n\t\t\t\tconst imgs = MagickImageCollection.create();\n\n\t\t\t\twhile (true) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst img = MagickImage.create(\n\t\t\t\t\t\t\tnew Uint8Array(buffer),\n\t\t\t\t\t\t\tnew MagickReadSettings({\n\t\t\t\t\t\t\t\tformat: MagickFormat.Ico,\n\t\t\t\t\t\t\t\tframeIndex: imgs.length,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t);\n\t\t\t\t\t\timgs.push(img);\n\t\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-unused-vars\n\t\t\t\t\t} catch (_) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (imgs.length === 0) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\ttype: \"error\",\n\t\t\t\t\t\terror: `Failed to read ICO -- no images found inside?`,\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\tconst convertedImgs: Uint8Array[] = [];\n\t\t\t\tawait Promise.all(\n\t\t\t\t\timgs.map(async (img, i) => {\n\t\t\t\t\t\tconst output = await magickConvert(\n\t\t\t\t\t\t\timg,\n\t\t\t\t\t\t\tmessage.to,\n\t\t\t\t\t\t\tkeepMetadata,\n\t\t\t\t\t\t\tcompression,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconvertedImgs[i] = output;\n\t\t\t\t\t}),\n\t\t\t\t);\n\n\t\t\t\tconst zip = makeZip(\n\t\t\t\t\tconvertedImgs.map(\n\t\t\t\t\t\t(img, i) =>\n\t\t\t\t\t\t\tnew File(\n\t\t\t\t\t\t\t\t[new Uint8Array(img)],\n\t\t\t\t\t\t\t\t`image${i}.${message.to.slice(1)}`,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t\t\"images.zip\",\n\t\t\t\t);\n\n\t\t\t\t// read the ReadableStream to the end\n\t\t\t\tconst zipBytes = await readToEnd(zip.getReader());\n\n\t\t\t\timgs.dispose();\n\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"finished\",\n\t\t\t\t\toutput: zipBytes,\n\t\t\t\t\tzip: true,\n\t\t\t\t};\n\t\t\t} else if (from === \".ani\") {\n\t\t\t\tconsole.log(\"Parsing ANI file\");\n\t\t\t\ttry {\n\t\t\t\t\tconst parsedAni = parseAni(new Uint8Array(buffer));\n\t\t\t\t\tconst files: File[] = [];\n\t\t\t\t\tawait Promise.all(\n\t\t\t\t\t\tparsedAni.images.map(async (img, i) => {\n\t\t\t\t\t\t\tconst blob = await magickConvert(\n\t\t\t\t\t\t\t\tMagickImage.create(\n\t\t\t\t\t\t\t\t\timg,\n\t\t\t\t\t\t\t\t\tnew MagickReadSettings({\n\t\t\t\t\t\t\t\t\t\tformat: MagickFormat.Ico,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\tmessage.to,\n\t\t\t\t\t\t\t\tkeepMetadata,\n\t\t\t\t\t\t\t\tcompression,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tfiles.push(\n\t\t\t\t\t\t\t\tnew File(\n\t\t\t\t\t\t\t\t\t[new Uint8Array(blob)],\n\t\t\t\t\t\t\t\t\t`image${i}${message.to}`,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\n\t\t\t\t\tconst zip = makeZip(files, \"images.zip\");\n\t\t\t\t\tconst zipBytes = await readToEnd(zip.getReader());\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\ttype: \"finished\",\n\t\t\t\t\t\toutput: zipBytes,\n\t\t\t\t\t\tzip: true,\n\t\t\t\t\t};\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconsole.error(e);\n\t\t\t\t}\n\t\t\t} else if (from === \".icns\") {\n\t\t\t\tconst icns: Uint8Array[] = parseIcns(new Uint8Array(buffer));\n\t\t\t\tif (typeof icns === \"string\") {\n\t\t\t\t\treturn {\n\t\t\t\t\t\ttype: \"error\",\n\t\t\t\t\t\terror: `Failed to read ICNS -- ${icns}`,\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\tconst formats = [\n\t\t\t\t\tMagickFormat.Png,\n\t\t\t\t\tMagickFormat.Jpeg,\n\t\t\t\t\tMagickFormat.Rgba,\n\t\t\t\t\tMagickFormat.Rgb,\n\t\t\t\t];\n\t\t\t\tconst outputs: Uint8Array[] = [];\n\t\t\t\tfor (const file of icns) {\n\t\t\t\t\tfor (const format of formats) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst img = MagickImage.create(\n\t\t\t\t\t\t\t\tfile,\n\t\t\t\t\t\t\t\tnew MagickReadSettings({\n\t\t\t\t\t\t\t\t\tformat: format,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tconst converted = await magickConvert(\n\t\t\t\t\t\t\t\timg,\n\t\t\t\t\t\t\t\tmessage.to,\n\t\t\t\t\t\t\t\tkeepMetadata,\n\t\t\t\t\t\t\t\tcompression,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\toutputs.push(converted);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-unused-vars\n\t\t\t\t\t\t} catch (_) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst zip = makeZip(\n\t\t\t\t\toutputs.map(\n\t\t\t\t\t\t(img, i) =>\n\t\t\t\t\t\t\tnew File(\n\t\t\t\t\t\t\t\t[new Uint8Array(img)],\n\t\t\t\t\t\t\t\t`image${i}.${message.to.slice(1)}`,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t\t\"images.zip\",\n\t\t\t\t);\n\t\t\t\tconst zipBytes = await readToEnd(zip.getReader());\n\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"finished\",\n\t\t\t\t\toutput: zipBytes,\n\t\t\t\t\tzip: true,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// build frames of animated formats (webp/gif)\n\t\t\t// APNG does not work on magick-wasm since it needs ffmpeg built-in (not in magick-wasm) - handle in ffmpeg\n\t\t\tif (\n\t\t\t\t(from === \".webp\" || from === \".gif\") &&\n\t\t\t\t(message.to === \".gif\" || message.to === \".webp\")\n\t\t\t) {\n\t\t\t\tconst collection = MagickImageCollection.create(\n\t\t\t\t\tnew Uint8Array(buffer),\n\t\t\t\t);\n\t\t\t\tconst format =\n\t\t\t\t\tmessage.to === \".gif\"\n\t\t\t\t\t\t? MagickFormat.Gif\n\t\t\t\t\t\t: MagickFormat.WebP;\n\t\t\t\tconst result = await new Promise<Uint8Array>((resolve) => {\n\t\t\t\t\tcollection.write(format, (output) => {\n\t\t\t\t\t\tresolve(structuredClone(output));\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t\tcollection.dispose();\n\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"finished\",\n\t\t\t\t\toutput: result,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst img = MagickImage.create(\n\t\t\t\tnew Uint8Array(buffer),\n\t\t\t\tnew MagickReadSettings({\n\t\t\t\t\tformat: from.slice(1).toUpperCase() as MagickFormat,\n\t\t\t\t}),\n\t\t\t);\n\n\t\t\tconst converted = await magickConvert(\n\t\t\t\timg,\n\t\t\t\tmessage.to,\n\t\t\t\tkeepMetadata,\n\t\t\t\tcompression,\n\t\t\t);\n\n\t\t\treturn {\n\t\t\t\ttype: \"finished\",\n\t\t\t\toutput: converted,\n\t\t\t};\n\t\t}\n\t\tdefault:\n\t\t\treturn {\n\t\t\t\ttype: \"error\",\n\t\t\t\terror: `Unknown message type: ${message.type}`,\n\t\t\t};\n\t}\n};\n\nconst readToEnd = async (reader: ReadableStreamDefaultReader<Uint8Array>) => {\n\tconst chunks: Uint8Array[] = [];\n\tlet done = false;\n\twhile (!done) {\n\t\tconst { value, done: d } = await reader.read();\n\t\tif (value) chunks.push(value);\n\t\tdone = d;\n\t}\n\tconst blob = new Blob(\n\t\tchunks.map((chunk) => new Uint8Array(chunk)),\n\t\t{ type: \"application/zip\" },\n\t);\n\tconst arrayBuffer = await blob.arrayBuffer();\n\treturn new Uint8Array(arrayBuffer);\n};\n\nconst magickConvert = async (\n\timg: IMagickImage,\n\tto: string,\n\tkeepMetadata: boolean,\n\tcompression?: number,\n) => {\n\tlet fmt = to.slice(1).toUpperCase();\n\tif (fmt === \"JFIF\") fmt = \"JPEG\";\n\n\t// ICO size clamp to avoid WidthOrHeightExceedsLimit\n\tif (fmt === \"ICO\") {\n\t\tconst max = 256;\n\t\tconst w = img.width;\n\t\tconst h = img.height;\n\n\t\tif (w > max || h > max) {\n\t\t\tconst scale = max / Math.max(w, h);\n\t\t\tconst newW = Math.max(1, Math.round(w * scale));\n\t\t\tconst newH = Math.max(1, Math.round(h * scale));\n\n\t\t\timg.resize(newW, newH);\n\t\t}\n\t}\n\n\tconst result = await new Promise<Uint8Array>((resolve, reject) => {\n\t\ttry {\n\t\t\t// magick-wasm automatically clamps (https://github.com/dlemstra/magick-wasm/blob/76fc6f2b0c0497d2ddc251bbf6174b4dc92ac3ea/src/magick-image.ts#L2480)\n\t\t\tif (compression) img.quality = compression;\n\t\t\tif (!keepMetadata) img.strip();\n\n\t\t\timg.write(fmt as unknown as MagickFormat, (o: Uint8Array) => {\n\t\t\t\tresolve(structuredClone(o));\n\t\t\t});\n\t\t} catch (error) {\n\t\t\treject(error);\n\t\t}\n\t});\n\n\treturn result;\n};\n\nonmessage = async (e) => {\n\tconst message = e.data;\n\ttry {\n\t\tconst res = await handleMessage(message);\n\t\tif (!res) return;\n\t\tpostMessage({\n\t\t\t...res,\n\t\t\tid: message.id,\n\t\t});\n\t} catch (e) {\n\t\tpostMessage({\n\t\t\ttype: \"error\",\n\t\t\terror: e,\n\t\t\tid: message.id,\n\t\t});\n\t}\n};\n"
  },
  {
    "path": "src/lib/workers/pandoc.ts",
    "content": "import type { WorkerMessage } from \"$lib/types\";\nimport * as wasiShim from \"@bjorn3/browser_wasi_shim\";\nimport * as zip from \"client-zip\";\n\nself.onmessage = async (e) => {\n\tconst message = e.data;\n\ttry {\n\t\tconst res = await handleMessage(message);\n\t\tif (!res) return;\n\t\tself.postMessage({\n\t\t\t...res,\n\t\t\tid: message.id,\n\t\t});\n\t} catch (e) {\n\t\tself.postMessage({\n\t\t\ttype: \"error\",\n\t\t\terror: e,\n\t\t\tid: message.id,\n\t\t});\n\t}\n};\n\nlet wasm: ArrayBuffer = null!;\n\ntype Format =\n\t| \".md\"\n\t| \".docx\"\n\t| \".csv\"\n\t| \".tsv\"\n\t| \".json\"\n\t| \".doc\"\n\t| \".rtf\"\n\t| \".rst\"\n\t| \".epub\"\n\t| \".odt\"\n\t| \".docbook\"\n\t| \".html\"\n\t| \".markdown\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst handleMessage = async (message: WorkerMessage): Promise<any> => {\n\tswitch (message.type) {\n\t\tcase \"load\": {\n\t\t\twasm = message.wasm;\n\t\t\tpostMessage({ type: \"loaded\", id: \"0\" });\n\t\t\tbreak;\n\t\t}\n\n\t\tcase \"convert\": {\n\t\t\ttry {\n\t\t\t\tconst { to: ext, input } = message;\n\t\t\t\tconst file = input.file as File;\n\t\t\t\tconst to = ext as Format;\n\t\t\t\tif (to === \".rtf\") {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\"Converting into RTF is currently not supported.\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst buf = new Uint8Array(await file.arrayBuffer());\n\t\t\t\tconst args = `-f ${formatToReader(`.${file.name.split(\".\").pop() || \"\"}` as Format)} -t ${formatToReader(to)} --extract-media=.`;\n\t\t\t\tconst [result, stderr, zip] = await pandoc(\n\t\t\t\t\targs,\n\t\t\t\t\tbuf,\n\t\t\t\t\tfile.name,\n\t\t\t\t\tto,\n\t\t\t\t);\n\t\t\t\tif (result.length === 0) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\ttype: \"error\",\n\t\t\t\t\t\terror: stderr\n\t\t\t\t\t\t\t.replaceAll(\"\\\\n\", \"\\n\")\n\t\t\t\t\t\t\t.replaceAll('\\\\\"', '\"')\n\t\t\t\t\t\t\t.split('\"')\n\t\t\t\t\t\t\t.slice(1, -1)\n\t\t\t\t\t\t\t.join('\"'),\n\t\t\t\t\t\terrorKind: stderr.split(\" \")[0],\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"finished\",\n\t\t\t\t\toutput: result,\n\t\t\t\t\tisZip: zip,\n\t\t\t\t};\n\t\t\t} catch (e) {\n\t\t\t\tconsole.error(e);\n\t\t\t\treturn { type: \"error\", error: e };\n\t\t\t}\n\t\t}\n\t}\n};\n\nconst formatToReader = (format: Format): string => {\n\tswitch (format) {\n\t\tcase \".md\":\n\t\tcase \".markdown\":\n\t\t\treturn \"markdown\";\n\t\tcase \".doc\":\n\t\tcase \".docx\":\n\t\t\treturn \"docx\";\n\t\tcase \".csv\":\n\t\t\treturn \"csv\";\n\t\tcase \".tsv\":\n\t\t\treturn \"tsv\";\n\t\tcase \".docbook\":\n\t\t\treturn \"docbook\";\n\t\tcase \".epub\":\n\t\t\treturn \"epub\";\n\t\tcase \".html\":\n\t\t\treturn \"html\";\n\t\tcase \".json\":\n\t\t\treturn \"json\";\n\t\tcase \".odt\":\n\t\t\treturn \"odt\";\n\t\tcase \".rtf\":\n\t\t\treturn \"rtf\";\n\t\tcase \".rst\":\n\t\t\treturn \"rst\";\n\t}\n\n\tthrow new Error(`Unsupported format: ${format}`);\n};\n\nasync function pandoc(\n\targs_str: string,\n\tin_data: Uint8Array,\n\tin_name: string,\n\tout_ext: string,\n): Promise<[Uint8Array, string, boolean]> {\n\tif (!wasm) throw new Error(\"WASM not loaded\");\n\tlet stderr = \"\";\n\tconst args = [\"pandoc.wasm\", \"+RTS\", \"-H64m\", \"-RTS\"];\n\tconst env: string[] = [];\n\tconst in_file = new wasiShim.File(in_data, {\n\t\treadonly: true,\n\t});\n\tconst out_file = new wasiShim.File(new Uint8Array(), {\n\t\treadonly: false,\n\t});\n\tconst map = new Map<string, wasiShim.File>([\n\t\t[\"in\", in_file],\n\t\t[\"out\", out_file],\n\t]);\n\tconst root = new wasiShim.PreopenDirectory(\"/\", map);\n\tconst fds = [\n\t\tnew wasiShim.OpenFile(\n\t\t\tnew wasiShim.File(new Uint8Array(), { readonly: true }),\n\t\t),\n\t\twasiShim.ConsoleStdout.lineBuffered((msg) => {\n\t\t\tconsole.log(`[WASI stdout] ${msg}`);\n\t\t}),\n\t\twasiShim.ConsoleStdout.lineBuffered((msg) => {\n\t\t\tconsole.warn(`[WASI stderr] ${msg}`);\n\t\t\tstderr += msg + \"\\n\";\n\t\t}),\n\t\troot,\n\t\tnew wasiShim.PreopenDirectory(\"/tmp\", new Map()),\n\t];\n\n\tconst wasi = new wasiShim.WASI(args, env, fds, { debug: false });\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tconst { instance }: { instance: any } = await WebAssembly.instantiate(\n\t\twasm,\n\t\t{\n\t\t\twasi_snapshot_preview1: wasi.wasiImport,\n\t\t},\n\t);\n\n\twasi.initialize(instance);\n\n\tinstance.exports.__wasm_call_ctors();\n\n\tfunction memory_data_view() {\n\t\treturn new DataView(instance.exports.memory.buffer);\n\t}\n\n\tconst argc_ptr = instance.exports.malloc(4);\n\tmemory_data_view().setUint32(argc_ptr, args.length, true);\n\tconst argv = instance.exports.malloc(4 * (args.length + 1));\n\tfor (let i = 0; i < args.length; ++i) {\n\t\tconst arg = instance.exports.malloc(args[i].length + 1);\n\t\tnew TextEncoder().encodeInto(\n\t\t\targs[i],\n\t\t\tnew Uint8Array(instance.exports.memory.buffer, arg, args[i].length),\n\t\t);\n\t\tmemory_data_view().setUint8(arg + args[i].length, 0);\n\t\tmemory_data_view().setUint32(argv + 4 * i, arg, true);\n\t}\n\tmemory_data_view().setUint32(argv + 4 * args.length, 0, true);\n\tconst argv_ptr = instance.exports.malloc(4);\n\tmemory_data_view().setUint32(argv_ptr, argv, true);\n\n\tinstance.exports.hs_init_with_rtsopts(argc_ptr, argv_ptr);\n\n\tconst args_ptr = instance.exports.malloc(args_str.length);\n\tnew TextEncoder().encodeInto(\n\t\targs_str,\n\t\tnew Uint8Array(\n\t\t\tinstance.exports.memory.buffer,\n\t\t\targs_ptr,\n\t\t\targs_str.length,\n\t\t),\n\t);\n\n\tinstance.exports.wasm_main(args_ptr, args_str.length);\n\t// list all files in /\n\tconst openedPath = root.dir.path_open(0, BigInt(0), 0).fd_obj;\n\tconst dirRet = openedPath.path_lookup(\".\", 0);\n\tconst dir = dirRet.inode_obj;\n\tif (dir) {\n\t\tconst opened = dir.path_open(0, BigInt(0), 0).fd_obj;\n\t\tif (!opened) {\n\t\t\treturn [out_file.data, stderr, false];\n\t\t}\n\n\t\tconst fs = readRecursive(opened);\n\t\t// const media = fs.get(\"media\");\n\t\t// if (media && media.type === \"folder\") {\n\t\t// \tconst file = new File(\n\t\t// \t\t[out_file.data],\n\t\t// \t\t`${in_name.split(\".\").slice(0, -1).join(\".\")}${out_ext}`,\n\t\t// \t);\n\t\t// \tconst zipped = await zipFiles(file, media.entries);\n\t\t// \treturn [zipped, stderr, true];\n\t\t// }\n\t\t// filter to folders\n\t\tconst folders = [...fs.entries()].filter(\n\t\t\t(f) => f[0] !== \"in\" && f[0] !== \"out\",\n\t\t);\n\t\tif (folders.length > 0) {\n\t\t\tconst file = new File(\n\t\t\t\t[new Uint8Array(Array.from(out_file.data))],\n\t\t\t\t`${in_name.split(\".\").slice(0, -1).join(\".\")}${out_ext}`,\n\t\t\t);\n\t\t\tconst filteredMap = new Map<string, PandocFsEntry>();\n\t\t\tfor (const [name, entry] of folders) {\n\t\t\t\tfilteredMap.set(name, entry);\n\t\t\t}\n\t\t\tconst zipped = await zipFiles(file, filteredMap);\n\t\t\treturn [zipped, stderr, true];\n\t\t}\n\t}\n\treturn [out_file.data, stderr, false];\n}\n\nconst zipFiles = async (\n\toutput: File,\n\tentries: PandocEntries,\n): Promise<Uint8Array> => {\n\tconst zipFormatted = pandocToFiles(entries);\n\tconst zipped = zip.makeZip([...zipFormatted, output]);\n\t// read the ReadableStream to the end\n\tconst reader = zipped.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet done = false;\n\twhile (!done) {\n\t\tconst { done: d, value } = await reader.read();\n\t\tdone = d;\n\t\tif (value) {\n\t\t\tchunks.push(value);\n\t\t}\n\t}\n\tconst totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);\n\tconst result = new Uint8Array(totalLength);\n\tlet offset = 0;\n\tfor (const chunk of chunks) {\n\t\tresult.set(chunk, offset);\n\t\toffset += chunk.length;\n\t}\n\treturn result;\n};\n\nconst pandocToFiles = (entries: PandocEntries, parent = \"\"): File[] => {\n\tconst flattened: File[] = [];\n\n\tfor (const [name, entry] of entries) {\n\t\tconst fullPath = parent ? `${parent}/${name}` : name;\n\n\t\tif (entry.type === \"folder\") {\n\t\t\tconst nestedFiles = pandocToFiles(entry.entries, fullPath);\n\t\t\tflattened.push(...nestedFiles);\n\t\t} else {\n\t\t\tconst file = new File([new Uint8Array(Array.from(entry.data))], fullPath);\n\t\t\tflattened.push(file);\n\t\t}\n\t}\n\n\treturn flattened;\n};\n\nconst readRecursive = (fd: wasiShim.Fd): PandocEntries => {\n\tconst entries = new Map<string, PandocFsEntry>();\n\tconst stat = fd.fd_filestat_get().filestat;\n\tif (!stat) return entries;\n\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tconst dir: any = fd.path_lookup(\".\", 0).inode_obj;\n\tif (!dir) return entries;\n\tconst dirEntries: Map<string, wasiShim.File | wasiShim.Directory> =\n\t\tdir.contents;\n\tconst results = readRecursiveInternal(dirEntries);\n\tfor (const [name, entry] of results) {\n\t\tentries.set(name, entry);\n\t}\n\n\treturn entries;\n};\n\nconst readRecursiveInternal = (\n\tcontents: Map<string, wasiShim.File | wasiShim.Directory>,\n): PandocEntries => {\n\tconst entries = new Map<string, PandocFsEntry>();\n\tfor (const [name, entry] of contents) {\n\t\tif (entry instanceof wasiShim.File) {\n\t\t\tconst file: PandocFile = {\n\t\t\t\tdata: entry.data,\n\t\t\t\ttype: \"file\",\n\t\t\t};\n\t\t\tentries.set(name, file);\n\t\t} else {\n\t\t\tconst folder: PandocFolder = {\n\t\t\t\tentries: readRecursiveInternal(\n\t\t\t\t\tentry.contents as unknown as Map<\n\t\t\t\t\t\tstring,\n\t\t\t\t\t\twasiShim.File | wasiShim.Directory\n\t\t\t\t\t>,\n\t\t\t\t),\n\t\t\t\ttype: \"folder\",\n\t\t\t};\n\t\t\tentries.set(name, folder);\n\t\t}\n\t}\n\treturn entries;\n};\n\ntype PandocEntries = Map<string, PandocFsEntry>;\n\ninterface PandocFile {\n\tdata: Uint8Array;\n\ttype: \"file\";\n}\n\ninterface PandocFolder {\n\tentries: PandocEntries;\n\ttype: \"folder\";\n}\n\ntype PandocFsEntry = PandocFile | PandocFolder;\n"
  },
  {
    "path": "src/routes/+layout.server.ts",
    "content": "export const load = () => {\n\tconst isAprilFools =\n\t\tnew Date().getDate() === 1 && new Date().getMonth() === 3;\n\treturn { isAprilFools };\n};\n"
  },
  {
    "path": "src/routes/+layout.svelte",
    "content": "<script lang=\"ts\">\n\timport { onMount } from \"svelte\";\n\timport { goto, beforeNavigate, afterNavigate } from \"$app/navigation\";\n\n\timport { PUB_PLAUSIBLE_URL, PUB_HOSTNAME } from \"$env/static/public\";\n\timport { DISABLE_ALL_EXTERNAL_REQUESTS, VERT_NAME } from \"$lib/util/consts.js\";\n\timport * as Layout from \"$lib/components/layout\";\n\timport * as Navbar from \"$lib/components/layout/Navbar\";\n\timport featuredImage from \"$lib/assets/VERT_Feature.webp\";\n\timport { Settings } from \"$lib/sections/settings/index.svelte\";\n\timport {\n\t\tfiles,\n\t\tisMobile,\n\t\teffects,\n\t\ttheme,\n\t\tdropping,\n\t\tvertdLoaded,\n\t\tlocale,\n\t\tupdateLocale,\n\t} from \"$lib/store/index.svelte\";\n\timport \"$lib/css/app.scss\";\n\timport { browser } from \"$app/environment\";\n\timport { initStores as initAnimStores } from \"$lib/util/animation.js\";\n\timport { VertdInstance } from \"$lib/sections/settings/vertdSettings.svelte.js\";\n\timport { ToastManager } from \"$lib/util/toast.svelte.js\";\n\timport { m } from \"$lib/paraglide/messages.js\";\n\timport { log } from \"$lib/util/logger.js\";\n\n\tlet { children, data } = $props();\n\tlet enablePlausible = $state(false);\n\n\tlet scrollPositions = new Map<string, number>();\n\n\tbeforeNavigate((nav) => {\n\t\tif (!nav.from || !$isMobile) return;\n\t\tscrollPositions.set(nav.from.url.pathname, window.scrollY);\n\t});\n\n\tafterNavigate((nav) => {\n\t\tif (!$isMobile) return;\n\t\tconst scrollY = nav.to\n\t\t\t? scrollPositions.get(nav.to.url.pathname) || 0\n\t\t\t: 0;\n\t\twindow.scrollTo(0, scrollY);\n\t});\n\n\tconst dropFiles = (e: DragEvent) => {\n\t\te.preventDefault();\n\t\tdropping.set(false);\n\t\tconst oldLength = files.files.length;\n\t\tfiles.add(e.dataTransfer?.files);\n\t\tif (oldLength !== files.files.length) goto(\"/convert\");\n\t};\n\n\tconst handleDrag = (e: DragEvent, drag: boolean) => {\n\t\te.preventDefault();\n\t\tdropping.set(drag);\n\t};\n\n\tconst handlePaste = (e: ClipboardEvent) => {\n\t\tconst clipboardData = e.clipboardData;\n\t\tif (!clipboardData || !clipboardData.files.length) return;\n\t\te.preventDefault();\n\t\tconst oldLength = files.files.length;\n\t\tfiles.add(clipboardData.files);\n\t\tif (oldLength !== files.files.length) goto(\"/convert\");\n\t};\n\n\tonMount(() => {\n\t\tinitAnimStores();\n\n\t\tconst handleResize = () => {\n\t\t\tisMobile.set(window.innerWidth <= 768);\n\t\t};\n\n\t\tisMobile.set(window.innerWidth <= 768); // initial page load\n\t\twindow.addEventListener(\"resize\", handleResize); // handle window resize\n\t\twindow.addEventListener(\"paste\", handlePaste);\n\n\t\teffects.set(localStorage.getItem(\"effects\") !== \"false\"); // defaults to true if not set\n\t\ttheme.set(\n\t\t\t(localStorage.getItem(\"theme\") as \"light\" | \"dark\") || \"light\",\n\t\t);\n\t\tconst storedLocale = localStorage.getItem(\"locale\");\n\t\tif (storedLocale) updateLocale(storedLocale);\n\n\t\tSettings.instance.load();\n\n\t\tif (!DISABLE_ALL_EXTERNAL_REQUESTS) {\n\t\t\tVertdInstance.instance\n\t\t\t\t.url()\n\t\t\t\t.then((u) => fetch(`${u}/api/version`))\n\t\t\t\t.then((res) => {\n\t\t\t\t\tif (res.ok) $vertdLoaded = true;\n\t\t\t\t});\n\t\t}\n\n\t\t// detect if insecure context\n\t\tif (!window.isSecureContext) {\n\t\t\tlog([\"layout\"], \"Insecure context (HTTP) detected, some features may not work as expected -- you may want to enable \\\"PUB_DISABLE_FAILURE_BLOCKS\\\" on local deployments.\");\n\t\t\tToastManager.add({\n\t\t\t\ttype: \"warning\",\n\t\t\t\tmessage: m[\"toast.insecure_context\"](),\n\t\t\t\tdisappearing: false,\n\t\t\t});\n\t\t}\n\n\t\treturn () => {\n\t\t\twindow.removeEventListener(\"paste\", handlePaste);\n\t\t\twindow.removeEventListener(\"resize\", handleResize);\n\t\t};\n\t});\n\n\t$effect(() => {\n\t\tenablePlausible =\n\t\t\t!!PUB_PLAUSIBLE_URL &&\n\t\t\tSettings.instance.settings.plausible &&\n\t\t\t!DISABLE_ALL_EXTERNAL_REQUESTS;\n\t\tif (!enablePlausible && browser) {\n\t\t\t// reset pushState on opt-out so that plausible stops firing events on page navigation\n\t\t\thistory.pushState = History.prototype.pushState;\n\t\t}\n\t});\n</script>\n\n<svelte:head>\n\t<title>{VERT_NAME}</title>\n\t<meta name=\"theme-color\" content=\"#F2ABEE\" />\n\t<meta\n\t\tname=\"title\"\n\t\tcontent=\"{VERT_NAME} — Free, fast, and awesome file converter\"\n\t/>\n\t<meta\n\t\tname=\"description\"\n\t\tcontent=\"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.\"\n\t/>\n\t<meta property=\"og:url\" content=\"https://vert.sh\" />\n\t<meta property=\"og:type\" content=\"website\" />\n\t<meta\n\t\tproperty=\"og:title\"\n\t\tcontent=\"{VERT_NAME} — Free, fast, and awesome file converter\"\n\t/>\n\t<meta\n\t\tproperty=\"og:description\"\n\t\tcontent=\"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.\"\n\t/>\n\t<meta property=\"og:image\" content={featuredImage} />\n\t<meta name=\"twitter:card\" content=\"summary_large_image\" />\n\t<meta property=\"twitter:domain\" content=\"vert.sh\" />\n\t<meta property=\"twitter:url\" content=\"https://vert.sh\" />\n\t<meta\n\t\tproperty=\"twitter:title\"\n\t\tcontent=\"{VERT_NAME} — Free, fast, and awesome file converter\"\n\t/>\n\t<meta\n\t\tproperty=\"twitter:description\"\n\t\tcontent=\"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.\"\n\t/>\n\t<meta property=\"twitter:image\" content={featuredImage} />\n\t<link rel=\"manifest\" href=\"/manifest.json\" />\n\t<link rel=\"canonical\" href=\"https://vert.sh/\" />\n\t{#if enablePlausible}\n\t\t<script\n\t\t\tdefer\n\t\t\tdata-domain={PUB_HOSTNAME || \"vert.sh\"}\n\t\t\tsrc=\"{PUB_PLAUSIBLE_URL}/js/script.js\"\n\t\t></script>\n\t{/if}\n\t{#if data.isAprilFools}\n\t\t<style>\n\t\t\t* {\n\t\t\t\tfont-family: \"Comic Sans MS\", \"Comic Sans\", cursive !important;\n\t\t\t}\n\t\t</style>\n\t{/if}\n</svelte:head>\n\n<!-- FIXME: if user resizes between desktop/mobile, highlight of page disappears (only shows on original size) -->\n{#key $locale}\n\t<div\n\t\tclass=\"flex flex-col min-h-screen h-full w-full overflow-x-hidden\"\n\t\tondrop={dropFiles}\n\t\tondragenter={(e) => handleDrag(e, true)}\n\t\tondragover={(e) => handleDrag(e, true)}\n\t\tondragleave={(e) => handleDrag(e, false)}\n\t\trole=\"region\"\n\t>\n\t\t<Layout.UploadRegion />\n\n\t\t<div>\n\t\t\t<Layout.MobileLogo />\n\t\t\t<Navbar.Desktop />\n\t\t</div>\n\n\t\t<!-- \n\t\tSvelteKit throws the following warning when developing - safe to ignore as we render the children in this component:\n\t\t`<slot />` or `{@render ...}` tag missing — inner content will not be rendered\n\t\t-->\n\t\t<Layout.PageContent {children} />\n\n\t\t<Layout.Toasts />\n\t\t<Layout.Dialogs />\n\n\t\t<div>\n\t\t\t<Layout.Footer />\n\t\t\t<Navbar.Mobile />\n\t\t</div>\n\t</div>\n{/key}\n\n<!-- Gradients placed here to prevent it overlapping in transitions -->\n<Layout.Gradients />\n"
  },
  {
    "path": "src/routes/+layout.ts",
    "content": "import { browser } from \"$app/environment\";\n\nexport const load = ({ data }) => {\n\tif (!browser) return data;\n\twindow.plausible =\n\t\twindow.plausible ||\n\t\t((_, opts) => {\n\t\t\topts?.callback?.({\n\t\t\t\tstatus: 200,\n\t\t\t});\n\t\t});\n\n\treturn data;\n};\n\nexport const prerender = true;\nexport const trailingSlash = \"always\";\n"
  },
  {
    "path": "src/routes/+page.svelte",
    "content": "<script lang=\"ts\">\n\timport Uploader from \"$lib/components/functional/Uploader.svelte\";\n\timport Tooltip from \"$lib/components/visual/Tooltip.svelte\";\n\timport { converters } from \"$lib/converters\";\n\timport { vertdLoaded } from \"$lib/store/index.svelte\";\n\timport clsx from \"clsx\";\n\timport { AudioLines, BookText, Check, Film, Image } from \"lucide-svelte\";\n\timport { m } from \"$lib/paraglide/messages\";\n\timport { OverlayScrollbarsComponent } from \"overlayscrollbars-svelte\";\n\timport { browser } from \"$app/environment\";\n\timport \"overlayscrollbars/overlayscrollbars.css\";\n\timport { onMount } from \"svelte\";\n\timport type { WorkerStatus } from \"$lib/converters/converter.svelte\";\n\timport { sanitize } from \"$lib/store/index.svelte\";\n\timport { DISABLE_ALL_EXTERNAL_REQUESTS } from \"$lib/util/consts\";\n\n\tconst getSupportedFormats = (name: string) =>\n\t\tconverters\n\t\t\t.find((c) => c.name === name)\n\t\t\t?.supportedFormats.map(\n\t\t\t\t(f) =>\n\t\t\t\t\t`${f.name}${f.fromSupported && f.toSupported ? \"\" : \"*\"}`,\n\t\t\t)\n\t\t\t.join(\", \") || \"none\";\n\n\tconst worker: {\n\t\t[key: string]: {\n\t\t\tformats: string;\n\t\t\ticon: typeof Image;\n\t\t\ttitle: string;\n\t\t\tstatus: WorkerStatus;\n\t\t};\n\t} = $derived.by(() => {\n\t\tconst output: {\n\t\t\t[key: string]: {\n\t\t\t\tformats: string;\n\t\t\t\ticon: typeof Image;\n\t\t\t\ttitle: string;\n\t\t\t\tstatus: WorkerStatus;\n\t\t\t};\n\t\t} = {\n\t\t\tImages: {\n\t\t\t\tformats: getSupportedFormats(\"imagemagick\"),\n\t\t\t\ticon: Image,\n\t\t\t\ttitle: m[\"upload.cards.images\"](),\n\t\t\t\tstatus:\n\t\t\t\t\tconverters.find((c) => c.name === \"imagemagick\")?.status ||\n\t\t\t\t\t\"not-ready\",\n\t\t\t},\n\t\t\tAudio: {\n\t\t\t\tformats: getSupportedFormats(\"ffmpeg\"),\n\t\t\t\ticon: AudioLines,\n\t\t\t\ttitle: m[\"upload.cards.audio\"](),\n\t\t\t\tstatus:\n\t\t\t\t\tconverters.find((c) => c.name === \"ffmpeg\")?.status ||\n\t\t\t\t\t\"not-ready\",\n\t\t\t},\n\t\t\tDocuments: {\n\t\t\t\tformats: getSupportedFormats(\"pandoc\"),\n\t\t\t\ticon: BookText,\n\t\t\t\ttitle: m[\"upload.cards.documents\"](),\n\t\t\t\tstatus:\n\t\t\t\t\tconverters.find((c) => c.name === \"pandoc\")?.status ||\n\t\t\t\t\t\"not-ready\",\n\t\t\t},\n\t\t};\n\n\t\tif (!DISABLE_ALL_EXTERNAL_REQUESTS) {\n\t\t\toutput.Video = {\n\t\t\t\tformats: getSupportedFormats(\"vertd\"),\n\t\t\t\ticon: Film,\n\t\t\t\ttitle: m[\"upload.cards.video\"](),\n\t\t\t\tstatus: $vertdLoaded === true ? \"ready\" : \"not-ready\", // not using converter.status for this\n\t\t\t};\n\t\t}\n\n\t\treturn output;\n\t});\n\n\tconst getTooltip = (format: string) => {\n\t\tconst converter = converters.find((c) =>\n\t\t\tc.supportedFormats.some((sf) => sf.name === format),\n\t\t);\n\n\t\tconst formatInfo = converter?.supportedFormats.find(\n\t\t\t(sf) => sf.name === format,\n\t\t);\n\n\t\tif (formatInfo) {\n\t\t\tconst direction = formatInfo.fromSupported\n\t\t\t\t? m[\"upload.tooltip.direction_input\"]()\n\t\t\t\t: m[\"upload.tooltip.direction_output\"]();\n\t\t\treturn m[\"upload.tooltip.partial_support\"]({ direction });\n\t\t}\n\t\treturn \"\";\n\t};\n\n\tconst getStatusText = (status: WorkerStatus) => {\n\t\tswitch (status) {\n\t\t\tcase \"downloading\":\n\t\t\t\treturn m[\"upload.cards.status.downloading\"]();\n\t\t\tcase \"ready\":\n\t\t\t\treturn m[\"upload.cards.status.ready\"]();\n\t\t\tdefault:\n\t\t\t\t// \"not-ready\", \"error\" and other statuses (somehow)\n\t\t\t\treturn m[\"upload.cards.status.not_ready\"]();\n\t\t}\n\t};\n\n\tlet scrollContainers: HTMLElement[] = $state([]);\n\t// svelte-ignore state_referenced_locally\n\tlet showBlur = $state(Array(Object.keys(worker).length).fill(false));\n\n\tonMount(() => {\n\t\tconst handleResize = () => {\n\t\t\tfor (let i = 0; i < scrollContainers.length; i++) {\n\t\t\t\t// show bottom blur if scrollable\n\t\t\t\tconst container = scrollContainers[i];\n\t\t\t\tif (!container) return;\n\t\t\t\tshowBlur[i] = container.scrollHeight > container.clientHeight;\n\t\t\t}\n\t\t};\n\n\t\thandleResize();\n\t\twindow.addEventListener(\"resize\", handleResize);\n\n\t\treturn () => {\n\t\t\twindow.removeEventListener(\"resize\", handleResize);\n\t\t};\n\t});\n</script>\n\n<div class=\"max-w-6xl w-full mx-auto px-6 md:px-8\">\n\t<div class=\"flex items-center justify-center pb-10 md:py-16\">\n\t\t<div\n\t\t\tclass=\"flex items-center h-auto gap-12 md:gap-24 md:flex-row flex-col\"\n\t\t>\n\t\t\t<div class=\"flex-grow w-full text-center md:text-left\">\n\t\t\t\t<h1\n\t\t\t\t\tclass=\"text-4xl px-12 md:p-0 md:text-6xl flex-wrap tracking-tight leading-tight md:leading-[72px] mb-4 md:mb-6\"\n\t\t\t\t>\n\t\t\t\t\t{m[\"upload.title\"]()}\n\t\t\t\t</h1>\n\t\t\t\t<p\n\t\t\t\t\tclass=\"font-normal px-5 md:p-0 text-lg md:text-xl text-black text-muted dynadark:text-muted\"\n\t\t\t\t>\n\t\t\t\t\t{m[\"upload.subtitle\"]()}\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t\t<div class=\"flex-grow w-full h-72\">\n\t\t\t\t<Uploader class=\"w-full h-full\" />\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<hr />\n\n\t<div class=\"mt-10 md:mt-16\">\n\t\t<h2 class=\"text-center text-4xl\">{m[\"upload.cards.title\"]()}</h2>\n\n\t\t<div class=\"flex gap-4 mt-8 md:flex-row flex-col\">\n\t\t\t{#if browser}\n\t\t\t\t{#each Object.entries(worker) as [key, s], i}\n\t\t\t\t\t{@const Icon = s.icon}\n\t\t\t\t\t<div class=\"file-category-card w-full flex flex-col gap-4\">\n\t\t\t\t\t\t<div class=\"file-category-card-inner\">\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tclass={clsx(\"icon-container\", {\n\t\t\t\t\t\t\t\t\t\"bg-accent-blue\": key === \"Images\",\n\t\t\t\t\t\t\t\t\t\"bg-accent-purple\": key === \"Audio\",\n\t\t\t\t\t\t\t\t\t\"bg-accent-green\": key === \"Documents\",\n\t\t\t\t\t\t\t\t\t\"bg-accent-red\": key === \"Video\",\n\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<Icon size=\"20\" />\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<span>{s.title}</span>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclass=\"file-category-card-content flex-grow relative\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<OverlayScrollbarsComponent\n\t\t\t\t\t\t\t\toptions={{\n\t\t\t\t\t\t\t\t\tscrollbars: {\n\t\t\t\t\t\t\t\t\t\tautoHide: \"move\",\n\t\t\t\t\t\t\t\t\t\tautoHideDelay: 1500,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tdefer\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\tclass=\"flex flex-col gap-4 h-[12.25rem] relative\"\n\t\t\t\t\t\t\t\t\tbind:this={scrollContainers[i]}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{#if key === \"Video\"}\n\t\t\t\t\t\t\t\t\t\t<p\n\t\t\t\t\t\t\t\t\t\t\tclass=\"flex tems-center justify-center gap-2\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<Check size=\"20\" />\n\t\t\t\t\t\t\t\t\t\t\t<Tooltip\n\t\t\t\t\t\t\t\t\t\t\t\ttext={m[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"upload.tooltip.video_server_processing\"\n\t\t\t\t\t\t\t\t\t\t\t\t]()}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"https://github.com/VERT-sh/VERT/blob/main/docs/VIDEO_CONVERSION.md\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{m[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"upload.cards.video_server_processing\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t]()}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclass=\"text-red-500 -ml-0.5\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t>*</span\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t\t\t<p\n\t\t\t\t\t\t\t\t\t\t\tclass=\"flex tems-center justify-center gap-2\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<Check size=\"20\" />\n\t\t\t\t\t\t\t\t\t\t\t{m[\n\t\t\t\t\t\t\t\t\t\t\t\t\"upload.cards.local_supported\"\n\t\t\t\t\t\t\t\t\t\t\t]()}\n\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t\t\t\t{@html sanitize(m[\"upload.cards.status.text\"]({\n\t\t\t\t\t\t\t\t\t\t\tstatus: getStatusText(s.status),\n\t\t\t\t\t\t\t\t\t\t}))}\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\tclass=\"flex flex-col items-center relative\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<b\n\t\t\t\t\t\t\t\t\t\t\t>{m[\n\t\t\t\t\t\t\t\t\t\t\t\t\"upload.cards.supported_formats\"\n\t\t\t\t\t\t\t\t\t\t\t]()}&nbsp;</b\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<p\n\t\t\t\t\t\t\t\t\t\t\tclass=\"flex flex-wrap justify-center leading-tight px-2\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{#each s.formats.split(\", \") as format, index}\n\t\t\t\t\t\t\t\t\t\t\t\t{@const isPartial =\n\t\t\t\t\t\t\t\t\t\t\t\t\tformat.endsWith(\"*\")}\n\t\t\t\t\t\t\t\t\t\t\t\t{@const formatName = isPartial\n\t\t\t\t\t\t\t\t\t\t\t\t\t? format.slice(0, -1)\n\t\t\t\t\t\t\t\t\t\t\t\t\t: format}\n\t\t\t\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\t\t\t\tclass=\"text-sm font-normal flex items-center relative\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{#if isPartial}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Tooltip\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttext={getTooltip(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tformatName,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{formatName}<span\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclass=\"text-red-500\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>*</span\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{formatName}\n\t\t\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t\t\t\t{#if index < s.formats.split(\", \").length - 1}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span>,&nbsp;</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</OverlayScrollbarsComponent>\n\t\t\t\t\t\t\t<!-- blur at bottom if scrollable - positioned relative to the card container -->\n\t\t\t\t\t\t\t{#if showBlur[i]}\n\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\tclass=\"absolute left-0 bottom-0 w-full h-10 pointer-events-none\"\n\t\t\t\t\t\t\t\t\tstyle={`background: linear-gradient(to top, var(--bg-panel), transparent 100%);`}\n\t\t\t\t\t\t\t\t></div>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t{/if}\n\t\t</div>\n\t</div>\n</div>\n\n<style lang=\"postcss\">\n\t.file-category-card {\n\t\t@apply bg-panel rounded-2xl p-5 shadow-panel relative;\n\t}\n\n\t.file-category-card p {\n\t\t@apply font-normal text-center text-sm;\n\t}\n\n\t.file-category-card-inner {\n\t\t@apply flex items-center justify-center gap-3 text-xl;\n\t}\n\n\t.file-category-card-content {\n\t\t@apply flex flex-col text-center justify-between;\n\t}\n\n\t.icon-container {\n\t\t@apply p-2 rounded-full text-on-accent;\n\t}\n</style>\n"
  },
  {
    "path": "src/routes/about/+page.svelte",
    "content": "<script lang=\"ts\">\n\timport { error } from \"$lib/util/logger\";\n\timport * as About from \"$lib/sections/about\";\n\timport { InfoIcon } from \"lucide-svelte\";\n\timport { onMount } from \"svelte\";\n\timport avatarNullptr from \"$lib/assets/avatars/nullptr.jpg\";\n\timport avatarLiam from \"$lib/assets/avatars/liam.jpg\";\n\timport avatarJovannMC from \"$lib/assets/avatars/jovannmc.jpg\";\n\timport avatarRealmy from \"$lib/assets/avatars/realmy.jpg\";\n\timport avatarAzurejelly from \"$lib/assets/avatars/azurejelly.jpg\";\n\timport { PUB_DONATION_URL, PUB_STRIPE_KEY } from \"$env/static/public\";\n\timport { DISABLE_ALL_EXTERNAL_REQUESTS, GITHUB_API_URL } from \"$lib/util/consts\";\n\timport { m } from \"$lib/paraglide/messages\";\n\timport { ToastManager } from \"$lib/util/toast.svelte\";\n\t// import { dev } from \"$app/environment\";\n\t// import { page } from \"$app/state\";\n\n\t/* interface Donator {\n\t\tname: string;\n\t\tamount?: string | number;\n\t\tavatar: string;\n\t} */\n\n\tinterface Contributor {\n\t\tname: string;\n\t\tgithub: string;\n\t\tavatar: string;\n\t\trole?: string;\n\t}\n\n\t// const donors: Donator[] = [];\n\n\tconst mainContribs: Contributor[] = [\n\t\t{\n\t\t\tname: \"nullptr\",\n\t\t\tgithub: \"https://github.com/not-nullptr\",\n\t\t\trole: m[\"about.credits.roles.lead_developer\"](),\n\t\t\tavatar: avatarNullptr,\n\t\t},\n\t\t{\n\t\t\tname: \"JovannMC\",\n\t\t\tgithub: \"https://github.com/JovannMC\",\n\t\t\trole: m[\"about.credits.roles.developer\"](),\n\t\t\tavatar: avatarJovannMC,\n\t\t},\n\t\t{\n\t\t\tname: \"Liam\",\n\t\t\tgithub: \"https://x.com/z2rMC\",\n\t\t\trole: m[\"about.credits.roles.designer\"](),\n\t\t\tavatar: avatarLiam,\n\t\t},\n\t];\n\n\tconst notableContribs: Contributor[] = [\n\t\t{\n\t\t\tname: \"azurejelly\",\n\t\t\tgithub: \"https://github.com/azurejelly\",\n\t\t\trole: m[\"about.credits.roles.docker_ci\"](),\n\t\t\tavatar: avatarAzurejelly,\n\t\t},\n\t\t{\n\t\t\tname: \"Realmy\",\n\t\t\tgithub: \"https://github.com/RealmyTheMan\",\n\t\t\trole: m[\"about.credits.roles.former_cofounder\"](),\n\t\t\tavatar: avatarRealmy,\n\t\t},\n\t];\n\n\tlet ghContribs: Contributor[] = [];\n\n\tonMount(async () => {\n\t\tif (DISABLE_ALL_EXTERNAL_REQUESTS) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Check if the data is already in sessionStorage\n\t\tconst cachedContribs = sessionStorage.getItem(\"ghContribs\");\n\t\tif (cachedContribs) {\n\t\t\tghContribs = JSON.parse(cachedContribs);\n\t\t\treturn;\n\t\t}\n\n\t\t// Fetch GitHub contributors\n\t\ttry {\n\t\t\tconst response = await fetch(`${GITHUB_API_URL}/contributors`);\n\t\t\tif (!response.ok) {\n\t\t\t\tToastManager.add({\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t\tmessage: m[\"about.errors.github_contributors\"](),\n\t\t\t\t});\n\t\t\t\tthrow new Error(`HTTP error, status: ${response.status}`);\n\t\t\t}\n\t\t\tconst allContribs = await response.json();\n\n\t\t\t// Filter out main and notable contributors\n\t\t\tconst excludedNames = new Set([\n\t\t\t\t...mainContribs.map((c) => c.github.split(\"/\").pop()),\n\t\t\t\t...notableContribs.map((c) => c.github.split(\"/\").pop()),\n\t\t\t\t\"Z2r-YT\",\n\t\t\t]);\n\n\t\t\tconst filteredContribs = allContribs.filter(\n\t\t\t\t(contrib: { login: string }) =>\n\t\t\t\t\t!excludedNames.has(contrib.login),\n\t\t\t);\n\n\t\t\t// Fetch and cache avatar images as Base64\n\t\t\tconst fetchAvatar = async (url: string) => {\n\t\t\t\tconst res = await fetch(url);\n\t\t\t\tconst blob = await res.blob();\n\t\t\t\treturn new Promise<string>((resolve, reject) => {\n\t\t\t\t\tconst reader = new FileReader();\n\t\t\t\t\treader.onloadend = () => resolve(reader.result as string);\n\t\t\t\t\treader.onerror = reject;\n\t\t\t\t\treader.readAsDataURL(blob);\n\t\t\t\t});\n\t\t\t};\n\n\t\t\tghContribs = await Promise.all(\n\t\t\t\tfilteredContribs.map(\n\t\t\t\t\tasync (contrib: {\n\t\t\t\t\t\tlogin: string;\n\t\t\t\t\t\tavatar_url: string;\n\t\t\t\t\t\thtml_url: string;\n\t\t\t\t\t}) => ({\n\t\t\t\t\t\tname: contrib.login,\n\t\t\t\t\t\tavatar: await fetchAvatar(contrib.avatar_url),\n\t\t\t\t\t\tgithub: contrib.html_url,\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t);\n\n\t\t\t// Cache the data in sessionStorage\n\t\t\tsessionStorage.setItem(\"ghContribs\", JSON.stringify(ghContribs));\n\t\t} catch (e) {\n\t\t\terror([\"general\"], `Error fetching GitHub contributors: ${e}`);\n\t\t}\n\t});\n\n\tconst donationsEnabled = PUB_STRIPE_KEY\n\t\t&& PUB_DONATION_URL\n\t\t&& !DISABLE_ALL_EXTERNAL_REQUESTS;\n</script>\n\n<div class=\"flex flex-col h-full items-center\">\n\t<h1 class=\"hidden md:block text-[40px] tracking-tight leading-[72px] mb-6\">\n\t\t<InfoIcon size=\"40\" class=\"inline-block -mt-2 mr-2\" />\n\t\t{m[\"about.title\"]()}\n\t</h1>\n\n\t<div\n\t\tclass=\"w-full max-w-[1280px] flex flex-col md:flex-row gap-4 p-4 md:px-4 md:py-0\"\n\t>\n\t\t<!-- Why VERT? & Credits -->\n\t\t<div class=\"flex flex-col gap-4 flex-1\">\n\t\t\t{#if donationsEnabled}\n\t\t\t\t<About.Donate />\n\t\t\t{/if}\n\t\t\t<About.Why />\n\t\t\t<About.Sponsors />\n\t\t</div>\n\n\t\t<!-- Resources & Donate to VERT -->\n\t\t<div class=\"flex flex-col gap-4 flex-1\">\n\t\t\t<About.Resources />\n\t\t\t<About.Credits {mainContribs} {notableContribs} {ghContribs} />\n\t\t</div>\n\t</div>\n</div>\n"
  },
  {
    "path": "src/routes/convert/+page.svelte",
    "content": "<script lang=\"ts\">\n\timport ConversionPanel from \"$lib/components/functional/ConversionPanel.svelte\";\n\timport FormatDropdown from \"$lib/components/functional/FormatDropdown.svelte\";\n\timport Uploader from \"$lib/components/functional/Uploader.svelte\";\n\timport Panel from \"$lib/components/visual/Panel.svelte\";\n\timport ProgressBar from \"$lib/components/visual/ProgressBar.svelte\";\n\timport Tooltip from \"$lib/components/visual/Tooltip.svelte\";\n\timport { categories, converters } from \"$lib/converters\";\n\timport {\n\t\teffects,\n\t\tfiles,\n\t\tgradientColor,\n\t\tshowGradient,\n\t\tvertdLoaded,\n\t\tdropdownStates,\n\t} from \"$lib/store/index.svelte\";\n\timport { VertFile } from \"$lib/types\";\n\timport {\n\t\tAudioLines,\n\t\tBookText,\n\t\tDownloadIcon,\n\t\tFileMusicIcon,\n\t\tFileQuestionIcon,\n\t\tFileVideo2,\n\t\tFilmIcon,\n\t\tImageIcon,\n\t\tImageOffIcon,\n\t\tRotateCwIcon,\n\t\tXIcon,\n\t} from \"lucide-svelte\";\n\timport { m } from \"$lib/paraglide/messages\";\n\timport { Settings } from \"$lib/sections/settings/index.svelte\";\n\timport { MAX_ARRAY_BUFFER_SIZE } from \"$lib/store/index.svelte\";\n\timport { GB } from \"$lib/util/consts\";\n\timport { log } from \"$lib/util/logger\";\n\n\tlet processedFileIds = $state(new Set<string>());\n\n\t$effect(() => {\n\t\tif (!Settings.instance.settings || files.files.length === 0) return;\n\n\t\tfiles.files.forEach((file) => {\n\t\t\tconst settings = Settings.instance.settings;\n\t\t\tif (processedFileIds.has(file.id)) return;\n\n\t\t\tconst converter = file.findConverter();\n\t\t\tif (!converter) return;\n\n\t\t\tlet category: string | undefined;\n\t\t\tconst isImage = converter.name === \"imagemagick\";\n\t\t\tconst isAudio = converter.name === \"ffmpeg\";\n\t\t\tconst isVideo = converter.name === \"vertd\";\n\t\t\tconst isDocument = converter.name === \"pandoc\";\n\n\t\t\tif (isImage) category = \"image\";\n\t\t\telse if (isAudio) category = \"audio\";\n\t\t\telse if (isVideo) category = \"video\";\n\t\t\telse if (isDocument) category = \"doc\";\n\t\t\tif (!category) return;\n\n\t\t\tlet targetFormat: string | undefined;\n\n\t\t\t// restore saved format (if navigated back to page for example)\n\t\t\tconst savedFormat = $dropdownStates[file.name];\n\t\t\tif (\n\t\t\t\tsavedFormat &&\n\t\t\t\tsavedFormat !== file.from &&\n\t\t\t\tcategories[category]?.formats.includes(savedFormat)\n\t\t\t) {\n\t\t\t\ttargetFormat = savedFormat;\n\t\t\t} else if (settings.useDefaultFormat) {\n\t\t\t\t// else use default format if enabled\n\t\t\t\tlet defaultFormat: string | undefined;\n\t\t\t\tconst df = settings.defaultFormat;\n\t\t\t\tif (category === \"image\") defaultFormat = df.image;\n\t\t\t\telse if (category === \"audio\") defaultFormat = df.audio;\n\t\t\t\telse if (category === \"video\") defaultFormat = df.video;\n\t\t\t\telse if (category === \"doc\") defaultFormat = df.document;\n\n\t\t\t\tif (\n\t\t\t\t\tdefaultFormat &&\n\t\t\t\t\tdefaultFormat !== file.from &&\n\t\t\t\t\tcategories[category]?.formats.includes(defaultFormat)\n\t\t\t\t) {\n\t\t\t\t\ttargetFormat = defaultFormat;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// or use first available format (or if default format is same as input)\n\t\t\tif (!targetFormat) {\n\t\t\t\tconst firstDiff = categories[category]?.formats.find(\n\t\t\t\t\t(f) => f !== file.from,\n\t\t\t\t);\n\t\t\t\ttargetFormat =\n\t\t\t\t\tfirstDiff || categories[category]?.formats[0] || \"\";\n\t\t\t}\n\n\t\t\tfile.to = targetFormat;\n\t\t\tprocessedFileIds.add(file.id);\n\t\t});\n\t});\n\n\tconst handleSelect = (option: string, file: VertFile) => {\n\t\tfile.result = null;\n\t};\n\n\t$effect(() => {\n\t\t// Set gradient color depending on the file types\n\t\tlet type = \"\";\n\t\tif (files.files.length) {\n\t\t\tconst converters = files.files.map(\n\t\t\t\t(file) => file.findConverter()?.name,\n\t\t\t);\n\t\t\tconst uniqueTypes = new Set(converters);\n\n\t\t\tif (uniqueTypes.size === 1) {\n\t\t\t\tconst onlyType = converters[0];\n\t\t\t\tif (onlyType === \"imagemagick\") type = \"blue\";\n\t\t\t\telse if (onlyType === \"ffmpeg\") type = \"purple\";\n\t\t\t\telse if (onlyType === \"vertd\") type = \"red\";\n\t\t\t\telse if (onlyType === \"pandoc\") type = \"green\";\n\t\t\t}\n\t\t}\n\n\t\tif (files.files.length === 0 || !type) {\n\t\t\tshowGradient.set(false);\n\t\t} else showGradient.set(true);\n\n\t\tgradientColor.set(type);\n\t});\n</script>\n\n{#snippet fileItem(file: VertFile, index: number)}\n\t{@const currentConverter = file.findConverter()}\n\t{@const isImage = currentConverter?.name === \"imagemagick\"}\n\t{@const isAudio = currentConverter?.name === \"ffmpeg\"}\n\t{@const isVideo = currentConverter?.name === \"vertd\"}\n\t{@const isDocument = currentConverter?.name === \"pandoc\"}\n\t<Panel class=\"p-5 flex flex-col min-w-0 gap-4 relative\">\n\t\t<div class=\"flex-shrink-0 h-8 w-full flex items-center gap-2\">\n\t\t\t{#if !converters.length}\n\t\t\t\t<Tooltip\n\t\t\t\t\ttext={m[\"convert.tooltips.unknown_file\"]()}\n\t\t\t\t\tposition=\"bottom\"\n\t\t\t\t>\n\t\t\t\t\t<FileQuestionIcon size=\"24\" class=\"flex-shrink-0\" />\n\t\t\t\t</Tooltip>\n\t\t\t{:else if isAudio}\n\t\t\t\t<Tooltip\n\t\t\t\t\ttext={m[\"convert.tooltips.audio_file\"]()}\n\t\t\t\t\tposition=\"bottom\"\n\t\t\t\t>\n\t\t\t\t\t<AudioLines size=\"24\" class=\"flex-shrink-0\" />\n\t\t\t\t</Tooltip>\n\t\t\t{:else if isVideo}\n\t\t\t\t<Tooltip\n\t\t\t\t\ttext={m[\"convert.tooltips.video_file\"]()}\n\t\t\t\t\tposition=\"bottom\"\n\t\t\t\t>\n\t\t\t\t\t<FilmIcon size=\"24\" class=\"flex-shrink-0\" />\n\t\t\t\t</Tooltip>\n\t\t\t{:else if isDocument}\n\t\t\t\t<Tooltip\n\t\t\t\t\ttext={m[\"convert.tooltips.document_file\"]()}\n\t\t\t\t\tposition=\"bottom\"\n\t\t\t\t>\n\t\t\t\t\t<BookText size=\"24\" class=\"flex-shrink-0\" />\n\t\t\t\t</Tooltip>\n\t\t\t{:else}\n\t\t\t\t<Tooltip\n\t\t\t\t\ttext={m[\"convert.tooltips.image_file\"]()}\n\t\t\t\t\tposition=\"bottom\"\n\t\t\t\t>\n\t\t\t\t\t<ImageIcon size=\"24\" class=\"flex-shrink-0\" />\n\t\t\t\t</Tooltip>\n\t\t\t{/if}\n\t\t\t<div class=\"flex-grow overflow-hidden\">\n\t\t\t\t{#if file.processing}\n\t\t\t\t\t<ProgressBar\n\t\t\t\t\t\tmin={0}\n\t\t\t\t\t\tmax={100}\n\t\t\t\t\t\tprogress={currentConverter?.reportsProgress || file.isZip()\n\t\t\t\t\t\t\t? file.progress\n\t\t\t\t\t\t\t: null}\n\t\t\t\t\t/>\n\t\t\t\t{:else}\n\t\t\t\t\t<h2\n\t\t\t\t\t\tclass=\"text-xl font-body overflow-hidden text-ellipsis whitespace-nowrap\"\n\t\t\t\t\t\ttitle={file.name}\n\t\t\t\t\t>\n\t\t\t\t\t\t{file.name}\n\t\t\t\t\t</h2>\n\t\t\t\t{/if}\n\t\t\t</div>\n\t\t\t<button\n\t\t\t\tclass=\"flex-shrink-0 w-8 rounded-full hover:bg-panel-alt h-full flex items-center justify-center\"\n\t\t\t\tonclick={async () => {\n\t\t\t\t\tawait file.cancel();\n\t\t\t\t\tfiles.files = files.files.filter((_, i) => i !== index);\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<XIcon size=\"24\" class=\"text-muted\" />\n\t\t\t</button>\n\t\t</div>\n\t\t{#if !currentConverter}\n\t\t\t{#if file.name.startsWith(\"vertd\")}\n\t\t\t\t<div\n\t\t\t\t\tclass=\"h-full flex flex-col text-center justify-center text-failure\"\n\t\t\t\t>\n\t\t\t\t\t<p class=\"font-body font-bold\">\n\t\t\t\t\t\t{m[\"convert.errors.cant_convert\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t\t<p class=\"font-normal\">\n\t\t\t\t\t\t{m[\"convert.errors.vertd_server\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t{:else}\n\t\t\t\t<div\n\t\t\t\t\tclass=\"h-full flex flex-col text-center justify-center text-failure\"\n\t\t\t\t>\n\t\t\t\t\t<p class=\"font-body font-bold\">\n\t\t\t\t\t\t{m[\"convert.errors.cant_convert\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t\t<p class=\"font-normal\">\n\t\t\t\t\t\t{m[\"convert.errors.unsupported_format\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t{/if}\n\t\t{:else}\n\t\t\t{@const formatInfo = currentConverter.supportedFormats.find(\n\t\t\t\t(f) => f.name === file.from,\n\t\t\t)}\n\t\t\t{@const isLarge = file.isLarge()}\n\t\t\t{#if formatInfo && !formatInfo.fromSupported}\n\t\t\t\t<div\n\t\t\t\t\tclass=\"h-full flex flex-col text-center justify-center text-failure\"\n\t\t\t\t>\n\t\t\t\t\t<p class=\"font-body font-bold\">\n\t\t\t\t\t\t{m[\"convert.errors.cant_convert\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t\t<p class=\"font-normal\">\n\t\t\t\t\t\t{m[\"convert.errors.format_output_only\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t{:else if isLarge && !file.supportsStreaming()}\n\t\t\t\t<div\n\t\t\t\t\tclass=\"h-full flex flex-col text-center justify-center text-failure\"\n\t\t\t\t>\n\t\t\t\t\t<p class=\"font-body font-bold\">\n\t\t\t\t\t\t{m[\"convert.errors.cant_convert\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t\t<p class=\"font-normal\">\n\t\t\t\t\t\t{m[\"workers.errors.file_too_large\"]({\n\t\t\t\t\t\t\tlimit: (MAX_ARRAY_BUFFER_SIZE / GB).toFixed(2),\n\t\t\t\t\t\t})}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t{:else if currentConverter.status === \"downloading\"}\n\t\t\t\t<div\n\t\t\t\t\tclass=\"h-full flex flex-col text-center justify-center text-failure\"\n\t\t\t\t>\n\t\t\t\t\t<p class=\"font-body font-bold\">\n\t\t\t\t\t\t{m[\"convert.errors.cant_convert\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t\t<p class=\"font-normal\">\n\t\t\t\t\t\t{m[\"convert.errors.worker_downloading\"]({\n\t\t\t\t\t\t\ttype: isAudio\n\t\t\t\t\t\t\t\t? m[\"convert.errors.audio\"]()\n\t\t\t\t\t\t\t\t: isVideo\n\t\t\t\t\t\t\t\t\t? \"Video\"\n\t\t\t\t\t\t\t\t\t: isDocument\n\t\t\t\t\t\t\t\t\t\t? m[\"convert.errors.doc\"]()\n\t\t\t\t\t\t\t\t\t\t: m[\"convert.errors.image\"](),\n\t\t\t\t\t\t})}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t{:else if currentConverter.status === \"error\"}\n\t\t\t\t<div\n\t\t\t\t\tclass=\"h-full flex flex-col text-center justify-center text-failure\"\n\t\t\t\t>\n\t\t\t\t\t<p class=\"font-body font-bold\">\n\t\t\t\t\t\t{m[\"convert.errors.cant_convert\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t\t<p class=\"font-normal\">\n\t\t\t\t\t\t{m[\"convert.errors.worker_error\"]({\n\t\t\t\t\t\t\ttype: isAudio\n\t\t\t\t\t\t\t\t? m[\"convert.errors.audio\"]()\n\t\t\t\t\t\t\t\t: isVideo\n\t\t\t\t\t\t\t\t\t? \"Video\"\n\t\t\t\t\t\t\t\t\t: isDocument\n\t\t\t\t\t\t\t\t\t\t? m[\"convert.errors.doc\"]()\n\t\t\t\t\t\t\t\t\t\t: m[\"convert.errors.image\"](),\n\t\t\t\t\t\t})}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t{:else if currentConverter.status === \"not-ready\"}\n\t\t\t\t<div\n\t\t\t\t\tclass=\"h-full flex flex-col text-center justify-center text-failure\"\n\t\t\t\t>\n\t\t\t\t\t<p class=\"font-body font-bold\">\n\t\t\t\t\t\t{m[\"convert.errors.cant_convert\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t\t<p class=\"font-normal\">\n\t\t\t\t\t\t{m[\"convert.errors.worker_timeout\"]({\n\t\t\t\t\t\t\ttype: isAudio\n\t\t\t\t\t\t\t\t? m[\"convert.errors.audio\"]()\n\t\t\t\t\t\t\t\t: isVideo\n\t\t\t\t\t\t\t\t\t? \"Video\"\n\t\t\t\t\t\t\t\t\t: isDocument\n\t\t\t\t\t\t\t\t\t\t? m[\"convert.errors.doc\"]()\n\t\t\t\t\t\t\t\t\t\t: m[\"convert.errors.image\"](),\n\t\t\t\t\t\t})}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t{:else if isVideo && !$vertdLoaded && !isAudio && !isImage && !isDocument}\n\t\t\t\t<div\n\t\t\t\t\tclass=\"h-full flex flex-col text-center justify-center text-failure\"\n\t\t\t\t>\n\t\t\t\t\t<p class=\"font-body font-bold\">\n\t\t\t\t\t\t{m[\"convert.errors.cant_convert\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t\t<p class=\"font-normal\">\n\t\t\t\t\t\t{m[\"convert.errors.vertd_not_found\"]()}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t{:else}\n\t\t\t\t<div class=\"flex flex-row justify-between\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"flex gap-4 w-full h-[152px] overflow-hidden relative\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<div class=\"w-1/2 h-full overflow-hidden rounded-xl\">\n\t\t\t\t\t\t\t{#if file.blobUrl}\n\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\tclass=\"object-cover w-full h-full\"\n\t\t\t\t\t\t\t\t\tsrc={file.blobUrl}\n\t\t\t\t\t\t\t\t\talt={file.name}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\tclass=\"w-full h-full flex items-center justify-center text-black\"\n\t\t\t\t\t\t\t\t\tstyle=\"background: var({isAudio\n\t\t\t\t\t\t\t\t\t\t? '--bg-gradient-purple-alt'\n\t\t\t\t\t\t\t\t\t\t: isVideo\n\t\t\t\t\t\t\t\t\t\t\t? '--bg-gradient-red-alt'\n\t\t\t\t\t\t\t\t\t\t\t: isDocument\n\t\t\t\t\t\t\t\t\t\t\t\t? '--bg-gradient-green-alt'\n\t\t\t\t\t\t\t\t\t\t\t\t: '--bg-gradient-blue-alt'})\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{#if isAudio}\n\t\t\t\t\t\t\t\t\t\t<FileMusicIcon size=\"56\" />\n\t\t\t\t\t\t\t\t\t{:else if isVideo}\n\t\t\t\t\t\t\t\t\t\t<FileVideo2 size=\"56\" />\n\t\t\t\t\t\t\t\t\t{:else if isDocument}\n\t\t\t\t\t\t\t\t\t\t<BookText size=\"56\" />\n\t\t\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t\t\t<ImageOffIcon size=\"56\" />\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"absolute top-16 right-0 mr-4 pl-2 h-[calc(100%-83px)] w-[calc(50%-38px)] pr-4 pb-1 flex items-center justify-center aspect-square\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclass=\"w-[122px] h-fit flex flex-col gap-2 items-center justify-center\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<FormatDropdown\n\t\t\t\t\t\t\t\t{categories}\n\t\t\t\t\t\t\t\tfrom={file.from}\n\t\t\t\t\t\t\t\tbind:selected={file.to}\n\t\t\t\t\t\t\t\tonselect={(option) =>\n\t\t\t\t\t\t\t\t\thandleSelect(option, file)}\n\t\t\t\t\t\t\t\t{file}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tclass=\"w-full flex items-center justify-between\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<Tooltip\n\t\t\t\t\t\t\t\t\ttext={m[\"convert.tooltips.convert_file\"]()}\n\t\t\t\t\t\t\t\t\tposition=\"bottom\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\tclass=\"btn {$effects\n\t\t\t\t\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t\t\t\t\t: '!scale-100'} p-0 w-14 h-14 text-black {isAudio\n\t\t\t\t\t\t\t\t\t\t\t? 'bg-accent-purple'\n\t\t\t\t\t\t\t\t\t\t\t: isVideo\n\t\t\t\t\t\t\t\t\t\t\t\t? 'bg-accent-red'\n\t\t\t\t\t\t\t\t\t\t\t\t: isDocument\n\t\t\t\t\t\t\t\t\t\t\t\t\t? 'bg-accent-green'\n\t\t\t\t\t\t\t\t\t\t\t\t\t: 'bg-accent-blue'}\"\n\t\t\t\t\t\t\t\t\t\tdisabled={!files.ready}\n\t\t\t\t\t\t\t\t\t\tonclick={() => file.convert()}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<RotateCwIcon size=\"24\" />\n\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t\t\t<Tooltip\n\t\t\t\t\t\t\t\t\ttext={m[\"convert.tooltips.download_file\"]()}\n\t\t\t\t\t\t\t\t\tposition=\"bottom\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\tclass=\"btn {$effects\n\t\t\t\t\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t\t\t\t\t: '!scale-100'} p-0 w-14 h-14\"\n\t\t\t\t\t\t\t\t\t\tonclick={file.download}\n\t\t\t\t\t\t\t\t\t\tdisabled={!file.result}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<DownloadIcon size=\"24\" />\n\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t{/if}\n\t\t{/if}\n\t</Panel>\n{/snippet}\n\n<div class=\"flex flex-col justify-center items-center gap-8 -mt-4 px-4 md:p-0\">\n\t<div class=\"max-w-[778px] w-full\">\n\t\t<ConversionPanel />\n\t</div>\n\n\t<div\n\t\tclass=\"w-full max-w-[778px] grid grid-cols-1 md:grid-cols-2 auto-rows-[240px] gap-4 md:p-0\"\n\t>\n\t\t{#each files.files as file, i (file.id)}\n\t\t\t{#if files.files.length >= 2 && i === 1}\n\t\t\t\t<Uploader\n\t\t\t\t\tclass=\"w-full h-full col-start-1 row-start-1 md:col-start-2\"\n\t\t\t\t/>\n\t\t\t{/if}\n\t\t\t{@render fileItem(file, i)}\n\t\t\t{#if files.files.length < 2}\n\t\t\t\t<Uploader class=\"w-full h-full\" />\n\t\t\t{/if}\n\t\t{/each}\n\t\t{#if files.files.length === 0}\n\t\t\t<Uploader class=\"w-full h-full col-span-2\" />\n\t\t{/if}\n\t</div>\n</div>\n"
  },
  {
    "path": "src/routes/privacy/+page.svelte",
    "content": "<script lang=\"ts\">\n\timport { m } from \"$lib/paraglide/messages\";\n\timport { link, sanitize } from \"$lib/store/index.svelte\";\n\timport { ShieldCheckIcon } from \"lucide-svelte\";\n</script>\n\n<div class=\"flex flex-col h-full items-center\">\n\t<h1 class=\"hidden md:block text-[40px] tracking-tight leading-[72px] mb-6\">\n\t\t<ShieldCheckIcon size=\"40\" class=\"inline-block -mt-2 mr-2\" />\n\t\t{m[\"privacy.title\"]()}\n\t</h1>\n\n\t<div\n\t\tclass=\"w-full max-w-[1280px] flex flex-col md:flex-row gap-4 p-4 md:px-4 md:py-0\"\n\t>\n\t\t<div class=\"bg-panel rounded-2xl p-6 shadow-panel text-lg font-normal\">\n\t\t\t<h2 class=\"text-2xl mb-3\">{m[\"privacy.summary.title\"]()}</h2>\n\t\t\t<p class=\"mb-4\">\n\t\t\t\t{@html sanitize(\n\t\t\t\t\tlink(\n\t\t\t\t\t\t[\"vert_link\"],\n\t\t\t\t\t\tm[\"privacy.summary.description\"](),\n\t\t\t\t\t\t[\"https://vert.sh\"],\n\t\t\t\t\t\t[true],\n\t\t\t\t\t),\n\t\t\t\t)}\n\t\t\t</p>\n\n\t\t\t<h2 class=\"text-2xl mb-3\">{m[\"privacy.conversions.title\"]()}</h2>\n\t\t\t<p class=\"mb-4\">\n\t\t\t\t{@html sanitize(m[\"privacy.conversions.description\"]())}\n\t\t\t</p>\n\n\t\t\t<h2 class=\"text-2xl mb-3\">{m[\"privacy.donations.title\"]()}</h2>\n\t\t\t<p class=\"mb-4\">\n\t\t\t\t{@html sanitize(\n\t\t\t\t\tlink(\n\t\t\t\t\t\t[\"about_link\", \"stripe_link\"],\n\t\t\t\t\t\tm[\"privacy.donations.description\"](),\n\t\t\t\t\t\t[\"/about\", \"https://stripe.com/docs/disputes/prevention/advanced-fraud-detection\"],\n\t\t\t\t\t\t[false, true],\n\t\t\t\t\t),\n\t\t\t\t)}\n\t\t\t</p>\n\n\t\t\t<h2 class=\"text-2xl mb-3\">\n\t\t\t\t{m[\"privacy.conversion_errors.title\"]()}\n\t\t\t</h2>\n\t\t\t<div class=\"mb-4\">\n\t\t\t\t{m[\"privacy.conversion_errors.description\"]()}\n\t\t\t\t<ul class=\"list-disc list-inside mt-2 mb-2\">\n\t\t\t\t\t<li>{m[\"privacy.conversion_errors.list_job_id\"]()}</li>\n\t\t\t\t\t<li>{m[\"privacy.conversion_errors.list_format_from\"]()}</li>\n\t\t\t\t\t<li>{m[\"privacy.conversion_errors.list_format_to\"]()}</li>\n\t\t\t\t\t<li>{m[\"privacy.conversion_errors.list_stderr\"]()}</li>\n\t\t\t\t\t<li>{m[\"privacy.conversion_errors.list_video\"]()}</li>\n\t\t\t\t</ul>\n\t\t\t\t{m[\"privacy.conversion_errors.footer\"]()}\n\t\t\t</div>\n\n\t\t\t<h3 class=\"text-xl mt-4 mb-2\">{m[\"privacy.analytics.title\"]()}</h3>\n\t\t\t<p class=\"mb-4\">\n\t\t\t\t{@html sanitize(\n\t\t\t\t\tlink(\n\t\t\t\t\t\t[\"settings_link\", \"plausible_link\"],\n\t\t\t\t\t\tm[\"privacy.analytics.description\"](),\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\"/settings\",\n\t\t\t\t\t\t\t\"https://plausible.io/privacy-focused-web-analytics\",\n\t\t\t\t\t\t],\n\t\t\t\t\t\t[false, true],\n\t\t\t\t\t),\n\t\t\t\t)}\n\t\t\t</p>\n\n\t\t\t<h3 class=\"text-xl mt-4 mb-2\">\n\t\t\t\t{m[\"privacy.local_storage.title\"]()}\n\t\t\t</h3>\n\t\t\t<p class=\"mb-4\">\n\t\t\t\t{@html sanitize(\n\t\t\t\t\tlink(\n\t\t\t\t\t\t[\"settings_link\"],\n\t\t\t\t\t\tm[\"privacy.local_storage.description\"](),\n\t\t\t\t\t\t[\"/settings\"],\n\t\t\t\t\t\t[false],\n\t\t\t\t\t),\n\t\t\t\t)}\n\t\t\t</p>\n\n\t\t\t<h3 class=\"text-xl mt-4 mb-2\">{m[\"privacy.contact.title\"]()}</h3>\n\t\t\t<p class=\"mb-0\">\n\t\t\t\t{@html sanitize(\n\t\t\t\t\tlink(\n\t\t\t\t\t\t[\"email_link\"],\n\t\t\t\t\t\tm[\"privacy.contact.description\"](),\n\t\t\t\t\t\t[\"mailto:hello@vert.sh\"],\n\t\t\t\t\t\t[false],\n\t\t\t\t\t),\n\t\t\t\t)}\n\t\t\t</p>\n\n\t\t\t<p class=\"text-sm text-muted mt-6\">{m[\"privacy.last_updated\"]()}</p>\n\t\t</div>\n\t</div>\n</div>\n"
  },
  {
    "path": "src/routes/settings/+page.svelte",
    "content": "<script lang=\"ts\">\n\timport { browser } from \"$app/environment\";\n\timport { log } from \"$lib/util/logger\";\n\timport * as Settings from \"$lib/sections/settings/index.svelte\";\n\timport { PUB_PLAUSIBLE_URL } from \"$env/static/public\";\n\timport { SettingsIcon } from \"lucide-svelte\";\n\timport { onMount } from \"svelte\";\n\timport { m } from \"$lib/paraglide/messages\";\n\timport { ToastManager } from \"$lib/util/toast.svelte\";\n\timport { DISABLE_ALL_EXTERNAL_REQUESTS } from \"$lib/util/consts\";\n\n\tlet settings = $state(Settings.Settings.instance.settings);\n\n\tlet isInitial = $state(true);\n\n\t$effect(() => {\n\t\tif (!browser) return;\n\t\tif (isInitial) {\n\t\t\tisInitial = false;\n\t\t\treturn;\n\t\t}\n\n\t\tconst savedSettings = localStorage.getItem(\"settings\");\n\t\tif (savedSettings) {\n\t\t\tconst parsedSettings = JSON.parse(savedSettings);\n\t\t\tif (JSON.stringify(parsedSettings) === JSON.stringify(settings))\n\t\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tSettings.Settings.instance.settings = settings;\n\t\t\tSettings.Settings.instance.save();\n\t\t\tlog([\"settings\"], \"saving settings\");\n\t\t} catch (error) {\n\t\t\tlog([\"settings\", \"error\"], `failed to save settings: ${error}`);\n\t\t\tToastManager.add({\n\t\t\t\ttype: \"error\",\n\t\t\t\tmessage: m[\"settings.errors.save_failed\"](),\n\t\t\t});\n\t\t}\n\t});\n\n\tonMount(() => {\n\t\tconst savedSettings = localStorage.getItem(\"settings\");\n\t\tif (savedSettings) {\n\t\t\tconst parsedSettings = JSON.parse(savedSettings);\n\t\t\tSettings.Settings.instance.settings = {\n\t\t\t\t...Settings.Settings.instance.settings,\n\t\t\t\t...parsedSettings,\n\t\t\t};\n\t\t\tsettings = Settings.Settings.instance.settings;\n\t\t}\n\t});\n</script>\n\n<div class=\"flex flex-col h-full items-center\">\n\t<h1 class=\"hidden md:block text-[40px] tracking-tight leading-[72px] mb-6\">\n\t\t<SettingsIcon size=\"40\" class=\"inline-block -mt-2 mr-2\" />\n\t\t{m[\"settings.title\"]()}\n\t</h1>\n\n\t<div\n\t\tclass=\"w-full max-w-[1280px] flex flex-col md:flex-row gap-4 p-4 md:px-4 md:py-0\"\n\t>\n\t\t<div class=\"flex flex-col gap-4 flex-1\">\n\t\t\t<Settings.Conversion bind:settings />\n\t\t\t{#if !DISABLE_ALL_EXTERNAL_REQUESTS}\n\t\t\t\t<Settings.Vertd bind:settings />\n\t\t\t{:else if PUB_PLAUSIBLE_URL}\n\t\t\t\t<Settings.Privacy bind:settings />\n\t\t\t{/if}\n\t\t</div>\n\n\t\t<div class=\"flex flex-col gap-4 flex-1\">\n\t\t\t<Settings.Appearance />\n\t\t\t{#if PUB_PLAUSIBLE_URL && !DISABLE_ALL_EXTERNAL_REQUESTS}\n\t\t\t\t<Settings.Privacy bind:settings />\n\t\t\t{/if}\n\t\t</div>\n\t</div>\n</div>\n"
  },
  {
    "path": "static/manifest.json",
    "content": "{\n\t\"name\": \"VERT\",\n\t\"short_name\": \"VERT\",\n\t\"description\": \"The file converter you'll love\",\n\t\"start_url\": \"/\",\n\t\"display\": \"standalone\",\n\t\"background_color\": \"#ffffff\",\n\t\"theme_color\": \"#F2ABEE\",\n\t\"icons\": [\n\t\t{\n\t\t\t\"src\": \"lettermark.jpg\",\n\t\t\t\"sizes\": \"192x192\",\n\t\t\t\"type\": \"image/jpeg\"\n\t\t},\n\t\t{\n\t\t\t\"src\": \"lettermark.jpg\",\n\t\t\t\"sizes\": \"512x512\",\n\t\t\t\"type\": \"image/jpeg\"\n\t\t},\n\t\t{\n\t\t\t\"src\": \"lettermark_maskable.png\",\n\t\t\t\"sizes\": \"192x192\",\n\t\t\t\"type\": \"image/png\",\n\t\t\t\"purpose\": \"maskable\"\n\t\t},\n\t\t{\n\t\t\t\"src\": \"lettermark_maskable.png\",\n\t\t\t\"sizes\": \"512x512\",\n\t\t\t\"type\": \"image/png\",\n\t\t\t\"purpose\": \"maskable\"\n\t\t}\n\t]\n}"
  },
  {
    "path": "static/robots.txt",
    "content": "User-agent: *\nAllow: /\n\nSitemap: https://vert.sh/sitemap.xml\n"
  },
  {
    "path": "static/sitemap.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset\n  xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\n  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n  xsi:schemaLocation=\"http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd\">\n\n  <url>\n    <loc>https://vert.sh/</loc>\n    <lastmod>2025-10-17T19:23:05+00:00</lastmod>\n    <priority>1.00</priority>\n  </url>\n  <url>\n    <loc>https://vert.sh/convert/</loc>\n    <lastmod>2025-10-17T19:23:05+00:00</lastmod>\n    <priority>0.80</priority>\n  </url>\n  <url>\n    <loc>https://vert.sh/settings/</loc>\n    <lastmod>2025-10-17T19:23:05+00:00</lastmod>\n    <priority>0.80</priority>\n  </url>\n  <url>\n    <loc>https://vert.sh/about/</loc>\n    <lastmod>2025-10-17T19:23:05+00:00</lastmod>\n    <priority>0.80</priority>\n  </url>\n  <url>\n    <loc>https://vert.sh/privacy/</loc>\n    <lastmod>2025-10-17T19:23:05+00:00</lastmod>\n    <priority>0.80</priority>\n  </url>\n</urlset>"
  },
  {
    "path": "static/sw.js",
    "content": "const CACHE_NAME = \"vert-wasm-cache-v2\"; // updated when workers update\n\nconst WASM_FILES = [\n\t\"/pandoc.wasm\",\n\t\"https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/esm/ffmpeg-core.js\",\n\t\"https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/esm/ffmpeg-core.wasm\",\n];\n\nconst WASM_URL_PATTERNS = [\n\t/\\/src\\/lib\\/workers\\/.*\\.js$/, // dev mode worker files\n\t/\\/assets\\/.*worker.*\\.js$/, // prod worker files\n\t/magick.*\\.wasm$/, // magick-wasm (unneeded?)\n];\n\nfunction shouldCacheUrl(url) {\n\tconst urlObj = new URL(url);\n\n\tif (WASM_FILES.includes(urlObj.pathname) || WASM_FILES.includes(url)) {\n\t\treturn true;\n\t}\n\n\treturn WASM_URL_PATTERNS.some(\n\t\t(pattern) => pattern.test(urlObj.pathname) || pattern.test(url),\n\t);\n}\n\nself.addEventListener(\"install\", (event) => {\n\tconsole.log(\"[SW] installing service worker\");\n\n\tevent.waitUntil(\n\t\tcaches.open(CACHE_NAME).then((cache) => {\n\t\t\tconst staticFiles = WASM_FILES.filter((file) =>\n\t\t\t\tfile.startsWith(\"/\"),\n\t\t\t);\n\t\t\tif (staticFiles.length > 0) {\n\t\t\t\tconsole.log(\"[SW] pre-caching static files:\", staticFiles);\n\t\t\t\treturn cache.addAll(staticFiles).catch((err) => {\n\t\t\t\t\tconsole.warn(\"[SW] failed to pre-cache some files:\", err);\n\t\t\t\t});\n\t\t\t}\n\t\t}),\n\t);\n\n\tself.skipWaiting();\n});\n\nself.addEventListener(\"activate\", (event) => {\n\tevent.waitUntil(\n\t\tcaches\n\t\t\t.keys()\n\t\t\t.then((cacheNames) => {\n\t\t\t\treturn Promise.all(\n\t\t\t\t\tcacheNames.map((cacheName) => {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tcacheName !== CACHE_NAME &&\n\t\t\t\t\t\t\tcacheName.startsWith(\"vert-wasm-cache\")\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tconsole.log(\"[SW] deleting old cache:\", cacheName);\n\t\t\t\t\t\t\treturn caches.delete(cacheName);\n\t\t\t\t\t\t}\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t})\n\t\t\t.then(() => {\n\t\t\t\treturn self.clients.claim();\n\t\t\t}),\n\t);\n});\n\nself.addEventListener(\"fetch\", (event) => {\n\tconst request = event.request;\n\n\tif (!shouldCacheUrl(request.url)) {\n\t\treturn; // Let the request go through normally if not a target URL\n\t}\n\n    // else intercept request\n\tevent.respondWith(\n\t\tcaches.match(request).then((cachedResponse) => {\n\t\t\tif (cachedResponse) {\n\t\t\t\tconsole.log(\"[SW] serving from cache:\", request.url);\n\t\t\t\treturn cachedResponse;\n\t\t\t}\n\n\t\t\tconsole.log(\"[SW] fetching and caching:\", request.url);\n\t\t\treturn fetch(request)\n\t\t\t\t.then((response) => {\n\t\t\t\t\tif (!response.ok) {\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t\"[SW] not caching failed response:\",\n\t\t\t\t\t\t\tresponse.status,\n\t\t\t\t\t\t\trequest.url,\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn response;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst responseToCache = response.clone();\n\t\t\t\t\tcaches.open(CACHE_NAME).then((cache) => {\n\t\t\t\t\t\tcache\n\t\t\t\t\t\t\t.put(request, responseToCache)\n\t\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\t\tconsole.log(\n\t\t\t\t\t\t\t\t\t\"[SW] cached successfully:\",\n\t\t\t\t\t\t\t\t\trequest.url,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t.catch((err) => {\n\t\t\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t\t\t\"[SW] failed to cache:\",\n\t\t\t\t\t\t\t\t\trequest.url,\n\t\t\t\t\t\t\t\t\terr,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t});\n\t\t\t\t\t});\n\n\t\t\t\t\treturn response;\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tconsole.error(\"[SW] fetch failed for:\", request.url, err);\n\t\t\t\t\tthrow err;\n\t\t\t\t});\n\t\t}),\n\t);\n});\n\nself.addEventListener(\"message\", (event) => {\n    if (!event.data) return;\n    const type = event.data.type;\n\n\tif (type === \"GET_CACHE_INFO\") {\n\t\tevent.waitUntil(\n\t\t\tcaches.open(CACHE_NAME).then(async (cache) => {\n\t\t\t\tconst keys = await cache.keys();\n\t\t\t\tlet totalSize = 0;\n\t\t\t\tconst files = [];\n\n\t\t\t\tfor (const request of keys) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst response = await cache.match(request);\n\t\t\t\t\t\tif (response) {\n\t\t\t\t\t\t\tconst blob = await response.blob();\n\t\t\t\t\t\t\tconst size = blob.size;\n\t\t\t\t\t\t\ttotalSize += size;\n\n\t\t\t\t\t\t\tfiles.push({\n\t\t\t\t\t\t\t\turl: request.url,\n\t\t\t\t\t\t\t\tsize: size,\n\t\t\t\t\t\t\t\ttype:\n\t\t\t\t\t\t\t\t\tresponse.headers.get(\"content-type\") ||\n\t\t\t\t\t\t\t\t\t\"unknown\",\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t\"[SW] failed to get info for cached file:\",\n\t\t\t\t\t\t\trequest.url,\n\t\t\t\t\t\t\terr,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tevent.ports[0].postMessage({\n\t\t\t\t\ttotalSize,\n\t\t\t\t\tfileCount: files.length,\n\t\t\t\t\tfiles,\n\t\t\t\t});\n\t\t\t}),\n\t\t);\n\t}\n\n\tif (type === \"CLEAR_CACHE\") {\n\t\tevent.waitUntil(\n\t\t\tcaches\n\t\t\t\t.delete(CACHE_NAME)\n\t\t\t\t.then(() => {\n\t\t\t\t\tconsole.log(\"[SW] cache cleared\");\n\t\t\t\t\treturn caches.open(CACHE_NAME);\n\t\t\t\t})\n\t\t\t\t.then(() => {\n\t\t\t\t\tevent.ports[0].postMessage({ success: true });\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tconsole.error(\"[SW] failed to clear cache:\", err);\n\t\t\t\t\tevent.ports[0].postMessage({\n\t\t\t\t\t\tsuccess: false,\n\t\t\t\t\t\terror: err.message,\n\t\t\t\t\t});\n\t\t\t\t}),\n\t\t);\n\t}\n});\n"
  },
  {
    "path": "svelte.config.js",
    "content": "import adapter from \"@sveltejs/adapter-static\";\nimport { vitePreprocess } from \"@sveltejs/vite-plugin-svelte\";\n\n/** @type {import('@sveltejs/kit').Config} */\nconst config = {\n\t// Consult https://svelte.dev/docs/kit/integrations\n\t// for more information about preprocessors\n\tpreprocess: vitePreprocess(),\n\n\tkit: {\n\t\t// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.\n\t\t// If your environment is not supported, or you settled on a specific environment, switch out the adapter.\n\t\t// See https://svelte.dev/docs/kit/adapters for more information about adapters.\n\t\tadapter: adapter(),\n\t\tpaths: {\n\t\t\trelative: false,\n\t\t},\n\t\tenv: {\n\t\t\tpublicPrefix: \"PUB_\",\n\t\t\tprivatePrefix: \"PRI_\",\n\t\t},\n\t},\n};\n\nexport default config;\n"
  },
  {
    "path": "tailwind.config.ts",
    "content": "import type { Config } from \"tailwindcss\";\nimport plugin from \"tailwindcss/plugin\";\n\nexport default {\n\tcontent: [\"./src/**/*.{html,js,svelte,ts}\"],\n\ttheme: {\n\t\textend: {\n\t\t\tbackgroundColor: {\n\t\t\t\tpanel: \"var(--bg-panel)\",\n\t\t\t\t\"panel-highlight\": \"var(--bg-panel-highlight)\",\n\t\t\t\tseparator: \"var(--bg-separator)\",\n\t\t\t\tbutton: \"var(--bg-button)\",\n\t\t\t\t\"panel-alt\": \"var(--bg-button)\",\n\t\t\t\tbadge: \"var(--bg-badge)\",\n\t\t\t},\n\t\t\tborderColor: {\n\t\t\t\tseparator: \"var(--bg-separator)\",\n\t\t\t\tbutton: \"var(--bg-button)\",\n\t\t\t},\n\t\t\ttextColor: {\n\t\t\t\tforeground: \"var(--fg)\",\n\t\t\t\tmuted: \"var(--fg-muted)\",\n\t\t\t\taccent: \"var(--fg-accent)\",\n\t\t\t\tfailure: \"var(--fg-failure)\",\n\t\t\t\t\"on-accent\": \"var(--fg-on-accent)\",\n\t\t\t\t\"on-badge\": \"var(--fg-on-badge)\",\n\t\t\t},\n\t\t\tcolors: {\n\t\t\t\taccent: \"var(--accent)\",\n\t\t\t\t\"accent-alt\": \"var(--accent-alt)\",\n\t\t\t\t\"accent-pink\": \"var(--accent-pink)\",\n\t\t\t\t\"accent-pink-alt\": \"var(--accent-pink-alt)\",\n\t\t\t\t\"accent-red\": \"var(--accent-red)\",\n\t\t\t\t\"accent-red-alt\": \"var(--accent-red-alt)\",\n\t\t\t\t\"accent-purple-alt\": \"var(--accent-purple-alt)\",\n\t\t\t\t\"accent-purple\": \"var(--accent-purple)\",\n\t\t\t\t\"accent-blue\": \"var(--accent-blue)\",\n\t\t\t\t\"accent-blue-alt\": \"var(--accent-blue-alt)\",\n\t\t\t\t\"accent-green\": \"var(--accent-green)\",\n\t\t\t\t\"accent-green-alt\": \"var(--accent-green-alt)\",\n\t\t\t},\n\t\t\tboxShadow: {\n\t\t\t\tpanel: \"var(--shadow-panel)\",\n\t\t\t},\n\t\t\tfontFamily: {\n\t\t\t\tdisplay: \"var(--font-display)\",\n\t\t\t\tbody: \"var(--font-body)\",\n\t\t\t},\n\t\t\tblur: {\n\t\t\t\txs: \"2px\",\n\t\t\t},\n\t\t\tborderRadius: {\n\t\t\t\t\"2.5xl\": \"1.25rem\",\n\t\t\t},\n\t\t},\n\t},\n\n\tplugins: [\n\t\tplugin(function ({ addVariant }) {\n\t\t\taddVariant(\"dynadark\", [\n\t\t\t\t\":root:not(.light).dark &\",\n\t\t\t\t\"@media (prefers-color-scheme: dark) { :root:not(.light) &\",\n\t\t\t]);\n\t\t}),\n\t],\n} satisfies Config;\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n\t\"extends\": \"./.svelte-kit/tsconfig.json\",\n\t\"compilerOptions\": {\n\t\t\"allowJs\": true,\n\t\t\"checkJs\": true,\n\t\t\"esModuleInterop\": true,\n\t\t\"forceConsistentCasingInFileNames\": true,\n\t\t\"resolveJsonModule\": true,\n\t\t\"skipLibCheck\": true,\n\t\t\"sourceMap\": true,\n\t\t\"strict\": true,\n\t\t\"moduleResolution\": \"bundler\"\n\t}\n\t// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias\n\t// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files\n\t//\n\t// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes\n\t// from the referenced tsconfig.json - TypeScript does not merge them in\n}\n"
  },
  {
    "path": "vite.config.ts",
    "content": "import { paraglideVitePlugin } from \"@inlang/paraglide-js\";\nimport { sveltekit } from \"@sveltejs/kit/vite\";\nimport { defineConfig, type PluginOption } from \"vite\";\nimport svg from \"@poppanator/sveltekit-svg\";\nimport wasm from \"vite-plugin-wasm\";\nimport { execSync } from \"child_process\";\n\n// coollify removes the .git folder but exposes commit via SOURCE_COMMIT env variable\nlet commitHash = process.env.SOURCE_COMMIT\n\t? process.env.SOURCE_COMMIT.substring(0, 7) // shorten it lol\n\t: \"unknown\";\n\nif (commitHash === \"unknown\") {\n\ttry {\n\t\tcommitHash = execSync(\"git rev-parse --short HEAD\").toString().trim();\n\t} catch (e) {\n\t\tconsole.warn(`Could not determine Git commit hash: ${e}`);\n\t\tcommitHash = \"unknown\";\n\t}\n}\n\nexport default defineConfig(({ command }) => {\n\tconst plugins: PluginOption[] = [\n\t\tsveltekit(),\n\t\tparaglideVitePlugin({\n\t\t\tproject: \"./project.inlang\",\n\t\t\toutdir: \"./src/lib/paraglide\",\n\t\t\tstrategy: [\"localStorage\", \"preferredLanguage\", \"baseLocale\"],\n\t\t}),\n\t\tsvg({\n\t\t\tincludePaths: [\"./src/lib/assets\"],\n\t\t\tsvgoOptions: {\n\t\t\t\tmultipass: true,\n\t\t\t\tplugins: [\n\t\t\t\t\t{\n\t\t\t\t\t\tname: \"preset-default\",\n\t\t\t\t\t\tparams: { overrides: { removeViewBox: false } },\n\t\t\t\t\t},\n\t\t\t\t\t{ name: \"removeAttrs\", params: { attrs: \"(fill|stroke)\" } },\n\t\t\t\t],\n\t\t\t},\n\t\t}),\n\t];\n\n\tif (command === \"serve\") {\n\t\tplugins.unshift(wasm());\n\t}\n\n\treturn {\n\t\tplugins,\n\t\tworker: {\n\t\t\tplugins: () => [wasm()],\n\t\t\tformat: \"es\",\n\t\t},\n\t\toptimizeDeps: {\n\t\t\texclude: [\"@ffmpeg/core-mt\", \"@ffmpeg/ffmpeg\", \"@ffmpeg/util\"],\n\t\t},\n\t\tcss: {\n\t\t\tpreprocessorOptions: {\n\t\t\t\tscss: {\n\t\t\t\t\tapi: \"modern\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tbuild: {\n\t\t\ttarget: \"esnext\",\n\t\t},\n\t\tdefine: {\n\t\t\t__COMMIT_HASH__: JSON.stringify(commitHash),\n\t\t},\n\t};\n});\n"
  }
]